Skip to content

Bölüm 5 / 13

Generics

Generic sınıf/metot, kısıtlar (where), varyans.

9 dakikalık okuma

Hat 01'den beri List<T> yazıyorsun; bu ders köşeli parantezin arkasındaki makineyi söküyor. Generics'in hangi iki problemi (tip güvensizliği + boxing) çözmek için doğduğunu, kendi generic tipini ve metodunu nasıl yazacağını, where kısıtlarının derleyiciyle pazarlık dilini, .NET'in her kapalı tip için ayrı kod üreten çalışma zamanı modelini ve mülakatın gözde konusu varyansı (in/out) işliyor.

01Ne öğreneceksin

  • Generics'in varlık nedeni: object tabanlı koleksiyonların iki bedeli
  • Generic sınıf, arayüz ve metot yazımı; tip çıkarımı
  • where kısıtları: derleyiciye T hakkında söz vermek
  • Çalışma zamanı modeli: kapalı tip başına kod, static alan tuzağı
  • Varyans: out/in, dizilerin bozuk kovaryansı, List neden değişmez

02Varlık nedeni: object'in iki bedeli

C# 1.0'da (2002) tek koleksiyon ailesi vardı ve her şeyi object olarak tutuyordu. 1. derste maliyetin yarısını gördün — boxing; öbür yarısı daha sinsidir — tip güvensizliği:

var liste = new ArrayList();
liste.Add(42);
liste.Add("kırk iki");                  // derleyici sesini çıkarmaz — her şey object

int toplam = 0;
foreach (object o in liste)
    toplam += (int)o;                   // "kırk iki"ye gelince: InvalidCastException
                                        // hata DERLEMEDE değil ÜRETİMDE patladı

Hat 01'den beri taşıdığın ilke — "hatayı derleme zamanına çek" — burada çiğneniyor: yanlış tipin eklendiği an değil, okunduğu an patlıyor. C# 2.0'ın cevabı generics: tipi, kullanım yerinde parametre olarak veriyorsun — List<int> — ve iki bedel birden ortadan kalkıyor: Add("kırk iki") artık derleme hatası, elemanlar çıplak int olarak durduğu için boxing sıfır. T bir tip parametresidir: metot parametresi değer alır, tip parametresi tip alır; List<T> açık (open) şablondur, List<int> kapalı (closed/constructed) tiptir. Bugün generic olmayan koleksiyonların tek kullanım alanı eski kod bakımıdır — yeni kodda görürsen kod incelemesi notudur.

03Kendi generic tipin ve metodun

Generic tip yazmak, "tipten bağımsız olan mantığı bir kez yazmak"tır. Klasik başlangıç, iki elemanlı bir çift:

public class Cift<TIlk, TIkinci>                  // birden çok tip parametresi
{
    public TIlk Ilk { get; }
    public TIkinci Ikinci { get; }
    public Cift(TIlk ilk, TIkinci ikinci) { Ilk = ilk; Ikinci = ikinci; }
}

var kayit = new Cift<int, string>(1001, "Ayşe");   // Dictionary<TKey,TValue> ile aynı kalıp

Metotlar da bağımsızca generic olabilir — ve çoğu zaman tip parametresini yazmana gerek kalmaz, derleyici argümandan çıkarır (type inference):

public static void Degistir<T>(ref T a, ref T b)
{
    T gecici = a; a = b; b = gecici;
}

int x = 1, y = 2;
Degistir(ref x, ref y);            // Degistir<int> yazmadın — çıkarıldı
Degistir(ref ad1, ref ad2);        // aynı kod string için de çalışıyor

Arayüzler de aynı mekanizmayla genericleşir ve BCL'nin omurgası tam budur: IEnumerable<T>, IComparable<T>, IEquatable<T>... 1. derste "struct'larda Equals için IEquatable<T> uygulanır" demiştik; nedeni artık okunur: generic olmayan Equals(object) değer tipini kutular, Equals(T) kutulamaz.

04where kısıtları: derleyiciye söz vermek

