İçeriğe geç / Skip to content / Zum Inhalt
Ahmet Balaman LogoAhmet Balaman

C# Kalıtım ve Polymorphism: virtual, override, new Rehberi

Ahmet Balaman

5 dk okuma

.NETC#OOPKalıtımPolymorphismSınav Soruları
C# Kalıtım ve Polymorphism: virtual, override, new Rehberi

Kalıtım (inheritance), bir sınıfın başka bir sınıfın üyelerini devralıp üzerine kendi davranışını eklemesidir. Polymorphism ise aynı metot çağrısının, nesnenin gerçek tipine göre farklı kod çalıştırmasıdır. İkisi birlikte şu sorunu çözer: on farklı çalışan tipiniz olsa bile maaş hesaplayan döngüyü bir kez yazarsınız ve yeni tip eklendiğinde o döngüye dokunmazsınız. Bu yazıda base, constructor zinciri, virtual/override ile new farkı, sealed, tip dönüşümleri ve sınavların vazgeçilmezi "bu kod ne yazdırır?" sorusunu sırayla ele alıyorum.

Kalıtımın Temeli: Ortak Olanı Yukarı Taşı

Access modifiers yazısında protected anlatılırken küçük bir kalıtım örneği görmüştük. Şimdi konuyu baştan kuralım. Bir bordro sistemi düşünün: her çalışanın adı ve temel maaşı var, ama maaş hesabı role göre değişiyor.

public class Calisan
{
    public string Ad { get; }
    protected decimal TemelMaas { get; }

    public Calisan(string ad, decimal temelMaas)
    {
        Ad = ad;
        TemelMaas = temelMaas;
    }

    public virtual decimal MaasHesapla() => TemelMaas;

    public override string ToString() => $"{Ad}: {MaasHesapla():N0} TL";
}

public class Yonetici : Calisan
{
    public decimal Prim { get; }

    public Yonetici(string ad, decimal temelMaas, decimal prim)
        : base(ad, temelMaas)
    {
        Prim = prim;
    }

    public override decimal MaasHesapla() => base.MaasHesapla() + Prim;
}

Yonetici : Calisan ifadesi "yönetici bir çalışandır" demek. Yonetici, Ad ve TemelMaas üyelerini yeniden yazmadan devralıyor. TemelMaas protected olduğu için türeyen sınıf görebiliyor, dışarıdaki kod göremiyor.

base ve Constructor Zinciri

base anahtar kelimesinin iki kullanımı var. Birincisi constructor'da: : base(ad, temelMaas) taban sınıfın constructor'ını çağırır. İkincisi metot içinde: base.MaasHesapla() taban sınıftaki sürümü çalıştırır, böylece o kodu kopyalamak yerine üzerine eklersiniz.

Sınavlarda sık sorulan kısım çalışma sırasıdır. Nesne oluşturulurken önce taban sınıfın constructor'ı, sonra türeyen sınıfınki çalışır:

public class Arac
{
    public Arac() => Console.WriteLine("1) Arac constructor");
}

public class Otomobil : Arac
{
    public Otomobil() => Console.WriteLine("2) Otomobil constructor");
}

var otomobil = new Otomobil();
// 1) Arac constructor
// 2) Otomobil constructor

Mantığı basit: türeyen sınıf, taban sınıfın alanlarına güvenerek çalışır; o alanların önce hazır olması gerekir. : base(...) yazmazsanız derleyici taban sınıfın parametresiz constructor'ını çağırmayı dener. Constructor yazısında gördüğümüz : this(...) ile aynı söz dizimi; biri aynı sınıftaki, diğeri taban sınıftaki constructor'a gider.

Bu sıranın pratik bir sonucu var: taban sınıfın constructor'ı içinde sanal bir metot çağırmayın. Çağrı türeyen sınıftaki override'a gider, ama o anda türeyen sınıfın constructor gövdesi henüz çalışmamıştır; metot, orada atanacak değerleri varsayılan halleriyle (null, 0) görür. Derleyici bunu engellemez, hata çalışma zamanında sessizce ortaya çıkar.

virtual ve override: Gerçek Polymorphism

Bir metodun türeyen sınıfta değiştirilebilmesi için taban sınıfta virtual (ya da abstract) olarak işaretlenmesi, türeyen sınıfta da override ile yazılması gerekir. Bu durumda hangi kodun çalışacağına değişkenin tipi değil, nesnenin gerçek tipi karar verir:

Calisan calisan = new Yonetici("Elif", 60000m, 15000m);
Console.WriteLine(calisan.MaasHesapla());   // 75000

Değişkenin tipi Calisan, ama bellekteki nesne bir Yonetici. Çağrı çalışma zamanında çözüldüğü için Yonetici.MaasHesapla çalışır. ToString içindeki MaasHesapla() çağrısı da aynı kurala uyar; taban sınıfta yazılmış olması sonucu değiştirmez.

