DEV Community

sadık emre
sadık emre

Posted on

C#'ta Ters Hissettiren Polimorfizm

ladybug

Bir metodun hangi sınıfa ait olduğu bazen göründüğü kadar net değil. C#'ta polimorfizm çoğu zaman beklendiği gibi çalışır, ama bazı durumlarda tam tersi olur ve bu hatalar genelde code review'a kadar fark edilmeden ilerler.

Aşağıda dört örnek var.

Hepsinde "hangi metot çalışır" sorusunun cevabı beklenenden farklı çıkıyor.

Sebebini örneklerden sonra birlikte açacağız.

1. Constructor İçinden Metot Çağırmak

public class MeyveSepeti
{
    public MeyveSepeti() => Hazirla();
    protected virtual void Hazirla() => Console.WriteLine("Sepet hazırlanıyor");
}

public class ElmaSepeti : MeyveSepeti
{
    private readonly string _meyveAdi = "Elma";
    public ElmaSepeti(string meyveAdi) { _meyveAdi = meyveAdi; }
    protected override void Hazirla() => Console.WriteLine($"Sepetten {_meyveAdi} alınıyor");
}

var sepet = new ElmaSepeti("Armut");
// Çıktı: Sepetten  alınıyor (meyve adı boş çıkıyor)
Enter fullscreen mode Exit fullscreen mode

Beklenen çıktı "Sepetten Armut alınıyor" gibi görünüyor, ama öyle olmuyor.

Çağrılan metot ElmaSepeti'nin metodu oluyor, doğru. Ama o an _meyveAdi henüz hiç değer almamış, adı boş çıkıyor.

Roslyn (C#'ın kod analizi yapan derleyici altyapısı) bu durumu ayrı bir kuralla işaretliyor: CA2214. Kural, constructor içinde override edilebilir bir metot çağrıldığında uyarı veriyor. Projede açık değilse fark edilmeden geçebiliyor.

2. Aynı Sınıftaki Bir Metodun Başka Bir Metodu Çağırması

public class MeyveSepeti
{
    public virtual void SesVer() => Console.WriteLine("Sepetten ses geliyor");
    public void Duyur() => this.SesVer();
}

public class ElmaSepeti : MeyveSepeti
{
    public override void SesVer() => Console.WriteLine("Elma sepetinden ses geliyor");
}

ElmaSepeti sepet = new ElmaSepeti();
sepet.Duyur();
// Çıktı: Elma sepetinden ses geliyor
Enter fullscreen mode Exit fullscreen mode

Duyur() metodu MeyveSepeti'de yazılı.

Ama çalıştığında ElmaSepeti'nin metodunu çağırıyor.

3. new ile Metot Gizleme

public class MeyveSepeti
{
    public void Tanit() => Console.WriteLine("Bu bir meyve sepeti");
}

public class ArmutSepeti : MeyveSepeti
{
    public new void Tanit() => Console.WriteLine("Bu bir armut sepeti");
}

MeyveSepeti sepet = new ArmutSepeti();
sepet.Tanit();
// Çıktı: Bu bir meyve sepeti
Enter fullscreen mode Exit fullscreen mode

Nesne gerçekte bir ArmutSepeti.

Ama çıktı "Bu bir armut sepeti" değil, "Bu bir meyve sepeti" oluyor.

4. Sadece Interface Üzerinden Görünen Metot

public interface IKapasiteHesaplanabilir
{
    double Kapasite();
}

public class MeyveSepeti : IKapasiteHesaplanabilir
{
    private readonly double _boyut;
    public MeyveSepeti(double boyut) => _boyut = boyut;
    double IKapasiteHesaplanabilir.Kapasite() => _boyut * _boyut;
}

var sepet = new MeyveSepeti(4);
// sepet.Kapasite(); -> derleme hatası

IKapasiteHesaplanabilir kapasiteli = sepet;
kapasiteli.Kapasite();
// Çıktı: 16
Enter fullscreen mode Exit fullscreen mode

sepet.Kapasite() hiç çalışmıyor, derleme hatası veriyor.

Aynı metot, kapasiteli.Kapasite() olarak yazınca çalışıyor.

Peki Bu Dördünün Sebebi Ne?

Dört örnek de aynı sebepten değil, iki ayrı sebepten kaynaklanıyor.

Birinci sebep: nesne oluşurken ayarlanan bir bilgi

new ElmaSepeti(...) çalıştığında sırayla şunlar olur:

  1. Nesne için bellekte yer ayrılır.

  2. Bu nesneye, "sen bir ElmaSepeti'sin" bilgisi hemen o anda iliştirilir. Bu bilgi, henüz hiçbir constructor çalışmadan hazır olur.

  3. Sonra MeyveSepeti'nin constructor'ı çalışır.

  4. Bu constructor içinde bir metot çağrıldığında, çalışma zamanı nesneye iliştirilmiş olan bu bilgiye bakar ve "bu aslında bir ElmaSepeti, o zaman ElmaSepeti'nin metodunu çalıştır" der.

  5. MeyveSepeti'nin constructor'ı bittikten sonra ElmaSepeti'nin kendi alanları değer alır.

  6. En son ElmaSepeti'nin constructor gövdesi çalışır.

  7. örnekte sorun burada: 4. adım, 5. adımdan önce oluyor. Metot doğru sınıfa ait ama alan henüz boş.

  8. örnekte de aynı 4. adım işliyor, ama bu sefer nesne tam kurulmuş durumda, bu yüzden sonuç aslında doğru. Sürpriz olan tek şey, MeyveSepeti içinde yazılan bir kodun ElmaSepeti'nin metodunu çalıştırabilmesi.

İkinci sebep: derleyicinin önceden verdiği karar

3 ve 4. örnekler bambaşka bir yerden geliyor. Burada çalışma zamanı hiçbir şeye bakmıyor.

Kod daha derlenirken, derleyici değişkenin hangi tipte tanımlandığına bakıyor ve çağrının nereye gideceğine orada karar veriyor.

  1. örnekte sepet değişkeni MeyveSepeti tipinde tanımlı. Derleyici bu yüzden çağrıyı MeyveSepeti'nin metoduna bağlıyor, nesnenin gerçekte ArmutSepeti olması hiç önemli değil.

  2. örnekte ise Kapasite() metodu, MeyveSepeti'nin normal bir üyesi olarak bile tanımlanmamış. Derleyici sepet.Kapasite() yazıldığında bu metodu MeyveSepeti'nin listesinde bulamıyor, hata veriyor.

Özetle: 1 ve 2, nesneye iliştirilen gerçek tip bilgisinden kaynaklanıyor. 3 ve 4 ise derleyicinin değişkenin tanımlı tipine bakarak verdiği erken karardan.

Kaçınma Yolları

  • Constructor içinde başka bir metot çağırmaktan kaçınmak. Bunun yerine nesneyi ayrı bir metotla, tam kurulduktan sonra hazırlamak.

  • Bir sınıfın kendi metodu başka bir metodunu çağırdığında, bunun alt sınıfta değişebileceğini akılda tutmak.

  • override ve new arasındaki farkı her zaman açıkça yazmak.

  • Interface üzerinden yazılan bir metodu çağırırken hangi tipteki değişkeni kullandığına dikkat etmek.

  • CA2214 kuralını projede açık tutmak.

Sonuç

Dört örnek de "hangi metot çalışır" sorusunun cevabının, göründüğünden daha karmaşık olabileceğini gösteriyor.

Bazen bu cevap nesneye iliştirilen gerçek tip bilgisine bağlı, bazen de derleyicinin daha en baştan verdiği bir karara.

Top comments (0)