C# Temsilciler (Delegates): Modern Programlamanın Esnek Gücü
Giriş: Temsilcilerin Önemi ve Yeri
Yazılım geliştirme dünyası, sürekli değişen gereksinimlere uyum sağlayabilen, esnek ve bakımı kolay kodlar yazmayı hedefler. C# programlama dili, bu hedeflere ulaşmak için geliştiricilere güçlü araçlar sunar. Bu araçlardan belki de en temel ve en çok yönlü olanlarından biri temsilcilerdir (delegates). İlk bakışta karmaşık gibi görünse de, temsilciler C# dilinin kalbinde yer alır ve birçok modern programlama deseninin, özellikle de olay güdümlü programlamanın (event-driven programming) temelini oluşturur.
Peki, bir temsilci tam olarak nedir ve neden bu kadar önemlidir? En basit tanımıyla bir temsilci, bir veya daha fazla metoda işaret eden, tür açısından güvenli (type-safe) bir nesnedir. C veya C++ gibi dillerdeki fonksiyon işaretçilerine (function pointers) benzer bir kavramdır, ancak C#’taki temsilciler nesne yönelimli, tür açısından güvenli ve çok daha esnektir. Temsilciler, metotları değişkenlere atamamıza, başka metotlara parametre olarak geçmemize ve olaylar (events) gibi mekanizmalar aracılığıyla dolaylı olarak çağırmamıza olanak tanır.
Bu makalede, C# temsilcilerinin ne olduğunu, nasıl çalıştığını, temel ve ileri düzey özelliklerini, C# ekosistemindeki yerini (özellikle olaylar, lambda ifadeleri ve LINQ ile ilişkisini) ve gerçek dünya senaryolarındaki kullanım alanlarını derinlemesine inceleyeceğiz. Temsilcilerin gücünü anladığınızda, daha modüler, daha esnek ve daha güçlü C# uygulamaları yazma yeteneğinizin önemli ölçüde artacağını göreceksiniz.
Bölüm 1: Temsilcilerin Temelleri — Bir Metot Referansı Olarak Temsilci
1.1. Temsilci Nedir? Fonksiyon İşaretçilerinin Evrimi
Temsilci kavramını anlamanın en iyi yollarından biri, onu bir “metot için referans türü” olarak düşünmektir. Tıpkı bir string değişkeninin bir metin dizisine veya bir int değişkeninin bir tam sayıya referans vermesi gibi, bir temsilci değişkeni de belirli bir imzaya (return type ve parametre listesi) sahip bir metoda referans verir.
Tür Güvenliği (Type Safety): C#’taki temsilcilerin en önemli özelliklerinden biri tür açısından güvenli olmalarıdır. Bir temsilci türü tanımladığınızda, o temsilcinin yalnızca belirli bir geri dönüş türüne ve belirli parametre türlerine sahip metotları işaret edebileceğini belirtirsiniz. Bu, derleme zamanında hataları yakalamanızı sağlar ve çalışma zamanı hatalarının önüne geçer. Örneğin, int döndüren ve iki int parametre alan bir metodu işaret etmek üzere tanımlanmış bir temsilciye, void döndüren veya farklı parametreler alan bir metodu atayamazsınız.
Nesne Yönelimli Doğa: Temsilciler C#’ta sınıflardır (System.Delegate ve System.MulticastDelegate sınıflarından türetilirler). Bu, onların nesne olarak ele alınabileceği anlamına gelir: Değişkenlere atanabilir, metotlara parametre olarak geçirilebilir ve metotlardan geri döndürülebilirler.
1.2. Temsilci Tanımlama (Declaring a Delegate)
Bir temsilci kullanmadan önce, onun türünü tanımlamanız gerekir. Bu tanım, temsilcinin işaret edeceği metotların sahip olması gereken imzayı belirtir. Temsilci tanımı delegate anahtar kelimesi kullanılarak yapılır ve bir metot imzasına çok benzer.
// Geri dönüş türü olmayan (void) ve bir string parametre alan metotları
// işaret edebilecek bir temsilci türü tanımlıyoruz.
public delegate void MesajGosterici(string mesaj);
// İki integer alıp bir integer döndüren metotları işaret edebilecek
// bir temsilci türü tanımlıyoruz.
public delegate int Hesaplayici(int sayi1, int sayi2);
Bu tanımlamalar, MesajGosterici ve Hesaplayici adında iki yeni tür oluşturur. Artık bu türlerden değişkenler (temsilci örnekleri) oluşturabiliriz.
1.3. Temsilci Örneği Oluşturma (Instantiating a Delegate)
Bir temsilci türü tanımladıktan sonra, bu türden bir örnek (instance) oluşturarak onu belirli bir metoda bağlayabiliriz. Bu işlem, temsilci türünün yapıcısına (constructor) hedef metodu argüman olarak vererek yapılır. Hedef metot, statik bir metot olabileceği gibi bir nesnenin örnek metodu (instance method) da olabilir.
public class Islemler
{
// Statik bir metot
public static void StatikMesaj(string m)
{
Console.WriteLine($"Statik Metot Mesajı: {m}");
}
// Örnek metodu
public void OrnekMesaj(string m)
{
Console.WriteLine($"Örnek Metot Mesajı: {m}");
}
public static int Topla(int a, int b)
{
return a + b;
}
public int Carp(int a, int b)
{
return a * b;
}
}
public class Program
{
static void Main(string[] args)
{
// Statik metoda bağlama
MesajGosterici temsilci1 = new MesajGosterici(Islemler.StatikMesaj);
// Örnek metodu için önce nesne oluşturulmalı
Islemler islemNesnesi = new Islemler();
MesajGosterici temsilci2 = new MesajGosterici(islemNesnesi.OrnekMesaj);
// Daha kısa sözdizimi (metot grubu dönüşümü - C# 2.0 ve sonrası)
MesajGosterici temsilci3 = Islemler.StatikMesaj;
MesajGosterici temsilci4 = islemNesnesi.OrnekMesaj;
// Hesaplayıcı temsilcileri
Hesaplayici hesapTemsilci1 = Topla; // Statik metot için kısa sözdizimi
Hesaplayici hesapTemsilci2 = islemNesnesi.Carp; // Örnek metodu için kısa sözdizimi
// ... temsilcileri çağırma ...
}
// Main metodu içinde tanımlı metotlar da kullanılabilir (Local functions C# 7.0+)
// veya Main metodunun ait olduğu Program sınıfı içindeki metotlar
public static int Cikar(int a, int b)
{
return a - b;
}
}
Yukarıdaki örnekte, MesajGosterici türündeki temsilcilere hem statik (Islemler.StatikMesaj) hem de örnek (islemNesnesi.OrnekMesaj) metotları atadık. C# 2.0 ile gelen “metot grubu dönüşümü” (method group conversion) sayesinde, new MesajGosterici(…) yazmak yerine doğrudan metot adını atayarak daha kısa bir sözdizimi kullanabiliyoruz. Derleyici, bu durumda uygun temsilci örneğini otomatik olarak oluşturur.
1.4. Temsilciyi Çağırma (Invoking a Delegate)
Bir temsilci örneği oluşturup bir metoda bağladıktan sonra, temsilciyi çağırarak bağlandığı metodu (veya metotları) çalıştırabiliriz. Temsilciyi çağırma sözdizimi, normal bir metodu çağırma sözdizimi ile aynıdır.
public class Program
{
static void Main(string[] args)
{
MesajGosterici temsilci1 = Islemler.StatikMesaj;
Islemler islemNesnesi = new Islemler();
MesajGosterici temsilci2 = islemNesnesi.OrnekMesaj;
Hesaplayici hesapTemsilci1 = Islemler.Topla;
Hesaplayici hesapTemsilci2 = islemNesnesi.Carp;
// Temsilcileri çağıralım:
Console.WriteLine("Temsilci 1 çağrılıyor:");
temsilci1("Merhaba Dünya!"); // Islemler.StatikMesaj("Merhaba Dünya!") çağrılır.
Console.WriteLine("\nTemsilci 2 çağrılıyor:");
temsilci2("C# Harika!"); // islemNesnesi.OrnekMesaj("C# Harika!") çağrılır.
Console.WriteLine("\nHesap Temsilcisi 1 çağrılıyor:");
int toplam = hesapTemsilci1(10, 5); // Islemler.Topla(10, 5) çağrılır.
Console.WriteLine($"10 + 5 = {toplam}");
Console.WriteLine("\nHesap Temsilcisi 2 çağrılıyor:");
int carpim = hesapTemsilci2(10, 5); // islemNesnesi.Carp(10, 5) çağrılır.
Console.WriteLine($"10 * 5 = {carpim}");
// Null kontrolü yapmak önemlidir!
MesajGosterici bosTemsilci = null;
// bosTemsilci("Hata!"); // Bu satır NullReferenceException fırlatır.
// Güvenli çağırma (1): if kontrolü
if (bosTemsilci != null)
{
bosTemsilci("Bu mesaj gösterilmeyecek");
}
// Güvenli çağırma (2): Null-conditional operator (C# 6.0+)
bosTemsilci?.Invoke("Bu da gösterilmeyecek");
temsilci1?.Invoke("Bu gösterilecek"); // Invoke() metodu ile çağırma
// Not: Temsilciyi doğrudan çağırmak (temsilci1(...)) ile Invoke() metodunu
// kullanmak (temsilci1.Invoke(...)) genellikle aynıdır. Null-conditional
// operator (?.) kullanımı için Invoke() gereklidir.
}
// ... Islemler sınıfı ve diğer metotlar ...
}
Önemli Not: Bir temsilciye henüz bir metot atanmamışsa (yani değeri null ise) ve onu çağırmaya çalışırsanız, System.NullReferenceException hatası alırsınız. Bu nedenle, bir temsilciyi çağırmadan önce null olup olmadığını kontrol etmek veya C# 6.0 ve sonrasında gelen null-conditional operatörünü (?.Invoke()) kullanmak iyi bir pratiktir.
Bölüm 2: Çoklu Yayın Temsilcileri (Multicast Delegates)
C#’taki tüm temsilci türleri örtük olarak System.MulticastDelegate sınıfından türetilir (System.Delegate sınıfı da System.MulticastDelegate’ten türetilir). Bu, bir temsilci örneğinin aynı anda birden fazla metoda işaret edebileceği anlamına gelir. Bu özelliğe “çoklu yayın” (multicasting) denir.
2.1. Metot Ekleme ve Çıkarma (+= ve -=)
Bir çoklu yayın temsilcisine metot eklemek için + veya += operatörleri, metot çıkarmak için ise — veya -= operatörleri kullanılır.
public class BildirimServisi
{
public void EpostaGonder(string konu)
{
Console.WriteLine($"E-posta gönderildi: {konu}");
}
public void SmsGonder(string icerik)
{
Console.WriteLine($"SMS gönderildi: {icerik}");
}
public void UygulamaIciBildirim(string mesaj)
{
Console.WriteLine($"Uygulama içi bildirim: {mesaj}");
}
}
public delegate void BildirimYapici(string bilgi);
public class Program
{
static void Main(string[] args)
{
BildirimServisi servis = new BildirimServisi();
BildirimYapici bildirimTemsilcisi = null; // Başlangıçta boş
// Metotları ekleyelim
bildirimTemsilcisi += servis.EpostaGonder;
bildirimTemsilcisi += servis.SmsGonder;
bildirimTemsilcisi += servis.UygulamaIciBildirim;
Console.WriteLine("Tüm bildirimler gönderiliyor:");
bildirimTemsilcisi?.Invoke("Acil Durum!");
// Çıktı (sırayla):
// E-posta gönderildi: Acil Durum!
// SMS gönderildi: Acil Durum!
// Uygulama içi bildirim: Acil Durum!
Console.WriteLine("\nSMS bildirimi çıkarılıyor...");
bildirimTemsilcisi -= servis.SmsGonder;
Console.WriteLine("\nKalan bildirimler gönderiliyor:");
bildirimTemsilcisi?.Invoke("Güncelleme Mevcut.");
// Çıktı:
// E-posta gönderildi: Güncelleme Mevcut.
// Uygulama içi bildirim: Güncelleme Mevcut.
// Olmayan bir metodu çıkarmaya çalışmak hata vermez.
bildirimTemsilcisi -= servis.SmsGonder; // Sorun yok.
// Tüm listeyi temizlemek için null atama
// bildirimTemsilcisi = null;
}
}
+= operatörü, mevcut temsilci listesinin sonuna yeni bir metot referansı ekler. -= operatörü ise, belirtilen metot referansını listeden çıkarır (eğer varsa). Eğer çıkarılmaya çalışılan metot listede birden fazla kez bulunuyorsa, genellikle sondaki eşleşme çıkarılır.
2.2. Çoklu Yayın Temsilcilerinin Çağrılması ve Geri Dönüş Değerleri
Bir çoklu yayın temsilcisi çağrıldığında, listesindeki tüm metotlar eklendikleri sırayla (FIFO — First In, First Out) çağrılır.
void Geri Dönüş Tipi: Eğer temsilcinin geri dönüş tipi void ise, tüm metotlar sırayla çalıştırılır.
void Olmayan Geri Dönüş Tipi: Eğer temsilcinin bir geri dönüş tipi varsa (örneğin int), tüm metotlar yine sırayla çalıştırılır, ancak temsilci çağrısının sonucu olarak yalnızca en son çağrılan metodun geri dönüş değeri elde edilir. Diğer metotların geri dönüş değerleri kaybolur.
public delegate int IslemYapici(int deger);
public class Islemci
{
public int IkiyeKatla(int x)
{
Console.WriteLine($"IkiyeKatla çağrıldı: {x} -> {x * 2}");
return x * 2;
}
public int KareAl(int x)
{
Console.WriteLine($"KareAl çağrıldı: {x} -> {x * x}");
return x * x;
}
public int BesEkle(int x)
{
Console.WriteLine($"BesEkle çağrıldı: {x} -> {x + 5}");
return x + 5;
}
}
public class Program
{
static void Main(string[] args)
{
Islemci islemci = new Islemci();
IslemYapici temsilci = null;
temsilci += islemci.IkiyeKatla;
temsilci += islemci.KareAl;
temsilci += islemci.BesEkle; // En son bu eklendi
int sonuc = temsilci(3); // Tüm metotlar 3 ile çağrılacak
Console.WriteLine($"\nTemsilci çağrısının sonucu: {sonuc}");
// Çıktı:
// IkiyeKatla çağrıldı: 3 -> 6
// KareAl çağrıldı: 3 -> 9
// BesEkle çağrıldı: 3 -> 8
//
// Temsilci çağrısının sonucu: 8 (En son çağrılan BesEkle metodunun sonucu)
}
}
2.3. Çoklu Yayında İstisna Yönetimi (Exception Handling)
Çoklu yayın temsilcisi çağrıldığında, eğer listedeki metotlardan biri bir istisna (exception) fırlatırsa, listenin geri kalanındaki metotlar çağrılmaz ve istisna doğrudan temsilciyi çağıran koda iletilir. Bu durum, beklenmedik davranışlara yol açabilir.
Eğer listedeki tüm metotların çalıştırılması (bir hata olsa bile) ve her birinin sonucunun veya fırlattığı istisnanın ayrı ayrı ele alınması gerekiyorsa, Delegate sınıfının GetInvocationList() metodu kullanılmalıdır. Bu metot, çoklu yayın listesindeki her bir metot referansını temsil eden tekil Delegate nesnelerinden oluşan bir dizi döndürür. Bu dizi üzerinde döngü kurarak her bir temsilciyi ayrı ayrı çağırabilir ve istisnaları try-catch blokları içinde yönetebilirsiniz.
public class Program
{
static void Main(string[] args)
{
Islemci islemci = new Islemci();
IslemYapici temsilci = null;
temsilci += islemci.IkiyeKatla;
temsilci += MetodHataVer; // Bu metot hata fırlatacak
temsilci += islemci.KareAl; // Bu metot çağrılmayacak (normal çağrıda)
Console.WriteLine("Normal Çağrı (Hata Alacak):");
try
{
int sonuc = temsilci(5);
Console.WriteLine($"Sonuç: {sonuc}"); // Bu satıra ulaşılamayacak
}
catch (Exception ex)
{
Console.WriteLine($"Hata yakalandı: {ex.Message}");
}
Console.WriteLine("\nGetInvocationList ile Güvenli Çağrı:");
Delegate[] cagriListesi = temsilci.GetInvocationList();
List sonuclar = new List();
List hatalar = new List();
foreach (IslemYapici tekTemsilci in cagriListesi)
{
try
{
int tekSonuc = tekTemsilci(5);
Console.WriteLine($"Başarılı çağrı sonucu: {tekSonuc}");
sonuclar.Add(tekSonuc);
}
catch (Exception ex)
{
Console.WriteLine($"Çağrı sırasında hata yakalandı: {ex.Message}");
hatalar.Add(ex);
}
}
Console.WriteLine("\nİşlem Tamamlandı.");
Console.WriteLine($"Toplam Başarılı Çağrı: {sonuclar.Count}");
Console.WriteLine($"Toplam Hata: {hatalar.Count}");
}
public static int MetodHataVer(int x)
{
Console.WriteLine("MetodHataVer çağrıldı...");
throw new InvalidOperationException("Bu metod kasıtlı olarak hata veriyor!");
// return 0; // Ulaşılamaz kod
}
// ... Islemci sınıfı ve diğer metotlar ...
}
GetInvocationList() kullanarak, her bir abonenin (metodun) bağımsız olarak çalışmasını sağlayabilir ve bir abonenin hatasının diğerlerini etkilemesini önleyebilirsiniz. Bu, özellikle olay (event) mekanizmalarında önemlidir.
Bölüm 3: Anonim Metotlar ve Lambda İfadeleri — Temsilcilerin Evrimi
Temsilciler güçlü olsa da, bazen sadece kısa ömürlü veya tek kullanımlık bir işlevsellik için ayrı bir metot tanımlamak zahmetli olabilir. C#, bu tür durumlar için daha pratik çözümler sunar: Anonim Metotlar ve Lambda İfadeleri. Her ikisi de “inline” olarak kod blokları oluşturarak temsilcilere atama yapmayı sağlar.
3.1. Anonim Metotlar (Anonymous Methods — C# 2.0)
Anonim metotlar, C# 2.0 ile tanıtıldı ve isimsiz kod blokları oluşturarak doğrudan bir temsilci örneğine atama yapmaya olanak tanıdı. delegate anahtar kelimesi kullanılarak tanımlanırlar.
public class Program
{
static void Main(string[] args)
{
// Normal metot ile temsilci atama
Hesaplayici toplaTemsilci = Islemler.Topla;
Console.WriteLine($"Normal Metot: 10 + 5 = {toplaTemsilci(10, 5)}");
// Anonim metot ile temsilci atama
Hesaplayici carpTemsilci = delegate (int x, int y)
{
return x * y;
};
Console.WriteLine($"Anonim Metot: 6 * 7 = {carpTemsilci(6, 7)}");
MesajGosterici mesajTemsilci = delegate (string msg)
{
Console.WriteLine($"Anonim Mesaj: {msg.ToUpper()}");
};
mesajTemsilci("küçük harf");
// Parametre listesi boş olsa bile parantez gereklidir (void için)
Action basitEylem = delegate ()
{
Console.WriteLine("Parametresiz anonim metot çalıştı.");
};
basitEylem();
}
// ... Hesaplayici, MesajGosterici delegate tanımları ve Islemler sınıfı ...
}
Anonim metotlar, özellikle olay yöneticileri (event handlers) gibi küçük kod parçacıkları için ayrı bir metot tanımlama ihtiyacını ortadan kaldırarak kodu daha okunabilir hale getirebilir. Ancak, C# 3.0 ile gelen lambda ifadeleri, anonim metotların yerini büyük ölçüde almıştır.
3.2. Lambda İfadeleri (Lambda Expressions — C# 3.0)
Lambda ifadeleri, anonim metotları yazmanın çok daha kısa ve öz bir yolunu sunar. Fonksiyonel programlama konseptlerinden esinlenilmiştir ve özellikle LINQ (Language Integrated Query) ile birlikte C#’ın vazgeçilmez bir parçası haline gelmiştir.
Lambda ifadesi => operatörünü kullanır. Bu operatör, sol tarafındaki parametre listesini sağ tarafındaki ifade veya ifade bloğuna bağlar.
İfade Lambdası (Expression Lambda): Sağ tarafı tek bir ifadeden oluşur. Bu ifadenin sonucu, lambda’nın geri dönüş değeri olur. {} ve return anahtar kelimesi kullanılmaz.
İfade Bloğu Lambdası (Statement Lambda): Sağ tarafı {} içine alınmış bir veya daha fazla ifadeden (statement) oluşur. Geri dönüş değeri olan lambdalarda return anahtar kelimesi açıkça kullanılmalıdır.
public class Program
{
static void Main(string[] args)
{
// Hesaplayici temsilcisi için lambda ifadeleri
// İfade Lambdası (Expression Lambda)
Hesaplayici lambdaTopla = (x, y) => x + y;
Console.WriteLine($"İfade Lambdası Topla: 8 + 9 = {lambdaTopla(8, 9)}");
// İfade Bloğu Lambdası (Statement Lambda)
Hesaplayici lambdaCarp = (a, b) =>
{
Console.WriteLine($"Çarpma işlemi: {a} * {b}");
return a * b; // return gerekli
};
Console.WriteLine($"İfade Bloğu Lambdası Carp: 4 * 5 = {lambdaCarp(4, 5)}");
// MesajGosterici temsilcisi için lambda
MesajGosterici lambdaMesaj = mesaj => Console.WriteLine($"Lambda Mesaj: {mesaj}");
lambdaMesaj("Çok Kısa!");
// Parametresiz lambda (Action için)
Action lambdaEylem = () => Console.WriteLine("Parametresiz lambda çalıştı.");
lambdaEylem();
// Tek parametre varsa parantez opsiyoneldir
MesajGosterici lambdaMesajKisa = m => Console.WriteLine($"Kısa Lambda: {m}");
lambdaMesajKisa("Parantezsiz");
// Türleri açıkça belirtmek de mümkündür (nadiren gerekir)
Hesaplayici lambdaTurBelirt = (int x, int y) => x - y;
Console.WriteLine($"Tür Belirtilmiş Lambda: 20 - 7 = {lambdaTurBelirt(20, 7)}");
}
// ... Delegate tanımları ...
}
Lambda İfadelerinin Avantajları:
Kısalık ve Okunabilirlik: Özellikle basit işlemler için çok daha az kod gerektirirler.
Tür Çıkarımı (Type Inference): Çoğu durumda derleyici, parametrelerin türlerini temsilci tanımından çıkarabilir, bu da kodun daha da kısalmasını sağlar.
Closures (Kapanımlar): Lambda ifadeleri (ve anonim metotlar), tanımlandıkları kapsamdaki (scope) yerel değişkenlere erişebilir ve bunları “yakalayabilir” (capture). Bu çok güçlü bir özelliktir.
3.3. Closures (Kapanımlar)
Closure, bir fonksiyonun (lambda veya anonim metot) tanımlandığı kapsamdaki değişkenlere, fonksiyon o kapsamın dışına taşınsa veya kapsam sona erse bile erişmeye devam edebilmesi durumudur.
public class Program
{
static void Main(string[] args)
{
Action eylem = ClosureOrnegi();
Console.WriteLine("eylem çağrılmadan önce.");
eylem(); // Metot bittikten sonra bile 'sayac' değişkenine erişir.
eylem();
eylem();
Action baskaEylem = ClosureOrnegi(); // Yeni bir 'sayac' örneği oluşturulur.
baskaEylem();
}
public static Action ClosureOrnegi()
{
int sayac = 0; // Bu değişken lambda tarafından yakalanacak (captured).
// Bu lambda, tanımlandığı 'sayac' değişkenine erişir.
Action artirici = () =>
{
sayac++;
Console.WriteLine($"Sayaç değeri: {sayac}");
};
Console.WriteLine("ClosureOrnegi metodu bitiyor, artirici döndürülüyor.");
return artirici; // Lambda, 'sayac' değişkenini de "yanında götürür".
}
}
Bu örnekte, ClosureOrnegi metodu bittikten sonra bile, döndürülen artirici (ve eylem değişkenine atanan) lambda, kendi sayac değişkeninin durumunu korur ve her çağrıldığında onu artırır. baskaEylem için ClosureOrnegi tekrar çağrıldığında, tamamen yeni ve bağımsız bir sayac değişkeni yakalanır.
Dikkat: Yakalanan değişkenlerin ömrü lambda’nın ömrüne bağlı hale gelir. Bu, beklenenden uzun süre bellekte kalmalarına ve potansiyel bellek sızıntılarına (özellikle olay abonelikleri gibi durumlarda dikkatli olunmazsa) yol açabilir.
Bölüm 4: Genel Temsilciler: Action, Func ve Predicate
Her farklı metot imzası için özel bir temsilci türü tanımlamak yerine, .NET Framework (ve .NET Core/.NET 5+), en yaygın kullanılan imzalar için önceden tanımlanmış genel (generic) temsilci türleri sunar. Bu, kod tekrarını azaltır ve standartlaşmayı sağlar.
4.1. Action<…> Temsilcileri
Action temsilcileri, geri dönüş değeri olmayan (void) metotları işaret etmek için kullanılır. Farklı sayıda parametre alabilen çeşitli versiyonları vardır:
Action: Parametre almayan, void döndüren metotlar için.
Action: Bir adet T türünde parametre alan, void döndüren metotlar için.
Action: İki adet (T1, T2) parametre alan, void döndüren metotlar için.
… (16 parametreye kadar Action)
public class Program
{
static void Main(string[] args)
{
// Action: Parametresiz, void
Action eylem1 = () => Console.WriteLine("Action çalıştı!");
eylem1();
// Action: Bir string parametre, void
Action eylem2 = mesaj => Console.WriteLine($"Action: {mesaj}");
eylem2("Merhaba Generic Delegate!");
// Action: İki int parametre, void
Action eylem3 = (x, y) =>
{
int toplam = x + y;
Console.WriteLine($"{x} + {y} = {toplam}");
};
eylem3(15, 25);
}
}
Artık MesajGosterici gibi özel temsilci tanımları yerine Action kullanabiliriz.
4.2. Func<…, TResult> Temsilcileri
Func temsilcileri, bir geri dönüş değeri olan metotları işaret etmek için kullanılır. Son generic tip parametresi (TResult) her zaman metodun geri dönüş türünü belirtir. Önceki tip parametreleri (varsa) metodun parametre türlerini belirtir.
Func: Parametre almayan, TResult türünde değer döndüren metotlar için.
Func: Bir adet T türünde parametre alan, TResult türünde değer döndüren metotlar için.
Func: İki adet (T1, T2) parametre alan, TResult türünde değer döndüren metotlar için.
… (16 parametreye kadar Func)
public class Program
{
static void Main(string[] args)
{
// Func: Parametresiz, int dönen
Func rastgeleSayi = () => new Random().Next(1, 101);
Console.WriteLine($"Rastgele Sayı: {rastgeleSayi()}");
// Func: string alan, int dönen (uzunluk)
Func metinUzunlugu = metin => metin.Length;
Console.WriteLine($"'C# Delegates' uzunluğu: {metinUzunlugu("C# Delegates")}");
// Func: İki double alan, double dönen (ortalama)
Func ortalama = (a, b) => (a + b) / 2.0;
Console.WriteLine($"10.5 ve 20.3 ortalaması: {ortalama(10.5, 20.3)}");
}
}
Artık Hesaplayici gibi özel temsilci tanımları yerine Func kullanabiliriz.
4.3. Predicate Temsilcisi
Predicate, özel bir Func temsilcisidir. Genellikle bir koleksiyondaki öğeleri filtrelemek veya belirli bir koşulu test etmek için kullanılır. Bir T türünde parametre alır ve bu parametrenin belirli bir koşulu sağlayıp sağlamadığını belirten bir bool değer döndürür (true veya false).
public class Program
{
static void Main(string[] args)
{
List sayilar = new List { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Predicate: int alıp bool dönen (çift sayı kontrolü)
Predicate ciftMi = sayi => sayi % 2 == 0;
Console.WriteLine($"5 çift mi? {ciftMi(5)}"); // False
Console.WriteLine($"6 çift mi? {ciftMi(6)}"); // True
// List.FindAll metodu Predicate kullanır
List ciftSayilar = sayilar.FindAll(ciftMi);
// Veya doğrudan lambda ile:
// List ciftSayilar = sayilar.FindAll(sayi => sayi % 2 == 0);
Console.WriteLine("Çift Sayılar:");
foreach (int cift in ciftSayilar)
{
Console.Write($"{cift} "); // 2 4 6 8 10
}
Console.WriteLine();
// Başka bir Predicate örneği: 5'ten büyük mü?
Predicate bestenBuyuk = n => n > 5;
List buyukSayilar = sayilar.FindAll(bestenBuyuk);
Console.WriteLine("\n5'ten Büyük Sayılar:");
buyukSayilar.ForEach(s => Console.Write($"{s} ")); // 6 7 8 9 10
Console.WriteLine();
}
}
Action, Func ve Predicate temsilcileri, C# kodunda temsilcilerle çalışırken standart ve okunabilir bir yol sunar. Özel bir temsilci adı kodun anlaşılırlığına önemli bir katkı sağlamıyorsa, bu genel temsilcileri kullanmak genellikle en iyi pratiktir.
Bölüm 5: Temsilciler ve Olaylar (Events)
Temsilcilerin en yaygın ve önemli kullanım alanlarından biri olay tabanlı programlamadır. Olaylar (Events), bir nesnenin (yayıncı — publisher) durumunda bir değişiklik olduğunda veya belirli bir eylem gerçekleştiğinde, ilgilenen diğer nesnelere (aboneler — subscribers) bildirim göndermesini sağlayan bir mekanizmadır.
5.1. Olayların Arkasındaki Mekanizma: Temsilciler
C#’ta olaylar, temel olarak temsilciler üzerine kurulmuş özel bir mekanizmadır. Bir olay tanımladığınızda, aslında derleyici arka planda özel erişim denetimlerine sahip bir temsilci alanı (genellikle private) ve bu alana abone eklemek (add erişimcisi) ve çıkarmak (remove erişimcisi) için genel (public) metotlar oluşturur.
Neden Doğrudan Public Temsilci Yerine Olay Kullanmalı?
Eğer bir sınıfta public bir temsilci alanı tanımlarsanız, dışarıdan herhangi bir kod:
Temsilciye doğrudan yeni bir değer atayarak (=) mevcut tüm aboneleri silebilir.
Temsilciyi doğrudan çağırarak olayı istediği zaman tetikleyebilir.
Temsilcinin abone listesine (GetInvocationList()) erişebilir.
Bu durumlar genellikle istenmez ve kapsülleme (encapsulation) ilkesini ihlal eder. Olaylar (event anahtar kelimesi ile tanımlanır) bu sorunları çözer:
Olaylara yalnızca sınıfın dışından += (abone ekleme) ve -= (abone çıkarma) operatörleriyle erişilebilir. Doğrudan atama (=) veya çağırma yapılamaz.
Olayı yalnızca olayın tanımlandığı sınıfın içinden tetikleyebilirsiniz (çağırabilirsiniz).
5.2. Olay Tanımlama ve Kullanma
Standart .NET olay deseni genellikle aşağıdaki adımları içerir:
Temsilci Tanımı: Olayın imzasını belirleyen bir temsilci tanımlanır. Genellikle void döndüren ve iki parametre alan bir temsilci kullanılır:
object sender: Olayı tetikleyen nesne.
EventArgs (veya ondan türetilmiş bir sınıf): Olayla ilgili ek verileri taşıyan nesne. Eğer ek veri yoksa EventArgs.Empty kullanılır.
Standart temsilci EventHandler veya EventHandler genellikle bu amaç için yeterlidir. (EventHandler = Action, EventHandler = Action)
Olay Tanımı: event anahtar kelimesi ve tanımlanan temsilci türü kullanılarak sınıf içinde olay tanımlanır.
Olay Tetikleme Metodu: Genellikle protected virtual void OnEventName(…) şeklinde bir metot tanımlanır. Bu metot, olayın tetiklenmesi gerektiğinde çağrılır. İçinde, olaya abone olanların olup olmadığını kontrol eder (null kontrolü) ve ardından temsilciyi çağırarak olayı tetikler. virtual olması, türetilmiş sınıfların olayın tetiklenme davranışını değiştirmesine olanak tanır.
Abone Olma/Abonelikten Çıkma: İlgilenen sınıflar (aboneler), olayı yayınlayan nesnenin olayına += operatörü ile bir olay yöneticisi metodu (event handler) eklerler. İlgilerini kaybettiklerinde -= operatörü ile abonelikten çıkarlar (bellek sızıntılarını önlemek için bu önemlidir).
Örnek: Bir sayacın değeri belirli bir eşiği aştığında olay tetikleyen bir sınıf.
// 1. Olay verisi için EventArgs türetilmiş sınıf (opsiyonel ama iyi pratik)
public class EsikAsildiEventArgs : EventArgs
{
public int UlasilanDeger { get; }
public int EsikDegeri { get; }
public DateTime Zaman { get; }
public EsikAsildiEventArgs(int ulasilanDeger, int esikDegeri)
{
UlasilanDeger = ulasilanDeger;
EsikDegeri = esikDegeri;
Zaman = DateTime.Now;
}
}
// Sayaç Sınıfı (Yayıncı - Publisher)
public class Sayac
{
private int _deger;
private readonly int _esik;
// 2. Olay için temsilci (EventHandler kullanıyoruz)
public event EventHandler EsikAsildi;
// Arka planda: private EventHandler EsikAsildi;
// public void add_EsikAsildi(...) { ... }
// public void remove_EsikAsildi(...) { ... }
public Sayac(int esik)
{
_esik = esik;
}
public void Artir()
{
_deger++;
Console.WriteLine($"Sayaç Değeri: {_deger}");
// Eşik aşıldı mı kontrol et
if (_deger >= _esik)
{
// 3. Olayı tetikleyen metodu çağır
OnEsikAsildi(new EsikAsildiEventArgs(_deger, _esik));
}
}
// 3. Olayı tetikleyen metot (protected virtual)
protected virtual void OnEsikAsildi(EsikAsildiEventArgs e)
{
// Abone var mı kontrol et ve olayı tetikle
EventHandler handler = EsikAsildi;
handler?.Invoke(this, e); // this: olayı tetikleyen nesne (Sayac)
// Veya: EsikAsildi?.Invoke(this, e);
}
}
// Abone Sınıfı
public class Kaydedici
{
public void Sayac_EsikAsildi(object sender, EsikAsildiEventArgs e)
{
// 4. Olay Yöneticisi Metodu
Console.WriteLine("--- Kaydedici Bildirimi ---");
Console.WriteLine($"Olayı Tetikleyen: {sender?.GetType().Name}"); // Sayac
Console.WriteLine($"Eşik Değeri ({e.EsikDegeri}) aşıldı.");
Console.WriteLine($"Ulaşılan Değer: {e.UlasilanDeger}");
Console.WriteLine($"Zaman: {e.Zaman}");
Console.WriteLine("---------------------------");
}
}
public class Program
{
static void Main(string[] args)
{
Sayac sayac = new Sayac(3); // Eşik değeri 3
Kaydedici kaydedici = new Kaydedici();
BildirimYapici bildirimci = new BildirimYapici(); // Başka bir abone
// 4. Abone Olma
sayac.EsikAsildi += kaydedici.Sayac_EsikAsildi;
sayac.EsikAsildi += bildirimci.Goster; // Başka bir abone ekleyelim
// Lambda ile de abone olunabilir
sayac.EsikAsildi += (sender, e) => {
Console.WriteLine($"*** Lambda Abone: Eşik {e.EsikDegeri} aşıldı (Değer: {e.UlasilanDeger}) ***");
};
Console.WriteLine("Sayaç artırılıyor...");
sayac.Artir(); // Değer 1
sayac.Artir(); // Değer 2
sayac.Artir(); // Değer 3 -> Eşik aşıldı, olay tetiklenecek!
sayac.Artir(); // Değer 4 -> Eşik aşıldı, olay tekrar tetiklenecek!
// Abonelikten Çıkma (Önemli!)
// Eğer kaydedici nesnesi artık kullanılmayacaksa veya
// bu olayı dinlemesi gerekmiyorsa abonelikten çıkılmalı.
sayac.EsikAsildi -= kaydedici.Sayac_EsikAsildi;
Console.WriteLine("\nKaydedici abonelikten çıkarıldı.");
sayac.Artir(); // Değer 5 -> Olay tetiklenecek ama kaydedici çağrılmayacak.
}
}
// Başka bir Abone Sınıfı
public class BildirimYapici
{
public void Goster(object sender, EsikAsildiEventArgs e)
{
Console.WriteLine($"!!! Bildirim: Sayac {e.UlasilanDeger} değerine ulaştı. !!!");
}
}
Bu örnek, olayların temsilcileri kullanarak nasıl bir yayıncı-abone (publisher-subscriber) deseni oluşturduğunu gösterir. Bu desen, bileşenler arasındaki bağımlılığı azaltır (loose coupling), çünkü yayıncı, abonelerinin kim olduğunu veya ne yaptığını bilmek zorunda değildir; sadece olay gerçekleştiğinde bildirimde bulunur.
Bölüm 6: İleri Düzey Konular: Kovaryans ve Kontravaryans
C# temsilcileri, generic tür parametreleri için kovaryans ve kontravaryansı destekler. Bu özellikler, temsilci türleri arasında daha esnek atamalar yapılmasına olanak tanır.
Kovaryans (Covariance — out): Bir metodun, temsilcinin beklediğinden daha türetilmiş bir geri dönüş türüne sahip olmasına izin verir. Bu, Func gibi geri dönüş değeri olan temsilciler için geçerlidir. Generic parametrenin out ile işaretlenmesi gerekir.
Kontravaryans (Contravariance — in): Bir metodun, temsilcinin beklediğinden daha az türetilmiş (yani daha temel) bir parametre türüne sahip olmasına izin verir. Bu, Action gibi parametre alan temsilciler için geçerlidir. Generic parametrenin in ile işaretlenmesi gerekir.
Örnek:
// Kovaryans Örneği (Geri Dönüş Tipi)
// String, object'ten türetilmiştir.
Func objectFunc = () => "Merhaba"; // String döndüren lambda, Func'e atanabilir.
Func stringFunc = () => "Dünya";
// Func türündeki bir temsilci, Func türündeki bir değişkene atanabilir.
// Çünkü string döndüren bir metot, object döndüren bir metotun beklentisini karşılar.
Func kovarTemsilci = stringFunc;
Console.WriteLine($"Kovaryans: {kovarTemsilci()}"); // "Dünya" yazdırır.
// Kontravaryans Örneği (Parametre Tipi)
// Action, Action'in beklentisinden daha temel bir parametre alır.
Action objectAction = obj => Console.WriteLine($"Object Action: {obj}");
Action stringAction = str => Console.WriteLine($"String Action: {str.ToUpper()}");
// Action türündeki bir temsilci, Action türündeki bir değişkene atanabilir.
// Çünkü object alabilen bir metot, string parametresini de güvenle işleyebilir.
Action kontraTemsilci = objectAction;
kontraTemsilci("test mesajı"); // "Object Action: test mesajı" yazdırır.
// stringAction = objectAction; // Bu atama geçersizdir (ters yön).
// Yerleşik Action ve Func tanımları zaten kovaryans/kontravaryans destekler:
// public delegate void Action(T obj);
// public delegate TResult Func();
// public delegate TResult Func(T arg);
Kovaryans ve kontravaryans, özellikle generic koleksiyonlar ve LINQ gibi alanlarda, kodun daha esnek ve yeniden kullanılabilir olmasına yardımcı olur.
Bölüm 7: Temsilcilerin Kullanım Alanları ve Desenler
Temsilciler, C# programlamada birçok farklı senaryoda ve tasarım deseninde kullanılır:
Olay Güdümlü Programlama: UI olayları (buton tıklamaları, fare hareketleri), arka plan işlemleri tamamlandığında bildirim, nesne durumu değiştiğinde haber verme vb. (Yukarıda detaylı incelendi).
Callback Metotları: Bir işlemin tamamlanmasından sonra çağrılacak bir metodu başka bir metoda geçmek için kullanılır. Özellikle asenkron programlamada (async/await öncesi veya bazı özel durumlarda) sıkça görülür.
public void UzunSurenIslem(int veri, Action islemBitince) { Console.WriteLine("İşlem başlıyor..."); System.Threading.Thread.Sleep(2000); // İşlem simülasyonu string sonuc = $"İşlem tamamlandı. Veri: {veri}"; islemBitince?.Invoke(sonuc); // İşlem bitince callback metodunu çağır } // Kullanım: // UzunSurenIslem(123, sonuc => Console.WriteLine($"Callback çağrıldı: {sonuc}"));
LINQ (Language Integrated Query): LINQ sorgu operatörlerinin ( Where, Select, OrderBy, GroupBy, Count, Any vb.) neredeyse tamamı, filtreleme, dönüşüm veya sıralama mantığını belirtmek için Func veya Action temsilcilerini (genellikle lambda ifadeleriyle) parametre olarak alır.
List sayilar = new List { 1, 5, 2, 8, 3, 7 }; // Where metodu Func (yani Predicate) alır. var besdenKucukler = sayilar.Where(n => n < 5); // Select metodu Func alır (burada int -> string dönüşümü). var metinler = sayilar.Select(n => $"Sayı: {n}");
Strateji Deseni (Strategy Pattern): Bir algoritmanın farklı varyasyonlarını uygulamak ve çalışma zamanında hangisinin kullanılacağını seçmek için kullanılabilir. Farklı algoritmalar temsilcilere atanabilir ve gerektiğinde çağrılabilir.
Komut Deseni (Command Pattern): Bir isteği, isteği yapan nesneden bağımsız olarak bir nesne içinde kapsüllemek için kullanılabilir. Temsilciler, bu komut nesnesinin “Execute” işlemini temsil etmek için idealdir.
Threading: Thread sınıfının yapıcısı ThreadStart (parametresiz void) veya ParameterizedThreadStart (object alan void) temsilcilerini kabul eder. Task Parallel Library (TPL) da yoğun olarak Action ve Func kullanır.
// ThreadStart delegate (Action'a benzer) Thread t = new Thread(() => Console.WriteLine("Başka bir thread çalışıyor.")); t.Start();
Bölüm 8: Performans ve En İyi Pratikler
Performans: Temsilci çağırmak, doğrudan metot çağırmaktan çok küçük bir miktar daha yavaştır çünkü arada bir dolaylılık katmanı vardır. Ancak çoğu uygulama için bu fark ihmal edilebilir düzeydedir. Çoklu yayın temsilcilerini çağırmak, listedeki her metot için ayrı bir çağrı içerdiğinden daha maliyetli olabilir. Lambda ifadeleri ve özellikle closure’lar, ek nesne (closure sınıfı) oluşturulmasına neden olabilir, bu da çok sık kullanılan döngüler içinde performans açısından dikkate alınmalıdır.
Genel Temsilcileri Tercih Edin: Action, Func, Predicate gibi standart generic temsilcileri kullanmak, kodun daha okunabilir ve standart olmasını sağlar. Özel temsilci adları yalnızca anlamsal olarak büyük bir değer katıyorsa (örneğin olay tanımlarında) kullanılmalıdır.
Olaylar için event Kullanın: Kapsüllemeyi sağlamak ve yanlış kullanımı önlemek için public temsilciler yerine her zaman event anahtar kelimesini kullanın.
Null Kontrolü: Temsilcileri çağırmadan önce null olup olmadığını kontrol edin (if (temsilci != null) veya temsilci?.Invoke(…)).
Abonelikten Çıkma: Özellikle olay aboneliklerinde, abone olan nesne artık gerekli değilse veya yayıncı nesnenin ömründen daha kısa bir ömre sahipse, bellek sızıntılarını önlemek için mutlaka -= ile abonelikten çıkın. IDisposable deseni bu konuda yardımcı olabilir.
Closure’lara Dikkat: Lambda ifadeleriyle değişken yakalarken (closure), yakalanan değişkenin ömrünün beklenmedik şekilde uzayabileceğini unutmayın.
İstisna Yönetimi (Multicast): Çoklu yayın temsilcilerinde bir metodun hata vermesinin diğerlerini engelleyebileceğini göz önünde bulundurun. Gerekirse GetInvocationList() ile manuel çağrı ve hata yönetimi yapın.
Sonuç: Temsilcilerin Kalıcı Gücü
C# temsilcileri, basit bir metot referansı konseptinden yola çıkarak dile muazzam bir esneklik ve güç katmıştır. Anonim metotlar ve lambda ifadeleri ile evrilerek daha da pratik hale gelen temsilciler, modern C# programlamanın temelini oluşturur. Olay güdümlü sistemlerden LINQ’nun akıcı sorgularına, asenkron programlamadan tasarım desenlerine kadar birçok alanda karşımıza çıkarlar.
Temsilcileri anlamak ve etkin bir şekilde kullanmak, C# geliştiricilerinin daha modüler, bakımı kolay, esnek ve güçlü uygulamalar oluşturmasının anahtarıdır. Onlar, metotların birinci sınıf vatandaşlar gibi ele alınmasını sağlayarak, kodun daha dinamik ve uyarlanabilir olmasına olanak tanır. C# ekosistemindeki yolculuğunuzda temsilcilerin sunduğu olanakları keşfettikçe, onların neden dilin bu kadar merkezi ve vazgeçilmez bir parçası olduğunu daha iyi anlayacaksınız.
Abdulkadir Güngör - Kişisel WebSite
Abdulkadir Güngör - Kişisel WebSite
Abdulkadir Güngör - Özgeçmiş
Github
Github
Linkedin