new ile Metot Gizleme ve Klasik Sınav Sorusu

Türeyen sınıfta aynı isimli metodu override yerine new ile yazarsanız taban sınıftaki metodu ezmezsiniz, sadece gizlersiniz. Bu durumda hangi metodun çalışacağına derleme zamanında, değişkenin tipine bakılarak karar verilir. İşte hemen her OOP sınavında çıkan soru:

public class A
{
    public virtual void Yaz() => Console.WriteLine("A.Yaz");
    public void Goster() => Console.WriteLine("A.Goster");
}

public class B : A
{
    public override void Yaz() => Console.WriteLine("B.Yaz");
    public new void Goster() => Console.WriteLine("B.Goster");
}

A nesne = new B();
nesne.Yaz();
nesne.Goster();
((B)nesne).Goster();

Bu kod ne yazdırır? Cevap:

B.Yaz
A.Goster
B.Goster

Yaz sanal olduğu için nesnenin gerçek tipine (B) gidilir. Goster sanal değil; değişken A tipinde olduğundan A.Goster çalışır. Aynı nesneyi B'ye dönüştürdüğünüzde derleyici artık B.Goster'i görür. Ezberlenecek kural: override nesneye bakar, new değişkene bakar.

Zorlaştırılmış hali de sorulur:

public class A2 { public virtual void F() => Console.WriteLine("A"); }
public class B2 : A2 { public override void F() => Console.WriteLine("B"); }
public class C2 : B2 { public new virtual void F() => Console.WriteLine("C"); }
public class D2 : C2 { public override void F() => Console.WriteLine("D"); }

A2 a = new D2();
a.F();   // B

C2 c = new D2();
c.F();   // D

C2 içindeki new virtual, override zincirini koparıp yeni bir zincir başlatır. A2 tipindeki değişken eski zinciri görür ve o zincirin en son halkası B2'dir. C2 tipindeki değişken yeni zinciri görür ve D2'ye ulaşır. Gerçek projelerde böyle kod yazmayın, ama kâğıt üzerinde izleyebilmeniz gerekir. Kod izleme becerisi algoritma sınavlarında da aynı şekilde işe yarar; binary search tree sınav soruları yazısındaki ekleme ve dolaşma soruları iyi bir alıştırmadır.

sealed: Kalıtımı Durdurmak

sealed bir sınıfa yazılırsa o sınıftan türetme yapılamaz. Bir override metoda yazılırsa daha alttaki sınıfların o metodu tekrar ezmesi engellenir:

public sealed class Stajyer : Calisan
{
    public Stajyer(string ad) : base(ad, 20000m) { }

    public override decimal MaasHesapla() => TemelMaas * 0.8m;
}

// public class KidemliStajyer : Stajyer { }
// CS0509: 'KidemliStajyer': cannot derive from sealed type 'Stajyer'

.NET'teki string sınıfı da sealed'dır. Türetilmesini beklemediğiniz sınıfları mühürlemek niyetinizi açıkça belli eder.

Upcasting, Downcasting, is ve as

Türeyen tipten taban tipe dönüşüm (upcasting) her zaman güvenlidir ve örtük yapılır. Tersi (downcasting) risklidir, çünkü her çalışan bir yönetici değildir:

Calisan calisan = new Yonetici("Elif", 60000m, 15000m);   // upcasting
Calisan stajyer = new Stajyer("Can");

// Yonetici hata = (Yonetici)stajyer;   // Çalışma zamanında InvalidCastException

Yonetici? belki = stajyer as Yonetici;   // Hata fırlatmaz, null döner

if (calisan is Yonetici yonetici)        // Kontrol + dönüşüm tek adımda
    Console.WriteLine($"{yonetici.Ad} primi: {yonetici.Prim:N0} TL");

Modern C#'ta tercih edilen yol pattern matching'dir. switch ifadesiyle tip ve özellik kontrolünü birlikte yapabilirsiniz:

static string Unvan(Calisan c) => c switch
{
    Yonetici { Prim: > 10000m } => "Üst düzey yönetici",
    Yonetici => "Yönetici",
    Stajyer => "Stajyer",
    _ => "Çalışan"
};

Bir uyarı: kodunuzun her yerinde tip kontrolü yapıyorsanız bu genellikle eksik bir virtual metodun işaretidir. Davranışı sınıfın içine taşımak çoğu zaman daha temizdir.

Mini Senaryo: Bordro Listesi

Parçaları birleştirelim. Ay sonunda tüm ekibin maaşını hesaplayan kod:

List<Calisan> ekip =
[
    new Calisan("Deniz", 40000m),
    new Yonetici("Elif", 60000m, 15000m),
    new Stajyer("Can")
];

