Mert Özen Her Satırda Daha İleri
Backend C# ile Nesne Yönelimli Programlama — #07

C#'ta Interface: Çoklu ve Explicit Implementation, Default Interface Method

Mert Özen 7 Kas 2026 10 dk 2 görüntülenme
C#'ta Interface: Çoklu ve Explicit Implementation, Default Interface Method

Interface bir contract: CS0535 ve CS0144, bir class'ın birden çok interface'i implement etmesi, aynı isimli member'lar için explicit implementation ve BCL'deki örnekleri, yalnızca interface üzerinden çağrılan default interface method'lar, abstract class ile seçim ve saati interface'e alarak test edilebilir kod.

Sürüm notu: Bu yazıdaki kodlar C# 10 ve .NET 6 API'leriyle yazılmıştır; .NET 10 SDK ile derlenip denenmiştir.

Önce Tahmin Et

IShippable bir interface ve ShippingCost method'unun body'sini kendisi taşıyor. Book bu interface'i uyguluyor ama ShippingCost'u yazmıyor; gerek yok, varsayılan body zaten var. Aynı kitabı bir kez interface üzerinden, bir kez kendi tipiyle çağırıyoruz. Hangi satır sorun çıkarır?

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

Book dune = new Book("Dune", 0.6m);
IShippable shippable = dune;

Console.WriteLine(shippable.ShippingCost());
Console.WriteLine(dune.ShippingCost());

public interface IShippable
{
    decimal WeightKg { get; }

    decimal ShippingCost() => WeightKg <= 1m ? 3.00m : 6.00m;
}

public class Book : IShippable
{
    public string Title { get; }
    public decimal WeightKg { get; }

    public Book(string title, decimal weightKg)
    {
        Title = title;
        WeightKg = weightKg;
    }
}

Önceki yazıda base class'tan gelen bir method'u derived class üzerinden rahatça çağırabiliyorduk. Interface'te de öyle mi? Cevap default interface method'ları anlattığımız bölümde.

Interface Bir Contract

Interface, bir tipin neler yapabileceğini söyleyen ama nasıl yapacağını çoğu zaman söylemeyen bir contract'tır (sözleşme). "Kargolanabilir olan her şeyin bir ağırlığı vardır ve kargo ücretini hesaplayabilir" cümlesi bir interface'tir. Bir class bu interface'i implement ettiğinde contract'ı imzalamış olur ve derleyici contract'taki her maddenin yerine getirildiğini denetler:

IShippable shippable = new IShippable();

public interface IShippable
{
    decimal WeightKg { get; }
    decimal ShippingCost();
}

public class Mug : IShippable
{
    public decimal WeightKg => 0.4m;
}
Program.cs(9,20): error CS0535: 'Mug' does not implement interface member 'IShippable.ShippingCost()'

Mug contract'ı imzaladı ama ShippingCost maddesini yerine getirmedi. Bu hatayı düzeltince ilk satır sıraya giriyor: error CS0144: Cannot create an instance of the abstract type or interface 'IShippable'. Abstract class gibi interface'ten de object oluşturulamaz. Ama interface tipinde değişken tanımlayabilirsin; o değişken, contract'ı implement eden herhangi bir object'i gösterebilir.

Birkaç kural: interface member'ları varsayılan olarak public'tir, bu yüzden yanlarına access modifier yazılmaz. Interface instance field tutamaz; durum, onu implement eden class'ın işidir. İsimler geleneksel olarak büyük I ile başlar ve çoğu zaman bir yetenek anlatır: IShippable, IComparable<T>, IDisposable, IEnumerable<T>.

Çoklu Implementasyon

Bir class yalnızca bir base class'tan türeyebilir ama istediği kadar interface implement edebilir. Bookstore'da bazı ürünler kargolanır, bazıları indirime girer, bazıları ikisini de yapar:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

List<Product> cart = new List<Product>
{
    new Book("Dune", 45.90m, 0.6m),
    new EBook("Clean Code", 29.90m),
    new Mug("Bookstore Mug", 12.50m)
};

foreach (IDiscountable item in cart.OfType<IDiscountable>())
{
    item.ApplyDiscount(10);
}

decimal shipping = 0;
foreach (Product product in cart)
{
    if (product is IShippable shippable)
    {
        shipping += shippable.ShippingCost();
    }

    Console.WriteLine($"{product.Name}: {product.Price}, shippable: {product is IShippable}");
}

