Skip to content

Bölüm 7 / 13

Delegate ve event

Delegate, Func/Action, lambda, event.

10 dakikalık okuma

Önceki derste lambda'yı iş görecek kadar kullandın; bu ders arkasındaki mekanizmayı söküyor: metodu veri gibi taşımak. Delegate'in ne olduğundan Func/Action ailesine, closure'ın sessiz tuzaklarına, event'in delegate'ten farkına ve Hat 02'nin 4. dersinde senedini verdiğimiz konuya — aboneliği kesilmeyen event handler'ın yol açtığı bellek sızıntısına — kadar. Bu ders aynı zamanda Hat 04'ün Strategy/Observer desenlerinin ve Hat 05'in tüm callback tabanlı API'lerinin altyapısıdır.

01Ne öğreneceksin

  • Delegate nedir: tip güvenli metot referansı, çok noktalı (multicast) yapısı
  • Hazır aile: Func, Action, Predicate; lambda ve closure mekanizması
  • Closure tuzakları: yakalanan değişkenin ömrü, döngü değişkeni davranışı
  • Event: delegate üzerine konan kapsülleme katmanı, standart EventHandler deseni
  • Event kaynaklı bellek sızıntısı: mekanizması, belirtisi, önlenmesi

02Delegate: metodu değişkende taşımak

Şimdiye kadar değişkenlerde veri taşıdın. Delegate, bir metodu taşıyan tip güvenli referanstır — "şu imzaya uyan herhangi bir metot" diyen bir sözleşme. Tanımı, gövdesi olmayan bir metot imzasına benzer:

public delegate decimal IndirimHesabi(decimal tutar);   // sözleşme: decimal alır, decimal döner

decimal Yuzde10(decimal t) => t * 0.90m;
decimal Sabit50(decimal t) => t - 50m;

IndirimHesabi kural = Yuzde10;        // metodu değişkene ATA (parantez YOK — çağırmıyorsun)
decimal sonuc = kural(1000m);         // 900 — değişken üzerinden çağır
kural = Sabit50;                      // aynı değişken, başka davranış
sonuc = kural(1000m);                 // 950

Neden değerli? Çünkü davranışı parametre yapabilirsin. Bir metoda "hangi veriyi işleyeceğini" değil "nasıl işleyeceğini" geçirmek, Hat 01'de interface ile kurduğun soyutlamanın hafif sıklet hâlidir: tek bir davranış için sınıf yazmak yerine metot geçersin. Hat 04'ün Strategy deseni tam olarak budur; LINQ'nun Where(m => ...) parametresi de bir delegate'tir.

İkinci özellik mülakat sorusudur: delegate çok noktalıdır (multicast) — içinde bir çağrı listesi tutar, += ile metot eklenir, -= ile çıkarılır:

Action bildir = () => Console.WriteLine("e-posta");
bildir += () => Console.WriteLine("SMS");
bildir();                    // ikisi de çalışır — ekleme sırasıyla
                             // DİKKAT: dönüş değeri varsa yalnız SONUNCUNUNKİ elde edilir

Ve delegate bir referans tipidir (1. dersin tablosu): null olabilir; null bir delegate'i çağırmak NullReferenceException verir — 2. dersteki ?.Invoke(...) kalıbının nedeni budur.

03Func, Action, Predicate: hazır aile ve lambda