decimal toplam = 0m;
foreach (Calisan kisi in ekip)
{
    Console.WriteLine(kisi);
    toplam += kisi.MaasHesapla();
}
Console.WriteLine($"Toplam bordro: {toplam:N0} TL");

Çıktı:

Deniz: 40.000 TL
Elif: 75.000 TL
Can: 16.000 TL
Toplam bordro: 131.000 TL

Döngüde tek bir if yok. Her nesne kendi maaşını nasıl hesaplayacağını biliyor. Yarın Danisman sınıfı eklenirse sadece o sınıfı yazarsınız; döngü, rapor ve toplam aynı kalır.

Ne Zaman Kullanılır, Ne Zaman Kullanılmaz?

Kalıtım, iki sınıf arasında gerçek bir "bir çeşit" (is-a) ilişkisi varsa ve türeyen sınıf, taban sınıfın kullanıldığı her yerde sorunsuz kullanılabiliyorsa doğru araçtır. Sadece kod tekrarından kaçmak için kalıtım kurmak ise en yaygın tasarım hatasıdır: taban sınıftaki her değişiklik bütün türeyenleri etkiler ve üç dört seviyelik hiyerarşilerde kimin neyi ezdiğini takip etmek zorlaşır.

Alternatif composition'dır, yani "sahiptir" (has-a) ilişkisi. "Kalıtım yerine composition'ı tercih et" önerisi buradan gelir. Örneğin prim hesabı role değil şirket politikasına bağlıysa, bunu sınıf hiyerarşisine gömmek yerine dışarıdan verin:

public interface IPrimPolitikasi
{
    decimal Hesapla(decimal temelMaas);
}

public class SabitPrim(decimal tutar) : IPrimPolitikasi
{
    public decimal Hesapla(decimal temelMaas) => tutar;
}

public class Bordro(IPrimPolitikasi primPolitikasi)
{
    public decimal Hesapla(decimal temelMaas) =>
        temelMaas + primPolitikasi.Hesapla(temelMaas);
}

Bordro bir prim politikasına sahip; politika değişince yeni bir alt sınıf değil, yeni bir politika nesnesi verirsiniz. Birbirine akraba olmayan tiplerin ortak yeteneği için de kalıtım değil interface gerekir; ayrımı interface ve abstract class farkı yazısında anlattım.

Sık Yapılan Hatalar

1. override yazmayı unutmak

Belirti: derleyici hata değil, CS0108 ya da sanal metotlarda CS0114 uyarısı verir ("hides inherited member"); program derlenir ama Calisan tipindeki değişken üzerinden taban sınıfın metodu çalışır ve maaşlar yanlış çıkar. Çözüm: niyetiniz ezmekse override yazın. Uyarıları görmezden geçmeyin.

2. virtual olmayan metodu ezmeye çalışmak

Belirti: CS0506: 'Yonetici.MaasHesapla()': cannot override inherited member 'Calisan.MaasHesapla()' because it is not marked virtual, abstract, or override. Çözüm: taban sınıftaki metoda virtual ekleyin.

3. Taban sınıfın constructor'ını unutmak

public class Danisman : Calisan
{
    public Danisman(string ad) { }
    // CS7036: There is no argument given that corresponds to the required parameter 'ad' of 'Calisan.Calisan(string, decimal)'
}

public class Danisman : Calisan
{
    public Danisman(string ad) : base(ad, 50000m) { }   // Doğrusu
}

Taban sınıfta parametresiz constructor yoksa : base(...) yazmak zorunludur.

4. Kontrolsüz downcasting

Belirti: çalışma zamanında System.InvalidCastException. Çözüm: (Yonetici)calisan yerine if (calisan is Yonetici y) kullanın; as kullanıyorsanız sonucu null için kontrol edin.

Sık Sorulan Sorular

C#'ta bir sınıf birden fazla sınıftan türeyebilir mi?

Hayır, bir sınıfın yalnızca bir taban sınıfı olabilir. Birden fazla yetenek kazandırmak için birden fazla interface uygulanır.

override ile new arasındaki fark tek cümleyle nedir?

override taban sınıftaki sanal metodun yerine geçer ve çağrı nesnenin gerçek tipine göre çözülür. new ise sadece aynı isimde ayrı bir metot tanımlar ve çağrı değişkenin tipine göre çözülür.

private üyeler miras alınır mı?

Nesnenin içinde yer alırlar ama türeyen sınıfın kodundan erişilemezler. Türeyen sınıfın erişmesi gerekiyorsa üyeyi protected yapın ya da bir property veya metot üzerinden açın.

Her metodu virtual yapmak doğru mu?

Hayır. virtual, "bu davranış değiştirilebilir" sözüdür ve ileride o metodu değiştirmenizi zorlaştırır. Sadece türeyen sınıfların gerçekten farklılaştırması gereken metotları sanal yapın.

Yorumlar