C# Generic Variance: Covariance ve Contravariance (in ve out)
List<EBook> neden List<Book> olamaz ama IEnumerable<Book> olabilir? Array covariance'ın deliği, out ile covariance, in ile contravariance, delegate'lerde variance, kuralların sınırları ve CS0266'nın önerdiği cast tuzağı.
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
Bookstore'da kitapların bir hiyerarşisi var: EBook ve AudioBook birer Book. Elimizde e-kitaplardan oluşan bir liste var ve onu dört farklı tipteki değişkene atıyoruz. Bu dört satırdan hangileri derlenir?
var ebooks = new List<EBook> { new("978-1617294532", "C# in Depth", 39.99m, 12.4) };
IEnumerable<Book> a = ebooks;
IReadOnlyList<Book> b = ebooks;
IList<Book> c = ebooks;
List<Book> d = ebooks;
record Book(string Isbn, string Title, decimal Price);
record EBook(string Isbn, string Title, decimal Price, double FileSizeMb) : Book(Isbn, Title, Price);
record AudioBook(string Isbn, string Title, decimal Price, int Minutes) : Book(Isbn, Title, Price);
Derleyicinin Cevabı
İlk iki satır derlenir, son ikisi derlenmez:
Program.cs(5,17): error CS0266: Cannot implicitly convert type 'System.Collections.Generic.List<EBook>' to 'System.Collections.Generic.IList<Book>'. An explicit conversion exists (are you missing a cast?)
Program.cs(6,16): error CS0029: Cannot implicitly convert type 'System.Collections.Generic.List<EBook>' to 'System.Collections.Generic.List<Book>'
Her EBook bir Book olduğuna göre "e-kitap listesi" de bir "kitap listesi" olmalı gibi geliyor. Derleyici buna kısmen katılıyor: e-kitapları IEnumerable<Book> ya da IReadOnlyList<Book> olarak görmene izin veriyor ama IList<Book> ya da List<Book> olarak görmene izin vermiyor. Generic bir tipin, tip argümanları arasındaki inheritance ilişkisini taşıyıp taşımamasına variance denir. Taşıyorsa covariance, ters yönde taşıyorsa contravariance, hiç taşımıyorsa invariance. Bu yazının bütün konusu, derleyicinin bu kararı neye göre verdiği.
Derlenen iki satırla devam edelim:
var ebooks = new List<EBook> { new("978-1617294532", "C# in Depth", 39.99m, 12.4) };
IEnumerable<Book> books = ebooks;
IReadOnlyList<Book> shelf = ebooks;
Console.WriteLine(shelf[0].Title);
Console.WriteLine(ReferenceEquals(books, ebooks));
object unknown = ebooks;
Console.WriteLine(unknown is IEnumerable<Book>);
record Book(string Isbn, string Title, decimal Price);
record EBook(string Isbn, string Title, decimal Price, double FileSizeMb) : Book(Isbn, Title, Price);
record AudioBook(string Isbn, string Title, decimal Price, int Minutes) : Book(Isbn, Title, Price);
C# in Depth
True
True
ReferenceEquals'in true dönmesi önemli: variance dönüşümü yeni bir collection üretmez, kopyalamaz, wrap etmez. Aynı object'e başka bir interface tipinden bakarsın. Son satırdaki is kontrolü de gösteriyor ki bu kural sadece derleyicide değil, runtime'da da geçerli.
Array'lerdeki Eski Delik
Generic'lerden önce C#'ta da bir tür covariance vardı ve hâlâ duruyor: array'ler. EBook[]'i Book[]'e atayabilirsin. Sorun, Book[] üzerinden yazma yapmaya çalıştığında çıkar:
Book[] shelf = new EBook[2];
shelf[0] = new EBook("978-1617294532", "C# in Depth", 39.99m, 12.4);
try
{
shelf[1] = new AudioBook("978-1491950357", "Building Microservices", 29.99m, 840);
}
catch (ArrayTypeMismatchException ex)
{
Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
}
record Book(string Isbn, string Title, decimal Price);
record EBook(string Isbn, string Title, decimal Price, double FileSizeMb) : Book(Isbn, Title, Price);
record AudioBook(string Isbn, string Title, decimal Price, int Minutes) : Book(Isbn, Title, Price);
ArrayTypeMismatchException: Attempted to access an element as a type incompatible with the array.
Derleyici açısından shelf bir Book[] ve AudioBook da bir Book; atama kusursuz görünüyor. Ama arkadaki object bir EBook[] ve içine sesli kitap giremez. Runtime bunu her array yazmasında kontrol eder ve uyuşmazlıkta exception atar. Bu kontrol de bedava değildir: reference type array'lere yapılan atamalar genellikle bu tip kontrolünden geçer. Array covariance, .NET'in ilk sürümünden beri Java benzeri dillerle uyum için var olan ve bugün çoğu kişinin tasarım hatası saydığı bir özellik. Generic collection'lar aynı hatayı tekrarlamamak için daha sıkı bir kuralla tasarlandı.
List<T> Neden Esnemez
Bir an için List<EBook>'yi List<Book>'e atayabildiğimizi düşünelim. List<Book>'in Add(Book) method'u var ve AudioBook bir Book olduğu için Add'e sesli kitap verebilirsin. Bu durumda e-kitap listesine sesli kitap girmiş olurdu ve sonra listeyi List<EBook> olarak okuyan kod, FileSizeMb'si olmayan bir object ile karşılaşırdı. Array'de runtime'ın yakaladığı hatayı generic tiplerde derleyici hiç başlatmaz.
Peki IEnumerable<T> neden serbest? Çünkü onun üzerinden içeri hiçbir şey koyamazsın, sadece dışarı alırsın. Bir e-kitap sequence'ından çıkan her şey bir e-kitaptır, dolayısıyla bir kitaptır. Okuyana yalan söylenmiş olmaz. Kural buradan çıkıyor: T'yi yalnızca dışarı veren bir tip covariant olabilir, T'yi yalnızca içeri alan bir tip contravariant olabilir, ikisini birden yapan tip invariant kalmak zorundadır. IList<T>'nin hem indexer'dan T döndürmesi hem de Add(T) alması onu invariant yapıyor.
out: Covariance
C#'ta bir interface ya da delegate'in tip parametresini out ile işaretlersen, onun covariant olduğunu söylersin. .NET'te IEnumerable<out T>, IEnumerator<out T>, IReadOnlyCollection<out T>, IReadOnlyList<out T> ve Func<out TResult> böyle tanımlanmıştır. Tahmin sorusunda geçen iki satırın arkasında bu var. Kendi interface'inde de aynı işareti kullanabilirsin, ama derleyici sözünü tutup tutmadığını denetler. Bir rafa hem kitap alma hem kitap koyma yeteneği verip out dersen:
interface IShelf<out T>
{
int Count { get; }
T GetAt(int index);
void Put(T item);
}
derleyici Put'u reddeder:
Program.cs(5,14): error CS1961: Invalid variance: The type parameter 'T' must be contravariantly valid on 'IShelf<T>.Put(T)'. 'T' is covariant.
Mesaj tam olarak yukarıdaki mantığı söylüyor: Put'ta T bir girdi pozisyonunda, ama sen T'yi covariant ilan ettin. Çözüm, okuma ve yazma contract'larını ayırmak. Interface sadece okumayı tanımlar, yazma yeteneği somut class'ta kalır:
var ebookShelf = new Shelf<EBook>(new[]
{
new EBook("978-1617294532", "C# in Depth", 39.99m, 12.4),
new EBook("978-1492056812", "Programming C# 10", 49.99m, 18.1),
});
IShelf<Book> anyShelf = ebookShelf;
PrintTitles(anyShelf);
static void PrintTitles(IShelf<Book> shelf)
{
for (int i = 0; i < shelf.Count; i++)
Console.WriteLine(shelf.GetAt(i).Title);
}
interface IShelf<out T>
{
int Count { get; }
T GetAt(int index);
}
class Shelf<T> : IShelf<T>
{
private readonly List<T> _items;
public Shelf(IEnumerable<T> items) => _items = new List<T>(items);
public int Count => _items.Count;
public T GetAt(int index) => _items[index];
public void Put(T item) => _items.Add(item);
}
record Book(string Isbn, string Title, decimal Price);
record EBook(string Isbn, string Title, decimal Price, double FileSizeMb) : Book(Isbn, Title, Price);
record AudioBook(string Isbn, string Title, decimal Price, int Minutes) : Book(Isbn, Title, Price);
C# in Depth
Programming C# 10
PrintTitles herhangi bir kitap rafını kabul ediyor ve e-kitap rafı da artık o kapıdan geçebiliyor. .NET'in kendi tasarımı da aynı: List<T> mutable ve invariant, onun okuma yüzü olan IReadOnlyList<out T> ise covariant. Önceki yazılarda "dışarıya IReadOnlyList<T> ver" derken bir kazancı daha almış oluyorsun: çağıran taraf onu daha genel bir tip olarak da kullanabiliyor.
in: Contravariance
Ters yönü düşün. Kitapları fiyata göre karşılaştıran bir comparer, e-kitapları da karşılaştırabilir mi? Elbette; her e-kitabın bir fiyatı var. T'yi sadece girdi olarak alan tiplerde dönüşüm ters yönde güvenlidir: Book ile çalışabilen bir şey, EBook ile de çalışabilir. Bunu in ile işaretlersin. IComparer<in T>, IEqualityComparer<in T>, IComparable<in T> ve Action<in T> böyle tanımlıdır:
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
var ebooks = new List<EBook>
{
new("978-1492056812", "Programming C# 10", 49.99m, 18.1),
new("978-1617294532", "C# in Depth", 39.99m, 12.4),
new("978-1098107703", "Head First C#", 44.99m, 52.0),
};
IComparer<Book> byPrice = Comparer<Book>.Create((x, y) => x.Price.CompareTo(y.Price));
ebooks.Sort(byPrice);
Action<Book> print = book => Console.WriteLine($"{book.Price} {book.Title}");
Action<EBook> printEBook = print;
ebooks.ForEach(printEBook);
IValidator<EBook> validator = new PriceValidator();
Console.WriteLine(validator.IsValid(ebooks[0]));
interface IValidator<in T>
{
bool IsValid(T item);
}
class PriceValidator : IValidator<Book>
{
public bool IsValid(Book item) => item.Price > 0;
}
record Book(string Isbn, string Title, decimal Price);
record EBook(string Isbn, string Title, decimal Price, double FileSizeMb) : Book(Isbn, Title, Price);
record AudioBook(string Isbn, string Title, decimal Price, int Minutes) : Book(Isbn, Title, Price);
39.99 C# in Depth
44.99 Head First C#
49.99 Programming C# 10
True
ebooks.Sort bir IComparer<EBook> bekliyor, biz ona IComparer<Book> verdik. Action<Book> türündeki print de Action<EBook> değişkenine sorunsuz atandı. Kendi yazdığımız IValidator<in T> ile de aynı şeyi yaptık: kitap doğrulayıcısı, e-kitap doğrulayıcısı yerine geçebiliyor. Bunun pratik getirisi şu: genel kuralları bir kez, taban tip için yazarsın ve her alt tip için ayrı comparer, validator ya da handler üretmek zorunda kalmazsın.
Akılda tutmanın kolay yolu adların kendisi: out parametresi sadece dışarı çıkar, in parametresi sadece içeri girer. Bir şey üreten tip (producer) covariant, tüketen tip (consumer) contravariant olur.