Kısıtsız bir T hakkında derleyici hiçbir şey bilmez — bu yüzden T üzerinde yalnızca object'in üyelerini çağırabilirsin. where kısıtı bir pazarlıktır: T'nin ne olabileceğini daraltırsın, karşılığında T'nin üyelerini kullanma hakkı kazanırsın:

public class Depo<T> where T : IVarlik, new()
{                                  // söz: T bir IVarlik'tir VE boş kurucusu vardır
    public T Olustur()
    {
        var varlik = new T();      // new() kısıtı olmasa DERLENMEZDİ
        varlik.Id = Guid.NewGuid();// IVarlik kısıtı Id'yi kullandırıyor
        return varlik;
    }
}
KısıtSözüKazandırdığı
where T : structT değer tipidirnull derdi yok; T? Nullable olur
where T : classT referans tipidirnull atanabilir, referans karşılaştırması
where T : new()boş kurucusu varnew T() yazılabilir (en sonda belirtilir)
where T : TabanSinifT, tabandan türertabanın tüm üyeleri kullanılabilir
where T : IArayuzT, arayüzü uygulararayüz üyeleri kullanılabilir; en yaygını
where T : notnullT null olmayan tiptir2. dersin NRT dünyasıyla uyum (sözlük anahtarları)

İki tamamlayıcı araç: default(T) — kısıtı bilinmeyen T'nin "sıfır değeri" (referansta null, değerde sıfırlanmış veri; kısayolu default) ve kısıt zinciri sözdizimi: where TKey : notnull where TDeger : class, new() — her tip parametresine ayrı satır.

05Çalışma zamanı: her kapalı tip ayrı bir gerçek

C# generics'i Java'dakinden ayıran mülakat sorusu şudur: .NET'te generics silinmez (type erasure yok), somutlaşır (reification). Çalışma zamanı her kapalı tip için gerçek bir tip üretir; değer tipleri için özelleşmiş makine kodu çıkarır — List<int>'in içinde çıplak int'ler durur, kutu yoktur; referans tipleri tek kod gövdesini paylaşır ama tip kimlikleri ayrıdır. Görünür sonuçları:

Console.WriteLine(typeof(List<int>) == typeof(List<string>));   // False — ayrı tipler

public class Sayac<T>
{
    public static int Adet;        // TUZAK: her kapalı tipe AYRI kopya!
}
Sayac<int>.Adet++;
Sayac<int>.Adet++;
Console.WriteLine(Sayac<string>.Adet);   // 0 — "ortak sayaç" sanan yanılır

Static alan tuzağı gerçek hataların kaynağıdır: generic sınıfa konan static önbellek, tip başına ayrı önbelleğe bölünür — bazen istenen tam budur (tip başına yapılandırma), bazen sızıntılı sürpriz. Kural: generic sınıfta static alan yazarken "bunun T başına ayrı kopyasını mı istiyorum?" sorusunu bilinçli cevapla.

06Varyans: in, out ve dizilerin günahı