Console.WriteLine($"Shipping: {shipping}");

public interface IShippable
{
    decimal WeightKg { get; }
    decimal ShippingCost();
}

public interface IDiscountable
{
    decimal Price { get; }
    void ApplyDiscount(int percent);
}

public abstract class Product
{
    public string Name { get; }
    public decimal Price { get; protected set; }

    protected Product(string name, decimal price)
    {
        Name = name;
        Price = price;
    }
}

public class Book : Product, IShippable, IDiscountable
{
    public decimal WeightKg { get; }

    public Book(string name, decimal price, decimal weightKg) : base(name, price)
    {
        WeightKg = weightKg;
    }

    public decimal ShippingCost()
    {
        return WeightKg <= 1m ? 3.00m : 6.00m;
    }

    public void ApplyDiscount(int percent)
    {
        Price -= Price * percent / 100;
    }
}

public class EBook : Product, IDiscountable
{
    public EBook(string name, decimal price) : base(name, price)
    {
    }

    public void ApplyDiscount(int percent)
    {
        Price -= Price * percent / 100;
    }
}

public class Mug : Product, IShippable
{
    public decimal WeightKg => 0.4m;

    public Mug(string name, decimal price) : base(name, price)
    {
    }

    public decimal ShippingCost()
    {
        return 7.50m;
    }
}
Dune: 41.31, shippable: True
Clean Code: 26.91, shippable: False
Bookstore Mug: 12.50, shippable: True
Shipping: 10.50

Class bildiriminde önce base class, sonra virgülle interface'ler yazılır. İndirim döngüsü yalnızca IDiscountable olanlara uğradı; LINQ'in OfType<T>() operatörü list'ten o tipe uyanları süzüyor. Kupa indirime girmedi, e-kitap kargo ücretine eklenmedi. Döngüler ürünlerin concrete tipini bilmiyor; yalnızca hangi contract'ı imzaladıklarına bakıyor.

İnce bir ayrıntı: IDiscountable bir Price property'si istiyor ve Book bunu kendisi yazmadı. Product'tan inherit ettiği public Price contract'ın o maddesini karşılıyor. Interface, member'ın nereden geldiğine bakmaz; class'ın dışarıya o isimde ve tipte bir public member sunması yeter.

Explicit Implementation

İki interface aynı isimde ama farklı anlamda bir member isterse ne olur? Kataloğa göre bir kitabın kodu ISBN'idir, depoya göre rafın kodu. Explicit implementation ile her interface'e ayrı cevap verirsin:

Book dune = new Book("978-0441172719", "A-12");

Console.WriteLine(((ICatalogItem)dune).Code);
Console.WriteLine(((IInventoryItem)dune).Code);

IInventoryItem item = dune;
Console.WriteLine(item.Code);

List<int> numbers = new List<int>();
Console.WriteLine(((ICollection<int>)numbers).IsReadOnly);

ICollection<int> readOnly = numbers.AsReadOnly();
Console.WriteLine(readOnly.IsReadOnly);
readOnly.Add(42);

public interface ICatalogItem
{
    string Code { get; }
}

public interface IInventoryItem
{
    string Code { get; }
}

public class Book : ICatalogItem, IInventoryItem
{
    public string Isbn { get; }
    public string Shelf { get; }

    public Book(string isbn, string shelf)
    {
        Isbn = isbn;
        Shelf = shelf;
    }

    string ICatalogItem.Code => Isbn;
    string IInventoryItem.Code => $"SHELF-{Shelf}";
}
978-0441172719
SHELF-A-12
SHELF-A-12
False
True
Unhandled exception. System.NotSupportedException: Collection is read-only.
   at System.Collections.ObjectModel.ReadOnlyCollection`1.System.Collections.Generic.ICollection<T>.Add(T value)

Explicit member'ın önünde access modifier yoktur ve adının başında interface adı yazar: string ICatalogItem.Code. Bu member'lar class'ın kendi yüzeyinde görünmez; yalnızca interface tipindeki bir referans üzerinden çağrılır. dune.Code yazmayı denediğimde derleyici CS1061 ile "'Book' does not contain a definition for 'Code'" dedi.

Explicit implementation'ın ikinci kullanımı, bir member'ı contract gereği sunmak ama tipin günlük yüzeyini kalabalıklaştırmamak. BCL bunu sık yapar. List<T>, ICollection<T>.IsReadOnly'yi explicit implement eder; numbers.IsReadOnly yazdığında da aynı CS1061'i alırsın, çünkü bir list için cevap hep false'tur ve kimsenin onu sormasına gerek yoktur. Üçüncü yazıdaki ReadOnlyCollection<T>'in sırrı da bu: Add'i explicit implement eder ve çağrıldığında exception fırlatır. Stack trace'teki ReadOnlyCollection`1.System.Collections.Generic.ICollection<T>.Add adı, explicit member'ın derlenmiş adını gösteriyor.

