C# Delegates ve Events
C#’ta delegate ve event kavramlarını öğrenin. Olay tabanlı programlama, callback mantığı ve kullanım senaryoları örneklerle anlatılıyor.
C# dilinde delegate ve event yapıları,
event-driven (olay tabanlı) programlamanın temelini oluşturur.
Bir delegate, bir metoda işaret edebilen type-safe bir referanstır.
Bir event ise bir sınıfın “belirli bir eylem gerçekleşti” bilgisini dış dünyaya bildirmesini sağlar.
Bu mekanizmalar gevşek bağlı (loosely coupled) mimariler kurmayı mümkün kılar
ve farklı metotların dinamik olarak çalıştırılmasını sağlar.
Delegate Nedir?
Delegate, belirli bir imzaya (parametreler ve dönüş tipi) sahip metotlara referans tutabilen bir türdür. Başka bir deyişle, metotların değişken gibi kullanılmasını sağlar.
Bir delegate değişkeni, uyumlu bir metoda işaret edebilir ve onu dinamik olarak çağırabilir. Bu da esnek ve gevşek bağlı kod yazmayı mümkün kılar.
// Delegate tanımı
public delegate void Notify(string message);
class Program
{
static void SendEmail(string message)
{
Console.WriteLine("Email sent: " + message);
}
static void Main()
{
Notify notifyHandler;
// Delegate'e bir metot atanır
notifyHandler = SendEmail;
notifyHandler("Toplantı saat 10:00'da.");
}
}
Bu örnekte notifyHandler delegate’i
SendEmail metoduna referans tutar ve onu çağırır.
Multicast Delegate
Multicast delegate, aynı anda birden fazla metoda referans tutabilen delegate türüdür.
C# dilinde delegate’ler += operatörü ile birleştirildiğinde varsayılan olarak multicast davranışı gösterir.
Multicast delegate çağrıldığında, invocation list içerisindeki her metot eklendiği sıraya göre çalıştırılır. Bu yapı özellikle bir bildirimi birden fazla dinleyiciye iletmek için kullanışlıdır.
Örnek
using System;
class Program
{
delegate void Notify(string message);
static void Main()
{
Notify notifier = LogToConsole;
notifier += LogToFile;
notifier += SendEmail;
notifier("Build tamamlandı.");
}
static void LogToConsole(string message)
{
Console.WriteLine("[Console] " + message);
}
static void LogToFile(string message)
{
Console.WriteLine("[File] " + message);
}
static void SendEmail(string message)
{
Console.WriteLine("[Email] " + message);
}
}
Invocation list’ten bir metodu çıkarmak için -= operatörü kullanılır:
notifier -= SendEmail;
- Multicast delegate’ler event yapılarının arka planında kullanılır.
- Handler’lardan biri exception fırlatırsa, kalan metotlar çalıştırılmaz.
- Delegate bir dönüş değerine sahipse, yalnızca son metodun sonucu döndürülür.
Dönüş Değeri Davranışı
Multicast delegate bir dönüş tipine sahipse, invocation list’teki tüm metotlar çalıştırılır; ancak yalnızca son metodun sonucu döndürülür.
using System;
class Program
{
delegate int Calculate(int x);
static void Main()
{
Calculate calc = Square;
calc += Double;
int result = calc(5);
Console.WriteLine(result);
// Çıktı: 10 (son metodun sonucu: Double)
}
static int Square(int x)
{
Console.WriteLine("Square çağrıldı");
return x * x;
}
static int Double(int x)
{
Console.WriteLine("Double çağrıldı");
return x * 2;
}
}
Hem Square hem de Double çalıştırılır;
ancak delegate, invocation list’e en son eklenen metodun sonucunu döndürür.
- Invocation list’teki tüm metotlar sırayla çalıştırılır.
- Yalnızca son metodun dönüş değeri korunur.
- Bu nedenle multicast delegate’ler genellikle
voiddönüş tipiyle (event’lerde olduğu gibi) kullanılır.
Event Nedir?
Event, bir eylemin gerçekleştiğini bildiren bir mekanizmadır.
Event’ler delegate’ler üzerine inşa edilir ve kontrollü erişim sağlar.
event anahtar kelimesi ile tanımlanır ve dışarıdan yalnızca
+= (abone olma) veya -= (abonelikten çıkma) ile erişilebilir.
Bu sayede:
- Event yalnızca tanımlandığı sınıf içinde tetiklenebilir.
- Dışarıdan invocation list üzerine yazılamaz.
- Encapsulation korunur.
public class Button
{
// Event tanımı (EventHandler standart bir delegate türüdür)
public event EventHandler? Click;
public void SimulateClick()
{
Console.WriteLine("Butona tıklandı!");
// Event tetiklenir
Click?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
var btn = new Button();
// Event'e abone olunuyor
btn.Click += (s, e) => Console.WriteLine("Event: Butona tıklandı.");
btn.SimulateClick();
}
}
Null-conditional operatörü (?.), abone yoksa
NullReferenceException oluşmasını engeller.
Bu örnekte Button sınıfı bir Click event’i yayınlar.
Abone olan lambda ifadesi, event tetiklendiğinde çalışır.
Bu desen WinForms ve WPF gibi UI framework’lerinde yaygın olarak kullanılır.
EventHandler ve EventArgs
C# dilinde event’ler için en yaygın kullanılan standart delegate türü
EventHandler'dır.
Bu yapı geleneksel event desenini takip eder:
(object sender, EventArgs e).
EventArgs sınıfından türeyerek,
event ile ilgili ek veriler taşınabilir.
Bu yaklaşım .NET event tasarım kurallarını takip eder.
public class OrderEventArgs : EventArgs
{
public int OrderId { get; }
public decimal Amount { get; }
public OrderEventArgs(int orderId, decimal amount)
{
OrderId = orderId;
Amount = amount;
}
}
public class OrderService
{
public event EventHandler<OrderEventArgs>? OrderCreated;
public void CreateOrder(int id, decimal amount)
{
Console.WriteLine($"Sipariş oluşturuldu (ID={id}, Tutar={amount})");
// Event tetiklenir
OrderCreated?.Invoke(this, new OrderEventArgs(id, amount));
}
}
class Program
{
static void Main()
{
var service = new OrderService();
service.OrderCreated += (s, e) =>
{
Console.WriteLine($"Bildirim: Sipariş alındı (#{e.OrderId}, {e.Amount} USD)");
};
service.CreateOrder(101, 250m);
}
}
Bu örnekte OrderService sınıfı,
yeni bir sipariş oluşturulduğunda OrderCreated event’ini tetikler.
Abone olan kod, sipariş bilgilerini özel
OrderEventArgs sınıfı üzerinden alır.
Bu yaklaşım type safety sağlar ve event tabanlı iletişimi daha açık ve düzenli hale getirir.
Delegate vs Event (Encapsulation)
Event’ler delegate’ler üzerine inşa edilmiş olsa da, aynı şey değildirler. Temel fark encapsulation (kapsülleme) ve erişim kontrolü noktasında ortaya çıkar.
Bir delegate alanı sınıf dışından serbestçe değiştirilebilirken, bir event dış erişimi kısıtlar ve invocation list’i korur.
Delegate Örneği
public class Publisher
{
public Action? OnChange;
}
OnChange bir delegate alanı olduğu için:
- Dışarıdan
=operatörü ile yeniden atanabilir. - Mevcut tüm aboneler yanlışlıkla silinebilir.
nullyapılabilir.- Dışarıdan doğrudan tetiklenebilir.
publisher.OnChange = null; // Tüm aboneleri temizler
publisher.OnChange = SomeMethod; // Invocation list’i değiştirir
publisher.OnChange?.Invoke(); // Dışarıdan tetiklenebilir
Event Örneği
public class Publisher
{
public event Action? OnChange;
}
OnChange bir event olarak tanımlandığında:
- Dışarıdan yalnızca
+=ve-=kullanılabilir. =ile invocation list üzerine yazılamaz.- Dışarıdan doğrudan tetiklenemez.
- Event yalnızca tanımlandığı sınıf içinde tetiklenebilir.
- Delegate’ler esneklik sağlar.
- Event’ler kontrollü erişim ve kapsülleme sağlar.
- Genel bildirim mekanizmalarında delegate alanı yerine event tercih edilmelidir.
Modern Alternatifler: Action & Func
Modern C#’ta özel delegate türleri tanımlamak çoğu zaman gerekli değildir.
Bunun yerine yerleşik generic delegate türleri olan
Action ve Func kullanılabilir.
Bu delegate’ler esnek, sade ve .NET ekosisteminde yaygın olarak kullanılır (LINQ, async programlama ve task tabanlı API’ler dahil).
Action
Action, geri dönüş değeri olmayan bir metodu temsil eder.
Sıfır veya daha fazla parametre alabilir.
Action log = message => Console.WriteLine(message);
log("Application started.");
Func
Func<T, TResult>, geri dönüş değeri olan bir metodu temsil eder.
Generic parametrelerin sonuncusu her zaman dönüş tipini ifade eder.
Func add = (a, b) => a + b;
int result = add(5, 3); // 8
Action, dönüş değeri gerekmediğinde kullanılır.Func, dönüş değeri gerektiğinde kullanılır.- Yerleşik delegate’lerin kullanılması gereksiz kod tekrarını azaltır.
- Modern C# projelerinde özel delegate tanımı yerine genellikle
ActionveFunctercih edilir.
Abonelikten Çıkma ve Memory Leak
Event’lerle çalışırken önemli konulardan biri abonelikten çıkmaktır (unsubscribe). Bir event’e abone olup daha sonra abonelikten çıkmazsanız, bu durum beklenmeyen memory leak’lere yol açabilir.
Event’ler abonelerine strong reference tutar. Publisher nesnesi hayatta kaldığı sürece, subscriber garbage collector tarafından toplanamaz.
Neden Bu Bir Sorundur?
Uzun ömürlü bir publisher (örneğin bir servis veya singleton) ve kısa ömürlü bir subscriber (örneğin bir UI bileşeni) düşünün. Eğer subscriber abonelikten çıkmazsa, artık kullanılmasa bile bellekte kalmaya devam eder.
public class Publisher
{
public event Action? OnChange;
public void Raise()
{
OnChange?.Invoke();
}
}
public class Subscriber
{
private readonly Publisher _publisher;
public Subscriber(Publisher publisher)
{
_publisher = publisher;
_publisher.OnChange += HandleChange;
}
private void HandleChange()
{
Console.WriteLine("Change detected.");
}
public void Dispose()
{
// Önemli: abonelikten çık!
_publisher.OnChange -= HandleChange;
}
}
Eğer Dispose metodu çağrılmazsa,
Subscriber nesnesi event tarafından referans tutulmaya devam eder
ve garbage collector tarafından temizlenemez.
- Subscriber’ın yaşam süresi bittiğinde mutlaka abonelikten çıkılmalıdır.
- Bu durum özellikle WinForms, WPF, Blazor gibi UI framework’lerinde önemlidir.
- Uzun ömürlü publisher + kısa ömürlü subscriber = potansiyel memory leak.
- Güvenli abonelikten çıkış için
IDisposableimplement etmek yaygın bir çözümdür.
Thread Safety ve Best Practices
Event’lerle çoklu thread ortamlarında çalışırken
thread safety önem kazanır.
En yaygın sorunlardan biri,
event’i çağırmadan önce null kontrolü yapmaktır.
Olası Race Condition
if (OnChange != null)
{
OnChange(this, EventArgs.Empty);
}
null kontrolü ile çağrı arasında
başka bir thread event’ten aboneliği kaldırabilir.
Bu durum NullReferenceException oluşmasına neden olabilir.
Güvenli Çağırma Deseni
Bu race condition’ı önlemek için, delegate referansını çağırmadan önce yerel bir değişkene kopyalayın:
var handler = OnChange;
handler?.Invoke(this, EventArgs.Empty);
Bu yaklaşım, null kontrolü ile metot çağrısı arasında invocation list’in değişmesini engeller.
Modern Sözdizimi
Çoğu durumda null-conditional operatörü, event çağırmak için sade ve güvenli bir yöntem sunar:
OnChange?.Invoke(this, EventArgs.Empty);
- Çoklu thread senaryolarında güvenli çağırma desenlerini kullanın.
- Temiz ve modern bir sözdizimi için
?.Invoketercih edin. - Event’ler multicast delegate’lerdir — bir handler hata verirse zincir durabilir.
- Yüksek güvenilirlik gereken durumlarda handler çağrılarını try/catch ile izole etmeyi düşünebilirsiniz.
Delegates ve Events Ne Zaman Kullanılmalı?
-
Delegate’ler:
Metotları parametre olarak geçirmek, callback mekanizması kurmak
veya esnek çalıştırma akışları oluşturmak için kullanılır.
Modern C#’ta genellikle
ActionveFunctercih edilir. - Event’ler: Bir sınıfın dış dünyaya bildirim yapması gerektiğinde, kapsüllemeyi koruyarak kullanılır. UI etkileşimleri, domain event’leri ve durum değişiklikleri için idealdir.
- Best Practice: Genel bildirimler için event kullanın, ham delegate alanlarını dışarı açmaktan kaçının. Gerekli durumlarda abonelikten çıkın ve güvenli çağırma desenlerini uygulayın.
TL;DR
delegate: Metotlara işaret eden type-safe referanslardır ve multicast destekler.event: Aboneliğe izin veren ancak kapsüllemeyi koruyan kontrollü bir delegate yapısıdır.EventHandlerveEventArgs: Veri taşımak için standart .NET event desenini takip eder.ActionveFunc: Özel delegate tanımlarına modern alternatiflerdir.- Memory leak’leri önlemek için gerekli durumlarda abonelikten çıkılmalıdır.
- Bildirimler için event, callback ve akış kontrolü için delegate (veya
Action/Func) kullanılmalıdır.
İlişkili Makaleler
C# Interface ve Abstract Sınıflar
C#’ta interface ve abstract sınıfları öğrenin. Farklarını, ne zaman hangisini kullanacağınızı ve tasarım senaryolarını örneklerle keşfedin.
C# Lambda İfadeleri
C#’ta lambda ifadelerini öğrenin. Kısa sözdizimi, Func ve Action kullanımı ile LINQ sorgularında pratik örnekler keşfedin.
C# Metotlar ve Parametre Kullanımı
C#’ta metot tanımlama ve parametre kullanımını öğrenin. Değer ve referans parametreleri, varsayılan parametreler ve örneklerle.
C# Sınıf (Class), Object, Property ve Metotlar
C#’ta class, object, property ve metot kavramlarını öğrenin. Nesne yönelimli programlamanın temel yapı taşları örneklerle açıklanıyor.