Soru masum görünür: List<Kedi>, bir List<Hayvan> yerine geçebilir mi? Sezgi "evet" der (her kedi hayvandır — Hat 01'in 12-13. dersleri), tip sistemi "hayır" der ve haklıdır: geçseydi, List<Hayvan> diye tuttuğun listeye Kopek ekleyebilirdin — kedilerin listesine köpek girerdi. Tehlike yazma yönünden gelir; o hâlde kural yönle ilgilidir:

  • Kovaryans — out T: T yalnızca çıkış pozisyonunda (dönüş değeri) kullanılıyorsa güvenlidir: IEnumerable<out T>'den yalnız okursun, bu yüzden IEnumerable<Kedi> bir IEnumerable<Hayvan> olarak kullanılabilir.
  • Kontravaryans — in T: T yalnızca giriş pozisyonundaysa (parametre) yön tersine döner: her hayvanı işleyebilen Action<Hayvan>, kedi işleyen yerde (Action<Kedi> beklenen) güvenle kullanılabilir — IComparer<in T> aynı mantık.
  • Değişmezlik (invariance): List<T> hem okur hem yazar — iki yön birden olduğundan hiçbir varyansa izin veremez. Varyans yalnız interface ve delegate'lerde bildirilebilir.
IEnumerable<Kedi> kediler = KedileriGetir();
IEnumerable<Hayvan> hayvanlar = kediler;    // out T sayesinde derlenir

// Dizilerin BOZUK kovaryansı — tarihî hata, farkında ol:
Hayvan[] siru = new Kedi[3];                // derlenir (C# 1.0 mirası)!
siru[0] = new Kopek();                      // çalışma zamanında ArrayTypeMismatchException

Dizi örneği, kuralın neden var olduğunun kanıtıdır: diziler kovaryansı tip güvenliğinden ödün vererek aldı ve hata derlemeden çalışma zamanına kaçtı — tam da generics'in düzeltmeye geldiği şey. Mülakatta "varyans nedir" sorusunun yıldız cevabı bu üçlüdür: out = üretici, in = tüketici, ikisi birden = değişmez.

07Sahada generics: nerede yazarsın, nerede abartırsın

Günlük işte generic tüketirsin (koleksiyonlar, LINQ — 6. ders bunun üstüne kurulu); generic yazmak şu sinyallerde gündeme gelir: aynı mantığın yalnız tip değiştirerek tekrarlandığını gördüğünde (kopyala-yapıştır ikizi metotlar) ve ortak davranışı tabana almanın kalıtımla çözülemediği yerde. Kurumsal kodun klasiği, Hat 04 ve 07'de kuracağın Repository<T> where T : class kalıbıdır — CRUD mantığı bir kez yazılır, her varlık tipi için çalışır. Ters uyarı da kıdemin parçası: üç tip parametreli, beş kısıtlı bir imza çoğu zaman tasarımın karmaşıklık çığlığıdır; generic, soyutlama ihtiyacı kanıtlanınca yazılır, "belki lazım olur" diye değil.

08Mini alıştırma — Depo'yu kur, varyansı kanıtla

Yaklaşık 20 dakika · dotnet run

  1. Tip güvenliği farkı: bölüm 02'deki ArrayList kazasını aynen yaz, çalışma zamanında patlat; sonra List<int>'e çevirip hatanın derleme zamanına taşındığını gör.
  2. Generic metot: T EnBuyuk<T>(List<T> liste) where T : IComparable<T> yaz (döngü + CompareTo). int, string ve DateTime listeleriyle çağır — tip çıkarımının seni tek kez bile <int> yazdırmadığına dikkat et. Kısıtı silip aldığın derleme hatasını oku: derleyici T hakkında neden hiçbir şey bilmiyor?
  3. Depo kalıbı: IVarlik (Guid Id) arayüzü + Depo<T> where T : IVarlik, new() sınıfını (Olustur, Ekle, List<T> Listele) yaz; Musteri ve Urun ile kullan.
  4. Static tuzak: bölüm 05'teki Sayac<T> deneyini çalıştır, çıktıyı tahminlerinle karşılaştır.
  5. Varyans kanıtı: Hayvan/Kedi/Kopek hiyerarşisini kur; (a) IEnumerable<Kedi> → IEnumerable<Hayvan> atamasının derlendiğini, (b) List<Kedi> → List<Hayvan>'ın derlenmediğini, (c) dizi kovaryansının derlenip çalışma zamanında ArrayTypeMismatchException fırlattığını üç ayrı denemeyle gör.

09Kendini sına

Generics hangi iki problemi çözmek için eklendi? Her ikisini ArrayList üzerinden örnekle.

Tip güvensizliği ve boxing. ArrayList her şeyi object tutar: listeye yanlışlıkla string eklemek derlemede yakalanmaz, okuma anında InvalidCastException olarak üretimde patlar — hata derleme zamanından çalışma zamanına kaçar. Ayrıca her değer tipi ekleme bir kutu tahsisidir — milyon elemanda milyon heap nesnesi ve GC baskısı. List<int> ikisini birden çözer: yanlış tip derleme hatasıdır, elemanlar çıplak int olarak durur.

where kısıtı neden gerekir? struct, class, new() ve interface kısıtlarının her birinin kazandırdığını söyle.

Kısıtsız T hakkında derleyici yalnız object'in üyelerini bilir; kısıt, T'nin ne olabileceğini daraltarak karşılığında üye kullanım hakkı verir. struct: T değer tipidir (null derdi yok, T? Nullable olur); class: T referans tipidir (null atanabilir); new(): boş kurucu garantisi — new T() yazılabilir; interface/taban kısıtı: o tipin üyeleri T üzerinde çağrılabilir — pratikte en yaygını budur (where T : IComparable<T> ile CompareTo kullanmak gibi).

.NET generics'inin "somutlaşma" (reification) modeli ne demektir? Static alan tuzağıyla bağını kur.

Çalışma zamanı her kapalı tip için gerçek, ayrı bir tip üretir — silme (erasure) yoktur; değer tiplerine özelleşmiş kod çıkar, bu yüzden List<int> kutusuz çalışır ve typeof(List<int>) ≠ typeof(List<string>). Sonucu: generic sınıftaki static alan, kapalı tip başına ayrı kopyadır — Sayac<int>.Adet ile Sayac<string>.Adet bağımsız sayaçlardır. "Tüm tipler için ortak" sanılan static önbellek böyle bölünür; generic sınıfta static yazarken bu soru bilinçli cevaplanır.

out ve in varyans işaretleri neyi bildirir? List<T> neden ikisini de alamaz ve dizi kovaryansı neden "bozuk"tur?

out T (kovaryans): T yalnız çıkış pozisyonundadır — üretici arayüz; IEnumerable<Kedi>, IEnumerable<Hayvan> yerine geçer. in T (kontravaryans): T yalnız giriştedir — tüketici; Action<Hayvan>, Action<Kedi> beklenen yerde kullanılır. List<T> hem okur hem yazar; kovaryant olsaydı List<Hayvan> diye tutulan kedi listesine köpek eklenebilirdi — bu yüzden değişmezdir. Diziler bu güvenliği C# 1.0'da feda etti: Hayvan[] = new Kedi[3] derlenir, yanlış eklemenin cezası çalışma zamanında ArrayTypeMismatchException olarak kesilir — hatanın derlemeden kaçışının ders kitabı örneği.

10Sınav köşesi

Yazılıda nasıl sorulur

(1) Kavram: "Generic yapıların faydası nedir?" — beklenen çift cevap: derleme zamanı tip güvenliği + boxing'siz performans. (2) Sözdizimi: kısıt eşleme — "new T() yazabilmek için hangi kısıt gerekir?" (new()), "T'nin değer tipi olmasını hangisi garantiler?" (struct). (3) Çıktı sorusu: static alanlı generic sınıf — Sayac<int> artırılıp Sayac<string> okutulur, cevap 0. (4) Doğru/yanlış: "List<Kedi>, List<Hayvan>'a atanabilir" (yanlış), "IEnumerable<Kedi>, IEnumerable<Hayvan>'a atanabilir" (doğru — out). (5) ArrayList/List<T> karşılaştırması eski müfredat sorusu olarak hâlâ gelir.

Sözlüde nasıl anlatılır — 2 dakikalık taslak

"Generics nedir, neden var?" için düzen: (1) Tarihsel gerekçe — "C# 1.0'ın object koleksiyonları iki bedel ödetiyordu: yanlış tip üretimde patlıyordu ve her değer tipi kutulanıyordu; generics tipi parametre yapıp ikisini derleme zamanında çözdü." (2) Kullanım katmanı — "kendi generic tipimi tekrar eden mantık kanıtlanınca yazarım; where kısıtlarıyla derleyiciye T'nin yeteneklerini bildiririm" + Repository<T> örneği. (3) Derinlik vuruşu — ".NET'te generics Java'daki gibi silinmez, somutlaşır: kapalı tip başına gerçek tip, değer tipine özel kod, static alanın tip başına kopyası" — bu karşılaştırma mülakatta seviye atlatır. (4) Kapanış: varyans üçlüsü — "out üretici, in tüketici, hem okuyup hem yazan List değişmez; dizilerin bozuk kovaryansı bu kuralın neden var olduğunun kanıtıdır."