Default Interface Method

Şimdi tahminin cevabı. İlk program derlenmedi:

Program.cs(9,24): error CS1061: 'Book' does not contain a definition for 'ShippingCost' and no accessible extension method 'ShippingCost' accepting a first argument of type 'Book' could be found (are you missing a using directive or an assembly reference?)

C# 8 ile interface'ler body'li member'lar taşıyabilir hâle geldi; buna default interface method (default interface member) denir. Ama bu member class'a inherit edilmez, interface'te kalır. Interface üzerinden shippable.ShippingCost() çalışır, dune.ShippingCost() çalışmaz. Base class'tan farkı tam olarak bu: base class kendi member'larını derived class'a verir, interface ise yalnızca kendi üzerinden çağrılana cevap verir.

Bu özelliğin asıl amacı versiyonlama. Yüzlerce class'ın implement ettiği bir interface'e yeni bir member eklemek eskiden hepsini kırardı. Varsayılan body'yle eklersen mevcut class'lar değişmeden derlenmeye devam eder, isteyen kendi versiyonunu yazar:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

List<IShippable> parcels = new List<IShippable>
{
    new Book("Dune", 0.6m),
    new Book("Encyclopedia Set", 7.2m),
    new Mug("Bookstore Mug", 0.4m)
};

foreach (IShippable parcel in parcels)
{
    Console.WriteLine($"{parcel.Name}: {parcel.ShippingCost()}, signature: {parcel.RequiresSignature()}");
}

public interface IShippable
{
    string Name { get; }
    decimal WeightKg { get; }

    decimal ShippingCost() => WeightKg <= 1m ? 3.00m : 6.00m;

    bool RequiresSignature() => WeightKg > 5m;
}

public class Book : IShippable
{
    public string Name { get; }
    public decimal WeightKg { get; }

    public Book(string name, decimal weightKg)
    {
        Name = name;
        WeightKg = weightKg;
    }
}

public class Mug : IShippable
{
    public string Name { get; }
    public decimal WeightKg { get; }

    public Mug(string name, decimal weightKg)
    {
        Name = name;
        WeightKg = weightKg;
    }

    public decimal ShippingCost()
    {
        return 7.50m;
    }
}
Dune: 3.00, signature: False
Encyclopedia Set: 6.00, signature: True
Bookstore Mug: 7.50, signature: False

RequiresSignature sonradan eklendi ve ne Book ne Mug değişti. Mug kendi ShippingCost'unu yazdığı için interface'in varsayılanı onun için devre dışı kaldı. Burada override kelimesi yok; class'ta aynı signature'a sahip public bir method olması yeterli. Bir not: bu özellik runtime desteği ister ve .NET Core 3.0 ile geldi; .NET Framework'te çalışmaz. Default method'u ortak kod koymak için bir çoklu inheritance aracı gibi kullanmaya başlarsan, işini abstract class'ın yapması gereken bir yere geldin demektir.

Interface mi, Abstract Class mı