Delegate'lerde Variance
Delegate'ler de interface'ler gibi variant tip parametrelerine sahip olabilir. Func<in T, out TResult> ikisini bir arada gösterir: parametre tarafında contravariant, dönüş tarafında covariant:
Func<EBook> createEBook = () => new EBook("978-1617294532", "C# in Depth", 39.99m, 12.4);
Func<Book> createBook = createEBook;
Console.WriteLine(createBook().Title);
Func<Book, string> describe = book => $"{book.Title} ({book.Isbn})";
Func<EBook, object> describeEBook = describe;
Console.WriteLine(describeEBook(new EBook("978-1492056812", "Programming C# 10", 49.99m, 18.1)));
record Book(string Isbn, string Title, decimal Price);
record EBook(string Isbn, string Title, decimal Price, double FileSizeMb) : Book(Isbn, Title, Price);
record AudioBook(string Isbn, string Title, decimal Price, int Minutes) : Book(Isbn, Title, Price);
C# in Depth
Programming C# 10 (978-1492056812)
E-kitap üreten bir fonksiyon, kitap üreten bir fonksiyonun yerine geçebilir. Kitap alıp string döndüren bir fonksiyon da e-kitap alıp object döndüren bir fonksiyonun yerine geçebilir; iki yönde birden esnedik. Event handler'lar, callback'ler ve LINQ operatörlerine verdiğin lambda'lar bu sayede taban tiplerle yazılabilir. Delegate'ler serinin ileri seviye yazılarında ayrıca ele alınacak.
Kuralların Sınırları
Variance sadece interface ve delegate'lerin tip parametrelerinde vardır. Bir class'a out yazmayı denersen:
class Box<out T>
{
}
Program.cs(1,11): error CS1960: Invalid variance modifier. Only interface and delegate type parameters can be specified as variant.
Class'lar ve struct'lar her zaman invariant'tır. Bu yüzden variant bir API tasarlamak istediğinde ortak noktayı bir interface'e çıkarırsın, tıpkı IShelf<out T>'de yaptığımız gibi.
İkinci sınır, variance'ın sadece reference type'lar arasında çalışması. int bir object'e dönüşebilir ama bu dönüşüm boxing gerektirir, yani yeni bir object üretir:
List<int> stocks = new() { 3, 0, 5 };
IEnumerable<object> boxes = stocks;
Program.cs(2,29): error CS0266: Cannot implicitly convert type 'System.Collections.Generic.List<int>' to 'System.Collections.Generic.IEnumerable<object>'. An explicit conversion exists (are you missing a cast?)
Variance dönüşümü aynı object'e başka bir gözle bakmaktır; int element'lerin her birini box ederek yeni bir collection üretmek bu tanımın dışında kalır. Böyle bir dönüşüme ihtiyacın varsa LINQ'in Cast<object>() ya da Select operatörüyle açıkça yeni bir sequence üretirsin. Aynı sebeple IEnumerable<EBook> → IEnumerable<Book> çalışırken, EBook bir struct olsaydı çalışmazdı.
Sık Düşülen Tuzaklar
"An explicit conversion exists" mesajına inanmak. Tahmin sorusundaki CS0266 bir cast öneriyor. Cast'i yazarsan kod derlenir, ama List<EBook> gerçekten IList<Book>'yi implemente etmediği için runtime'da patlar:
var ebooks = new List<EBook> { new("978-1617294532", "C# in Depth", 39.99m, 12.4) };
try
{
IList<Book> books = (IList<Book>)ebooks;
}
catch (InvalidCastException ex)
{
Console.WriteLine(ex.GetType().Name);
}
record Book(string Isbn, string Title, decimal Price);
record EBook(string Isbn, string Title, decimal Price, double FileSizeMb) : Book(Isbn, Title, Price);
InvalidCastException
Derleyici bu cast'e izin veriyor, çünkü teoride List<EBook>'den derive eden ve IList<Book>'yi de implemente eden bir class olabilir. Doğru çözüm, ihtiyacın olan şeyi ifade eden covariant interface'i istemek ya da ebooks.Cast<Book>().ToList() ile açıkça yeni bir liste oluşturmak.
Her okuma interface'inin covariant olduğunu sanmak. IReadOnlyDictionary<TKey, TValue> read-only ama invariant; TryGetValue'daki out TValue parametresi ve key'i girdi olarak alan method'lar buna izin vermiyor:
var byIsbn = new Dictionary<string, EBook>();
IReadOnlyDictionary<string, Book> catalog = byIsbn;
record Book(string Isbn, string Title, decimal Price);
record EBook(string Isbn, string Title, decimal Price, double FileSizeMb) : Book(Isbn, Title, Price);
Program.cs(2,45): error CS0266: Cannot implicitly convert type 'System.Collections.Generic.Dictionary<string, EBook>' to 'System.Collections.Generic.IReadOnlyDictionary<string, Book>'. An explicit conversion exists (are you missing a cast?)
Böyle bir durumda method'un aslında neye ihtiyacı olduğuna bak. Sadece kitapları dolaşacaksa byIsbn.Values'u IEnumerable<Book> olarak verebilirsin.
Array covariance'ı güvenli sanmak. Book[] parametresi alan bir method'a EBook[] verebilirsin; method içeride array'e yazarsa runtime'da ArrayTypeMismatchException alırsın. Sadece okuyacaksan parametreyi IReadOnlyList<Book> yap.
Value type'larda variance beklemek. IEnumerable<int>, IEnumerable<object> değildir; IEnumerable<decimal> de IEnumerable<IComparable> değildir.
Küçük Bir Deneme
★☆☆ IEnumerable<AudioBook> alan bir değişkene List<Book> atamayı dene. Derleyici ne diyor ve neden?
★★☆ IShelf<out T>'ye T? FindByIsbn(string isbn) method'unu ekle. Derleniyor mu? Sonra bool Contains(T item) eklemeyi dene ve hata mesajını oku.
★★★ IHandler<in T> adında, void Handle(T item) method'u olan bir interface yaz. Book için bir loglama handler'ı, EBook için bir indirme linki handler'ı oluştur ve ikisini de List<IHandler<EBook>> içinde topla.
Bu yazıyla serinin C# 10 ve .NET 6 üzerine kurulu kemik yazıları tamamlandı: generic'ler, constraint'ler, temel collection'lar, interface'ler, immutable tipler ve variance. Bundan sonraki iki yazı sürüm yazısı. İlkinde C# 11–12 ve .NET 7–8 ile gelenlere bakıyoruz: static abstract member'larla generic math, Frozen collection'lar ve [1, 2, 3] şeklinde yazılan collection expression'lar. Her başlıkta önce C# 10'da nasıl yazdığımızı, sonra yeni sürümün neyi değiştirdiğini göreceğiz.