Yükleniyor...

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 void dö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:


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:


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:

  • 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 Action ve Func tercih 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 IDisposable implement 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 ?.Invoke tercih 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ı?


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.
  • EventHandler ve EventArgs: Veri taşımak için standart .NET event desenini takip eder.
  • Action ve Func: Ö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# Lambda İfadeleri

C#’ta lambda ifadelerini öğrenin. Kısa sözdizimi, Func ve Action kullanımı ile LINQ sorgularında pratik örnekler keşfedin.