Skip to content

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/override mekanizması ve geç bağlama — çağrının çalışma zamanında gerçek tipe göre çözülmesi
  • new ile gizlemenin (method hiding) farkı — klasik sınav tuzağı
  • ToString override; abstract sı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

  1. Geçen dersin Kisi'sini abstract yap ve public 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.
  2. 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.
  3. İki sınıfa da ToString override'ı yaz; Console.WriteLine(nesne) çıktısının tip adından anlamlı metne döndüğünü doğrula.
  4. Tuzak deneyi: Egitmen'deki bir override'ı geçici olarak new'e çevir; Kisi referansı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.
  5. Genişleme testi — polimorfizmin asıl kanıtı: sisteme Veli : Kisi ekle (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."