İkisi de "bu member'ları sağla" der, ama ağırlıkları farklı. Abstract class bir aile kurar: ortak durum (field'lar), constructor, ortak kod ve doldurulacak boşluklar. Bir class bu aileye ancak tek bir kapıdan girer. Interface ise bir yetenek tarif eder; hiçbir aile ilişkisi kurmadan birbirinden bağımsız tiplere aynı rolü verir. Bir kitap ile bir kupa aynı aileden gelmeyebilir ama ikisi de kargolanabilir.

Pratik bir ölçü: tipler arasında paylaşılan durum ve kod varsa abstract class, paylaşılan yalnızca bir rol ya da davranış contract'ıysa interface. İkisi birlikte de çalışır, bu yazıdaki Book : Product, IShippable, IDiscountable gibi. Şüphede kalırsan interface ile başla; ileride base class'a geçmek, bir base class'ı söküp interface'e dönmekten çok daha ucuzdur.

Interface'e Bağlı Kod Test Edilebilir

Interface'lerin en büyük günlük faydası, kodu concrete bağımlılıklardan ayırması. Bookstore'da saat 21:00'den sonra yüzde 20 gece indirimi var. DateTime.Now'ı doğrudan kullanan bir method'u test etmek için gece yarısını beklemen gerekirdi. Saati bir interface'in arkasına alalım:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

Checkout morning = new Checkout(new FixedClock(new DateTime(2026, 11, 7, 10, 0, 0)));
Checkout evening = new Checkout(new FixedClock(new DateTime(2026, 11, 7, 21, 30, 0)));
Checkout now = new Checkout(new SystemClock());

Console.WriteLine(morning.Total(45.90m));
Console.WriteLine(evening.Total(45.90m));
Console.WriteLine(now.Total(45.90m) > 0);

public interface IClock
{
    DateTime Now { get; }
}

public class SystemClock : IClock
{
    public DateTime Now => DateTime.Now;
}

public class FixedClock : IClock
{
    public DateTime Now { get; }

    public FixedClock(DateTime now)
    {
        Now = now;
    }
}

public class Checkout
{
    private readonly IClock _clock;

    public Checkout(IClock clock)
    {
        _clock = clock;
    }

    public decimal Total(decimal price)
    {
        bool isNightSale = _clock.Now.Hour >= 21;
        return isNightSale ? price * 0.80m : price;
    }
}
45.90
36.7200
True

Checkout saatin nereden geldiğini bilmiyor; yalnızca bir IClock istiyor ve onu constructor'dan alıyor. Uygulamada SystemClock, testte istediğin anı döndüren FixedClock verirsin. Bu, dependency injection'ın özüdür; ASP.NET Core'daki container'ı .NET Core Web API serisinin dependency injection yazısında anlattık. .NET 8 ile BCL'e bu iş için hazır bir TimeProvider tipi geldi; onu asenkron programlama serisinin .NET 8 yazısında göreceğiz.

Sık Düşülen Tuzaklar

Her class'a bir interface. BookService'in yanında tek implementasyonu olan, hiç test edilmeyen ve hiç değiştirilmeyecek bir IBookService yalnızca dosya sayısını ikiye katlar. Interface'i bir ihtiyaç doğurduğunda ekle: ikinci bir implementasyon, test için sahte bir versiyon ya da bir module sınırı.

Şişman interface'ler. Kırk member'lı bir IProductOperations'ı implement eden her class, ihtiyacı olmayan otuz method için NotImplementedException yazmaya başlar. Küçük, tek bir rolü anlatan interface'ler daha iyidir; SOLID'in I'sı (interface segregation) bunu söyler.

Default method'u class üzerinden çağırmayı beklemek. Tahmin örneğindeki CS1061 bunun sonucuydu. Değişken interface tipinde değilse varsayılan body'ye ulaşamazsın.

Explicit implementation'ı gizleme aracı sanmak. Explicit member'lar yalnızca yüzeyden kaldırılır; cast eden herkes onlara ulaşır. Gerçekten gizli olması gereken bir şey private olmalı.

Küçük Bir Deneme

★☆☆ IGiftWrappable adında, decimal WrappingFee() method'u olan bir interface yaz. Book ve Mug implement etsin, EBook etmesin. Sepetteki paketlenebilir ürünlerin toplam paket ücretini OfType<IGiftWrappable>() ile hesapla.

★★☆ IShippable'a string Carrier() => "Standard"; varsayılan method'unu ekle. Ağırlığı 5 kg'ı geçen ürünler için "Freight" dönen kendi versiyonunu yalnızca bir class'ta yaz ve parcels döngüsünde sonucu yazdır.

★★★ FixedClock yerine, her Now okunduğunda saati 1 saat ileri alan bir SteppingClock yaz. Aynı Checkout ile arka arkaya 24 kez Total çağır ve hangi saatlerde indirim uygulandığını yazdır. Saatin bir interface'in arkasında olması bu deneyi nasıl mümkün kıldı?

Şimdiye kadar yazdığımız bütün tipler class'tı ve hepsi reference type'tı. C#'ta bir de değer gibi davranan, atandığında kopyalanan tipler var. Bir sonraki yazıda struct'a geçiyoruz: class ile farkı, kopyalama semantiğinin tuzakları, readonly struct ve bir tipi ne zaman struct yapman gerektiği.