Bölüm 13 / 15
Polimorfizm
virtual/override, abstract sınıflar.
8 dakikalık okuma
Geçen ders açık bir soruyla bitti: üst tip referansından
yapılan çağrıda, nesnenin gerçek tipi kendi davranışını konuşturabilir mi? Cevap
polimorfizmdir — "çok biçimlilik": aynı çağrı, nesnenin gerçek tipine göre farklı
çalışır. Bu derste mekanizmayı (virtual/override), en
bilinen tuzağını (new ile gizleme), ToString'i ve
abstract sınıfları kuracaksın. Mülakatların en çok kod örneği istenen
konusu budur.
01Ne öğreneceksin
virtual/overridemekanizması ve geç bağlama — çağrının çalışma zamanında gerçek tipe göre çözülmesinewile gizlemenin (method hiding) farkı — klasik sınav tuzağıToStringoverride;abstractsınıf ve metot; abstract-virtual farkı- Tip sorgusunun (
is) neden kod kokusu olduğu ve polimorfizmin onu nasıl ortadan kaldırdığı
02Sorun: tip sorgusu merdiveni
Kurs sistemindeki List<Kisi>'de herkes kendine göre tanıtılsın:
öğrenci notunu, eğitmen branşını söylesin. Polimorfizm bilmeyen kod bunu tip sorgusuyla
çözer:
foreach (var kisi in herkes)
{
if (kisi is Ogrenci o) // is: "gerçek tipin bu mu?" sorgusu
{
Console.WriteLine($"{o.Ad} — öğrenci, not: {o.Not}");
}
else if (kisi is Egitmen e)
{
Console.WriteLine($"{e.Ad} — eğitmen, branş: {e.Brans}");
}
}
Çalışır — ve iki yapısal kusuru vardır. Birincisi: davranış, sahibi olan sınıflardan
koparılıp dışarıdaki bir merdivene taşındı; "eğitmen kendini nasıl tanıtır" bilgisinin
evi artık Egitmen değil. İkincisi ve daha kötüsü: sisteme Veli tipi
eklendiğinde bu merdiveni — ve kod tabanındaki tüm benzerlerini — bulup dal eklemen
gerekir; birini kaçırırsan Veli sessizce yazdırılmaz. Var olan kodu değiştirmeden
yeni tür ekleyebilmek, Hat 04'te OCP (açık-kapalı ilkesi) adıyla kurallaşacak;
mekanizması bu derste.
03virtual / override: geç bağlama
Çözüm iki anahtar kelimedir: temel sınıf metodu virtual ile "türevler
bunu değiştirebilir" diye işaretler; türev, override ile kendi sürümünü
koyar:
class Kisi
{
public string Ad { get; }
public Kisi(string ad) { Ad = ad; }
public virtual void KendiniTanit() // virtual: değiştirilebilir
{
Console.WriteLine($"Ben {Ad}");
}
}
class Ogrenci : Kisi
{
public int Not { get; private set; }
public Ogrenci(string ad, int not) : base(ad) { Not = not; }
public override void KendiniTanit() // override: kendi sürümüm
{
Console.WriteLine($"{Ad} — öğrenci, not: {Not}");
}
}
class Egitmen : Kisi
{
public string Brans { get; }
public Egitmen(string ad, string brans) : base(ad) { Brans = brans; }
public override void KendiniTanit()
{
base.KendiniTanit(); // istersen önce orijinali çağır
Console.WriteLine($" Branş: {Brans}");
}
}
var herkes = new List<Kisi> { new Ogrenci("Aysel", 85), new Egitmen("Deniz", "Yazılım") };
foreach (var kisi in herkes)
{
kisi.KendiniTanit(); // değişkenin tipi Kisi — ama HERKES KENDİ sürümünü çalıştırır
}
Kritik cümle şu: kisi.KendiniTanit() çağrısında hangi metodun
çalışacağına derleyici değil, çalışma zamanı karar verir — değişkenin tipine
değil, o an gösterdiği nesnenin gerçek tipine bakarak. Buna geç bağlama
(late binding / dynamic dispatch) denir; virtual olmayan çağrılar ise derlemede,
değişkenin tipine göre bağlanır (erken bağlama). Merdiven yok oldu: yeni
Veli tipi eklemek, yalnızca Veli sınıfını yazıp override
etmekten ibaret — foreach'e ve başka hiçbir koda dokunulmaz. base.
çağrısı da geçen dersten tanıdık: türev, orijinal davranışın üstüne ekleme yapmak
isterse kullanır.
04Tuzak: override yerine new
Türevde override yerine new yazılırsa (ya da hiçbir şey
yazılmazsa — derleyici uyarı verip new varsayar) metot ezilmez, gizlenir: iki
ayrı metot yan yana yaşar ve hangisinin çalışacağını değişkenin tipi belirler.
Sınavların en sevdiği fark budur:
class Kisi
{
public virtual string Rol() => "kişi";
}
class OverrideliOgrenci : Kisi
{
public override string Rol() => "öğrenci"; // ezme: tek metot, gerçek tip kazanır
}
class NewliOgrenci : Kisi
{
public new string Rol() => "öğrenci"; // gizleme: iki metot, değişken tipi kazanır
}
Kisi a = new OverrideliOgrenci();
Kisi b = new NewliOgrenci();
Console.WriteLine(a.Rol()); // "öğrenci" — override: nesnenin gerçek tipi konuştu
Console.WriteLine(b.Rol()); // "kişi" — new: değişken Kisi tipinde, Kisi'ninki çalıştı
Aynı görünen iki sınıf, üst tip referansından farklı davranıyor — tehlike tam
olarak bu sessizliktedir. Pratik kural: davranış değiştirmek istiyorsan her zaman
virtual/override çifti kullan; new ile gizleme bilinçli ve nadir bir
araçtır, günlük kodda görünüyorsa çoğunlukla unutulmuş bir override'dır.
Derleyicinin "member hides inherited member" uyarısını asla susturarak geçme —
uyarı, bu tabloyu sana hatırlatıyor.
05ToString: her nesnenin sözcüsü
Geçen ders: her sınıf object'ten türer ve ToString()
devralır — varsayılanı tip adını basar, bu yüzden Console.WriteLine(aysel)
ekrana Ogrenci yazar. ToString object'te virtual'dır;
yani az önce öğrendiğin mekanizmayla düzeltilir:
class Ogrenci : Kisi
{
// ...
public override string ToString() => $"{Ad} (öğrenci, not {Not})";
}
Console.WriteLine(aysel); // Aysel (öğrenci, not 85)
Console.WriteLine($"Kayıt: {aysel}"); // interpolation da ToString'i çağırır
WriteLine, interpolation, hata ayıklayıcının değer gösterimi —
hepsi ToString'e sorar. Alışkanlık olarak her veri taşıyan sınıfına anlamlı bir
ToString yaz; loglarında Ogrenci yerine Aysel (öğrenci, not
85) görmek, hata ayıklamada (Hat 15) saat kazandırır.
06abstract: gövdesiz sözleşme, yarım kalıp
Kisi sınıfının kendisi hiç nesne olmayacaksa — sistemde herkes ya öğrenci ya
eğitmense — "çıplak Kisi" yaratılabilmesi bir açıktır. abstract bunu
çözer: sınıf yalnızca temel olarak var olur, new'lenemez;
abstract işaretli metotları ise gövdesizdir ve türevler override etmek
zorundadır:
abstract class Kisi
{
public string Ad { get; }
protected Kisi(string ad) { Ad = ad; } // kurucu: türevlerin kullanımına
public abstract string Rol(); // gövde YOK — her türev yazmak zorunda
public virtual void KendiniTanit() // abstract sınıfta normal üye de olur
{
Console.WriteLine($"{Ad} ({Rol()})"); // gövdesiz metodu şimdiden KULLANABİLİR
}
}
class Ogrenci : Kisi
{
public Ogrenci(string ad) : base(ad) { }
public override string Rol() => "öğrenci"; // yazmazsan DERLEME HATASI
}
// var k = new Kisi("X"); → DERLEME HATASI: abstract sınıf örneklenemez
Kisi referans = new Ogrenci("Aysel"); // referans tipi olarak kullanım serbest
KendiniTanit'e dikkat: gövdesi olmayan Rol()'ü çağırıyor —
çalışma zamanında nesne mutlaka bir türev olacağı için orada gerçek bir Rol hep
bulunur. Bu, şablon gücüdür: temel sınıf akışın iskeletini yazar, boşlukları
türevlere bıraktırır. Ayrımı netle: virtual "varsayılanım var, istersen değiştir";
abstract "benim sürümüm yok, yazmak zorundasın". Abstract metot yalnızca
abstract sınıfta olabilir; abstract sınıfın normal üyeleri, alanları, kurucuları
olabilir — "hiç gövdesi olmayan saf sözleşme" ihtiyacı ise bir sonraki dersin konusu:
interface.
07is hâlâ ne zaman meşru?
Polimorfizm varken 02. bölümdeki is merdiveni kod kokusudur — davranış
sınıflara dağıtılmalıdır. Ama is'in meşru kullanımları da vardır:
sınırda, tipin gerçekten bilinmediği yerde tek seviyeli kontrol
(dışarıdan gelen object'i süzmek gibi) ve C#'ın modern desen eşleme
sözdizimi (if (kisi is Ogrenci { Not: >= 70 } basarili) — tip + koşulu
tek satırda süzer; ayrıntısı Hat 03 cs-modern'de). Ölçüt şudur: is'ten sonra
davranış seçiyorsan polimorfizm eksik demektir; veri süzüyorsan meşru olabilir.
Mülakatta "is/switch ile tip kontrolü yerine ne kullanırdınız?" sorusunun beklediği
cevap bu derstir.
08Mini alıştırma — bildirim sistemi
Yaklaşık 20 dakika · geçen dersin kalitim projesine devam
- Geçen dersin
Kisi'siniabstractyap vepublic abstract string Rol();ekle;KendiniTanit'i virtual yapıp Rol'ü kullanacak biçimde düzenle. Ogrenci ve Egitmen'de Rol'ü override et;new Kisi(...)satırının artık derlenmediğini gör. - Ogrenci'nin KendiniTanit'ini override et (not bilgisini eklesin,
base.KendiniTanit()ile başlasın).List<Kisi>üzerinden foreach'le çağır; her tipin kendi sürümünün çalıştığını gözle. - İki sınıfa da
ToStringoverride'ı yaz;Console.WriteLine(nesne)çıktısının tip adından anlamlı metne döndüğünü doğrula. - Tuzak deneyi: Egitmen'deki bir override'ı geçici olarak
new'e çevir;Kisireferansından çağırıp davranışın değiştiğini gör, derleyici uyarısını oku, override'a geri dön. Gördüğün farkı 04. bölümün tablosuyla bir cümlede açıkla. - Genişleme testi — polimorfizmin asıl kanıtı: sisteme
Veli : Kisiekle (Rol: "veli"; KendiniTanit override'ı serbest). Listeye bir veli koy ve foreach dahil mevcut hiçbir satıra dokunmadan programın doğru çalıştığını doğrula. 02. bölümdeki is merdiveniyle bu adımın maliyetini kıyasla.
09Kendini sına
virtual/override çağrısında hangi metodun çalışacağına ne zaman ve neye göre karar verilir?
Çalışma zamanında, değişkenin tipine değil nesnenin gerçek
tipine göre (geç bağlama / dynamic dispatch). Kisi k = new Ogrenci(...)
için k.KendiniTanit(), Ogrenci'nin override'ını çalıştırır. Virtual
olmayan metotlarda bağlama derlemede, değişkenin tipine göre yapılır.
override ile new (gizleme) arasındaki davranış farkını üst tip referansı üzerinden anlat.
override tek metodu ezer: üst tip referansından da çağrılsa
nesnenin gerçek tipinin sürümü çalışır. new iki ayrı metot yaratır (gizleme): hangi
sürümün çalışacağını değişkenin tipi belirler — Kisi b = new NewliOgrenci()
için b.Rol() Kisi'ninkini çalıştırır. Davranış değiştirmenin doğru yolu
virtual/override'dır; new bilinçli ve nadir kullanılır, derleyicinin gizleme uyarısı
susturulmaz.
abstract metot ile virtual metot arasındaki fark nedir? Abstract sınıfın iki temel özelliği nedir?
virtual metodun bir varsayılan gövdesi vardır, türev isterse override eder; abstract metodun gövdesi yoktur, türev override etmek zorundadır (etmezse derleme hatası). Abstract sınıf: (1) new ile örneklenemez — yalnızca temel sınıf ve referans tipi olarak yaşar; (2) normal üyeler, alanlar ve kurucular içerebilir, hatta gövdeli metotları henüz gövdesiz abstract metotları çağırabilir (şablon gücü).
foreach içinde is/else if merdiveniyle tip kontrolü neden kod kokusudur? Polimorfizm bunu nasıl çözer?
İki sebep: davranış, sahibi olan sınıflardan koparılıp dış merdivene taşınır; ve her yeni tip, kod tabanındaki tüm merdivenlere dal eklemeyi gerektirir — biri unutulursa hata sessizdir. Polimorfizmde davranış sınıfın kendisinde (override) yaşar; yeni tip eklemek yalnızca yeni sınıfı yazmaktır, çağıran kod (foreach) hiç değişmez. Bu, açık-kapalı ilkesinin (Hat 04) mekanizmasıdır.
10Sınav köşesi
Yazılıda nasıl sorulur
Bu konu yazılıda neredeyse her zaman çıktı sorusudur ve iki kurulumla gelir: (1) virtual/override'lı hiyerarşi, üst tip referansından çağrı — "ekrana ne yazılır?"; cevabın anahtarı gerçek tipin kazanması. (2) Aynı kurulumun new'li sürümü — bu kez değişken tipi kazanır; iki soruyu yan yana çözebilen sınıfı geçer. Tanım tarafında: "Türetilmiş sınıfın temel sınıf metodunu yeniden tanımlamasına ne denir?" (override), "Gövdesiz metot hangi tür sınıfta tanımlanır?" (abstract), "Abstract sınıf new'lenebilir mi?" (hayır).
Sözlüde nasıl anlatılır — 2 dakikalık taslak
"Polimorfizm nedir?" sorusunda tanımı 20 saniyede geç, kalan süreyi mekanizma ve değere ver: (1) Tanım — aynı çağrının nesnenin gerçek tipine göre farklı davranması. (2) Mekanizma — virtual/override, kararın çalışma zamanında verildiği; new ile gizlemenin farkını kendiliğinden söyle, en sevilen takip sorusudur. (3) Değer — tip sorgusu merdivenlerini yok eder: "yeni tür eklerken var olan koda dokunmam; davranış sınıfın kendisinde yaşar" — OCP bağlantısını bir cümleyle kur. (4) abstract'ı konumlandırarak kapat: "Temel sınıf nesneleşmeyecekse abstract yaparım; zorunlu davranışı abstract metotla sözleşmeye çeviririm."