Her imza için delegate tipi tanımlamak yorucudur; .NET generic hazır aileyi verir (5. dersin generics'i burada işe yarıyor):

TipİmzaÖrnek
Actionparametre yok, dönüş yokAction log = () => Console.WriteLine("ok");
Action<T>T alır, dönüş yokAction<string> yaz = s => Console.WriteLine(s);
Func<TSonuc>parametre yok, TSonuc dönerFunc<DateTime> simdi = () => DateTime.Now;
Func<T, TSonuc>T alır, TSonuc dönerFunc<decimal, decimal> kdv = t => t * 1.20m;
Predicate<T>T alır, bool dönerPredicate<int> cift = x => x % 2 == 0;

Kural: Func'ın son tip parametresi dönüş tipidir, öncekiler parametrelerdir (Func<int, string, bool> = int ve string alır, bool döner). Predicate<T> ile Func<T, bool> aynı işi yapar; modern kod ve LINQ Func'ı tercih eder.

Lambda (x => ifade), bu delegate'lere değer üretmenin kısa yoludur: isimsiz, yerinde tanımlanan metot. Çok satırlı gövde de yazılabilir:

Func<decimal, decimal> kural = tutar =>
{
    if (tutar > 1000m) return tutar * 0.85m;
    return tutar;
};

// metot grubu dönüşümü: lambda yazmadan doğrudan metot adı da verilebilir
Action<string> yaz = Console.WriteLine;

04Closure: lambda'nın yuttuğu değişkenler

Lambda, tanımlandığı yerdeki yerel değişkenleri kullanabilir — bunlara yakalanan değişken (captured variable), oluşan yapıya closure denir. Mekanizması Hat 02'nin 4. dersiyle doğrudan bağlantılıdır: derleyici, yakalanan değişkeni gizli bir sınıfın alanına taşır; yani değişken artık stack'te değil heap'te yaşar ve lambda yaşadığı sürece o veri de yaşar.

decimal esik = 1000m;
Func<decimal, bool> buyukMu = t => t > esik;   // esik YAKALANDI (kopyalanmadı!)

esik = 5000m;                                   // aynı değişkeni değiştirdim
Console.WriteLine(buyukMu(2000m));              // False — lambda GÜNCEL değeri gördü

Kritik nokta: lambda değişkenin değerini değil kendisini yakalar. Bu, en klasik C# tuzağının kaynağıydı — döngü değişkeni yakalama. C# 5'ten beri foreach değişkeni her turda yeni sayılır ve tuzak kapandı; ama for döngüsünde hâlâ canlıdır:

var isler = new List<Action>();
for (int i = 0; i < 3; i++)
    isler.Add(() => Console.Write(i));      // hepsi AYNI i'yi yakaladı
foreach (var is_ in isler) is_();           // 333 — 012 değil!

for (int i = 0; i < 3; i++)
{
    int yerel = i;                          // her turda YENİ değişken
    isler.Add(() => Console.Write(yerel));  // 012 — doğrusu
}

İkinci saha etkisi ömürdür: bir lambda büyük bir nesneyi yakalayıp uzun ömürlü bir yere (statik alan, event, önbellek) konursa, o nesne lambda yaşadıkça toplanamaz — 4. dersteki "unutulmuş referans" sızıntısının bir çeşidi. Sıradaki bölümdeki event sızıntısı bunun en yaygın hâlidir.

05Event: delegate'in üzerine konan kapsülleme

Bir sınıf "bir şey olduğunda haber vereyim" diyorsa, delegate alanını public yapmak yeterli görünür — ama üç kapı açık kalır: dışarıdan = null ile tüm aboneler silinebilir, dışarıdan bildir() ile sahte olay tetiklenebilir, abonelik listesi dışarıdan okunabilir. event anahtar kelimesi tam bu üçünü kapatır: Hat 01'in kapsülleme dersinin delegate'e uygulanmış hâlidir. Event'te dışarıdan yalnızca += ve -= yapılabilir; tetikleme yalnız sahibi sınıfın içinden mümkündür.

public class Siparis
{
    public event EventHandler<SiparisOlayi> Onaylandi;      // .NET standart deseni

    public void Onayla()
    {
        // ... iş mantığı ...
        Onaylandi?.Invoke(this, new SiparisOlayi(Id));      // ?.Invoke: abone yoksa null
    }
}

public class SiparisOlayi : EventArgs                        // olay verisi EventArgs'tan türer
{
    public int SiparisId { get; }
    public SiparisOlayi(int id) => SiparisId = id;
}

// abone taraf:
siparis.Onaylandi += (gonderen, e) => Console.WriteLine($"#{e.SiparisId} onaylandı");
  • Standart imza (object gonderen, TEventArgs e)'dir; .NET genelinde bu desen beklenir — kendi olayların da uysun.
  • ?.Invoke zorunlu alışkanlıktır: hiç abone yoksa event null'dır (2. ders).
  • Olayın anlamı geçmiş zamandır: Onaylandi, Degisti — "oldu, haberin olsun". Bu, event'i "iş yaptır" çağrısından ayıran adlandırma disiplinidir.
  • Abonelerin sırası ve hataları: abone listesi sırayla çalışır; bir abone exception fırlatırsa sonrakiler çalışmaz — yayıncı, aboneye güvenmemeyi (kritik yerde try/catch ile sarmalamayı) düşünmelidir.

06Event sızıntısı: Hat 02'nin senedi ödeniyor

Hat 02'nin 4. dersinde bellek sızıntısının "klasik failleri" arasında aboneliği kesilmeyen event handler geçmiş, ayrıntısı bu derse bırakılmıştı. Mekanizma tek cümlede: += yapan abone değil, yayıncı aboneye referans tutar. Uzun ömürlü bir yayıncıya abone olan kısa ömürlü nesne, aboneliğini kesmezse yayıncı yaşadığı sürece heap'te tutulur — çöp toplayıcı onu asla toplayamaz:

public class Pencere                        // kısa ömürlü
{
    public Pencere(VeriServisi servis)      // servis: uygulama boyu yaşayan singleton
    {
        servis.VeriDegisti += Yenile;       // servis artık BU pencereye referans tutuyor
    }
    void Yenile(object g, EventArgs e) { /* ... */ }
}
// Pencere kapandı, hiçbir yerde tutulmuyor sanılıyor —
// ama servis'in abone listesi onu tutuyor: GC toplayamaz. 100 açılış = 100 ölü pencere.

Belirtisi 4. dersteki tabloyla aynıdır: bellek grafiği günler içinde tırmanır, sonunda OutOfMemoryException; teşhis aracı bellek profilleyicidir (aynı tipten binlerce "ölü" örnek görürsün). Önlemler:

  • Simetri kuralı: += yazan yer, -='nin nerede yazılacağını da bilmek zorundadır — nesnenin kapanış/temizlik noktasında (Dispose — 9. ders) aboneliği kes.
  • Ömür farkına dikkat: tehlike yalnız "uzun ömürlü yayıncı + kısa ömürlü abone" bileşiminde vardır; ikisi de aynı ömürdeyse (ikisi de uygulama boyu ya da ikisi de aynı anda ölüyorsa) sorun yoktur.
  • Anonim lambda ile abone olurken dikkat: -= için elde referans kalmaz; kesilmesi gereken abonelikte lambda'yı bir değişkende tut ya da isimli metot kullan.
  • Alternatif desenler: zayıf event (weak event) yaklaşımları ve mesaj tabanlı gevşek bağlama — Hat 19'un mesajlaşma dünyası bu problemi mimari düzeyde çözer.

07Sahada: delegate mi interface mi, event mi çağrı mı

İki karar, kod incelemelerinin gündemidir. Delegate mi interface mi? Tek bir davranış noktası varsa ve kısa yazılıyorsa delegate/lambda (LINQ, sıralama karşılaştırıcısı, yeniden deneme kuralı); birden çok ilişkili metot, durum ya da test edilebilir bağımlılık gerekiyorsa interface (Hat 04-05'in DI dünyası interface'lerle konuşur). Event mi doğrudan çağrı mı? Yayıncı, kimin dinlediğini bilmemeli ve dinleyen sayısı değişebiliyorsa event (Observer deseni — Hat 04); tek bir iş akışı zorunluysa doğrudan çağrı daha okunur olur — her şeyi olaya bağlamak, akışı takip edilemez hâle getirir.

Bir de köprü: bu dersin lambda'ları, 8. dersteki asenkron dünyada async gövdelerle birleşecek; event'lerin async void handler'ları ise orada ayrı bir tuzak başlığıdır — adını buradan tanıyor ol.

08Mini alıştırma — Kuralı parametre yap, sızıntıyı gör

Yaklaşık 20 dakika · dotnet run

  1. Davranışı parametre yap: decimal Uygula(decimal tutar, Func<decimal, decimal> kural) metodu yaz; üç kuralla çağır (yüzde 10 indirim, sabit 50 düşüş, değişiklik yok). Sonra aynı işi bir IIndirimKurali arayüzüyle kur — hangi durumda hangisini seçerdin, iki cümle yaz.
  2. Multicast: Action<string> bildir değişkenine üç farklı metot ekle (+=), birini çıkar (-=), çağır. Sonra Func<int> ile aynısını dene: iki metot eklenmişken dönen değer hangisininki?
  3. Closure kanıtı: bölüm 04'teki for döngüsü deneyini yaz — 333 çıktısını gör, sonra yerel değişkenle düzelt. Aynı deneyi foreach ile tekrarla ve farkı not et.
  4. Event kur: Sayac sınıfı yaz: Artir() metodu 10'un katlarında EsigeUlasti event'ini ?.Invoke ile tetiklesin. İki abone bağla (biri ekrana yazsın, biri listeye kaydetsin), 25 kez artır, çıktıyı doğrula.
  5. Sızıntı gözlemi: uzun ömürlü bir VeriServisi ve döngüde 10.000 kez oluşturulup abone olan Pencere nesneleri kur; her 1000 adımda GC.Collect(); GC.WaitForPendingFinalizers(); sonrası GC.GetTotalMemory(true) yazdır. Sonra -= ile abonelik kesen sürümü çalıştır, iki grafiği karşılaştır.

09Kendini sına

Delegate nedir, ne işe yarar? Multicast davranışı ve dönüş değeri kuralı nedir?

Delegate, belirli bir imzaya uyan metotları taşıyabilen tip güvenli bir referans tipidir; davranışı veri gibi değişkende tutmayı ve parametre olarak geçirmeyi sağlar (LINQ'nun Where'i, Strategy deseni). Multicast'tir: içinde çağrı listesi tutar, += ile metot eklenir, -= ile çıkarılır ve çağrıldığında hepsi ekleme sırasıyla çalışır. Dönüş değeri olan bir multicast delegate'te yalnızca son çalışan metodun dönüşü elde edilir — bu yüzden çok abonelikli senaryolarda dönüşsüz (Action tarzı) imzalar tercih edilir. Referans tipi olduğundan null olabilir; çağırmadan önce ?.Invoke kullanılır.

Closure nedir? Yakalanan değişkenin ömrü ve for döngüsü tuzağını açıkla.

Lambda'nın, tanımlandığı kapsamdaki yerel değişkenleri kullanabilmesine closure denir; derleyici yakalanan değişkeni gizli bir sınıfın alanına taşır, yani değişken stack yerine heap'te yaşar ve lambda yaşadıkça toplanamaz. Lambda değeri değil değişkenin kendisini yakalar: sonradan yapılan değişikliği görür. for döngüsünde tek bir i değişkeni olduğundan tüm lambda'lar aynı i'yi yakalar ve döngü bitince hepsi son değeri okur (012 yerine 333); çözüm her turda yeni bir yerel değişkene kopyalamaktır. foreach'te C# 5'ten beri değişken her turda yeni sayıldığı için bu tuzak yoktur.

event ile public delegate alanı arasındaki fark nedir? Standart .NET olay deseni neyi öngörür?

event, delegate üzerine konan bir kapsülleme katmanıdır: dışarıdan yalnızca += ve -= yapılabilir; dışarıdan atama (= null ile tüm aboneleri silme), dışarıdan tetikleme (sahte olay yayınlama) ve abone listesine doğrudan erişim engellenir — tetikleme yalnız sahibi sınıfın içinde mümkündür. Standart desen: EventHandler<TEventArgs> tipi, (object gonderen, TEventArgs e) imzası, EventArgs'tan türeyen olay verisi sınıfı, geçmiş zaman kipinde olay adı (Onaylandi, Degisti) ve abone yoksa null olacağı için ?.Invoke ile tetikleme.

Event kaynaklı bellek sızıntısı nasıl oluşur? Hangi ömür bileşiminde tehlikelidir ve nasıl önlenir?

+= ile abone olunduğunda yayıncı, abonenin handler'ı üzerinden aboneye referans tutar. Uzun ömürlü bir yayıncıya (singleton servis, statik olay) abone olan kısa ömürlü nesne aboneliğini kesmezse, yayıncının abone listesi onu canlı tutar ve GC toplayamaz — bellek grafiği tırmanır, sonunda OutOfMemoryException; profilleyicide aynı tipten binlerce ölü örnek görünür. Tehlike yalnız "uzun ömürlü yayıncı + kısa ömürlü abone" bileşimindedir; ömürler eşitse sorun yoktur. Önleme: simetri kuralı — += yazan yer -='nin nerede yazılacağını bilir, temizlik noktasında (Dispose) abonelik kesilir; kesilmesi gereken aboneliklerde anonim lambda yerine isimli metot ya da saklanan referans kullanılır.

10Sınav köşesi

Yazılıda nasıl sorulur

(1) Tanım: "Delegate nedir?" (metotlara işaret eden tip güvenli referans tipi), "event ile delegate farkı" (kapsülleme: dışarıdan yalnız +=/-=). (2) İmza eşleme: Func<int, string, bool> kaç parametre alır, ne döner (iki parametre, bool — son tip dönüştür); Action'ın dönüşü var mı (yok); Predicate<T> neye denktir (Func<T,bool>). (3) Çıktı sorusu: for döngüsünde lambda yakalama — 333 klasiği. (4) Multicast: iki metot ekli delegate çağrıldığında hangi dönüş elde edilir (sonuncunun). (5) Kavram: "aboneliği kesilmeyen event neden bellek sızdırır?" — Hat 02'nin bellek dersiyle çapraz sorulur.

Sözlüde nasıl anlatılır — 2 dakikalık taslak

"Delegate ve event'i anlat" için düzen: (1) Çekirdek — "delegate, metodu veri gibi taşıyan tip güvenli referanstır; davranışı parametre yapmayı sağlar — LINQ'nun Where'i ve Strategy deseni bunun üzerine kuruludur." (2) Pratik katman — "günlük işte Func/Action ailesini ve lambda'yı kullanırım; lambda'nın değişken yakaladığını, yakalanan değişkenin heap'e taşındığını bilirim." (3) Event ayrımı — "event, delegate'in kapsüllenmiş hâlidir: dışarıdan tetiklenemez ve sıfırlanamaz; standart EventHandler desenini ve ?.Invoke alışkanlığını kullanırım." (4) Kıdem vuruşu — "abonelikte yayıncı aboneye referans tutar; uzun ömürlü servise abone olan kısa ömürlü nesne aboneliğini kesmezse GC onu toplayamaz, bu klasik sızıntıyı Dispose'da -= ile önlerim" — bu sızıntı bilgisi, olay mekanizmasını ezberden ayıran cevaptır.