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:
objecttabanlı koleksiyonların iki bedeli - Generic sınıf, arayüz ve metot yazımı; tip çıkarımı
wherekı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ıt | Sözü | Kazandırdığı |
|---|---|---|
where T : struct | T değer tipidir | null derdi yok; T? Nullable olur |
where T : class | T referans tipidir | null atanabilir, referans karşılaştırması |
where T : new() | boş kurucusu var | new T() yazılabilir (en sonda belirtilir) |
where T : TabanSinif | T, tabandan türer | tabanın tüm üyeleri kullanılabilir |
where T : IArayuz | T, arayüzü uygular | arayüz üyeleri kullanılabilir; en yaygını |
where T : notnull | T null olmayan tiptir | 2. 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üzdenIEnumerable<Kedi>birIEnumerable<Hayvan>olarak kullanılabilir. - Kontravaryans —
in T: T yalnızca giriş pozisyonundaysa (parametre) yön tersine döner: her hayvanı işleyebilenAction<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
- 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. - 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? - Depo kalıbı:
IVarlik(Guid Id) arayüzü +Depo<T> where T : IVarlik, new()sınıfını (Olustur, Ekle,List<T> Listele) yaz;MusteriveUrunile kullan. - Static tuzak: bölüm 05'teki
Sayac<T>deneyini çalıştır, çıktıyı tahminlerinle karşılaştır. - Varyans kanıtı:
Hayvan/Kedi/Kopekhiyerarş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ındaArrayTypeMismatchExceptionfı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."
