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
EventHandlerdeseni - 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 |
|---|---|---|
Action | parametre yok, dönüş yok | Action log = () => Console.WriteLine("ok"); |
Action<T> | T alır, dönüş yok | Action<string> yaz = s => Console.WriteLine(s); |
Func<TSonuc> | parametre yok, TSonuc döner | Func<DateTime> simdi = () => DateTime.Now; |
Func<T, TSonuc> | T alır, TSonuc döner | Func<decimal, decimal> kdv = t => t * 1.20m; |
Predicate<T> | T alır, bool döner | Predicate<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. ?.Invokezorunlu 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
- 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 birIIndirimKuraliarayüzüyle kur — hangi durumda hangisini seçerdin, iki cümle yaz. - Multicast:
Action<string> bildirdeğişkenine üç farklı metot ekle (+=), birini çıkar (-=), çağır. SonraFunc<int>ile aynısını dene: iki metot eklenmişken dönen değer hangisininki? - Closure kanıtı: bölüm 04'teki
fordöngüsü deneyini yaz — 333 çıktısını gör, sonra yerel değişkenle düzelt. Aynı deneyiforeachile tekrarla ve farkı not et. - Event kur:
Sayacsınıfı yaz:Artir()metodu 10'un katlarındaEsigeUlastievent'ini?.Invokeile tetiklesin. İki abone bağla (biri ekrana yazsın, biri listeye kaydetsin), 25 kez artır, çıktıyı doğrula. - Sızıntı gözlemi: uzun ömürlü bir
VeriServisive döngüde 10.000 kez oluşturulup abone olanPencerenesneleri kur; her 1000 adımdaGC.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.
