Delegates und Ereignisse in C#
Lernen Sie Delegates und Events in C#, um ereignisgesteuerte Programmierung mit Callbacks und Beispielen umzusetzen.
In C# bilden Delegates und Events die Grundlage der ereignisgesteuerten Programmierung.
Ein Delegate ist eine typsichere Referenz, die auf eine Methode zeigen kann.
Ein Event ermöglicht es einer Klasse, der Außenwelt mitzuteilen, dass „eine bestimmte Aktion stattgefunden hat“.
Diese Mechanismen ermöglichen lose gekoppelte Architekturen und machen es möglich, unterschiedliche Methoden dynamisch auszuführen.
Was ist ein Delegate?
Ein Delegate ist ein Typ, der Referenzen auf Methoden mit einer bestimmten Signatur (Parameter und Rückgabetyp) speichern kann. Mit anderen Worten: Methoden können wie Variablen behandelt werden.
Eine Delegate-Variable kann auf eine kompatible Methode zeigen und diese dynamisch aufrufen. Dadurch wird flexibler und lose gekoppelter Code ermöglicht.
// Delegate-Definition
public delegate void Notify(string message);
class Program
{
static void SendEmail(string message)
{
Console.WriteLine("Email gesendet: " + message);
}
static void Main()
{
Notify notifyHandler;
// Methode dem Delegate zuweisen
notifyHandler = SendEmail;
notifyHandler("Meeting ist um 10:00 Uhr.");
}
}
In diesem Beispiel verweist der Delegate notifyHandler
auf die Methode SendEmail und ruft sie auf.
Multicast-Delegates
Ein Multicast-Delegate ist ein Delegate, der gleichzeitig mehrere Methoden referenzieren kann.
In C# sind Delegates standardmäßig multicastfähig, wenn sie mit += kombiniert werden.
Wird ein Multicast-Delegate aufgerufen, werden alle Methoden in der Invocation List in der Reihenfolge ihrer Hinzufügung ausgeführt. Dies ist besonders nützlich, um eine Benachrichtigung an mehrere Empfänger zu senden.
Beispiel
using System;
class Program
{
delegate void Notify(string message);
static void Main()
{
Notify notifier = LogToConsole;
notifier += LogToFile;
notifier += SendEmail;
notifier("Build abgeschlossen.");
}
static void LogToConsole(string message)
{
Console.WriteLine("[Console] " + message);
}
static void LogToFile(string message)
{
Console.WriteLine("[Datei] " + message);
}
static void SendEmail(string message)
{
Console.WriteLine("[Email] " + message);
}
}
Eine Methode kann mit -= aus der Invocation List entfernt werden:
notifier -= SendEmail;
- Multicast-Delegates werden im Hintergrund von Events verwendet.
- Wenn ein Handler eine Exception auslöst, wird die weitere Ausführung gestoppt.
- Hat der Delegate einen Rückgabewert, wird nur das Ergebnis der letzten Methode zurückgegeben.
Rückgabeverhalten
Wenn ein Multicast-Delegate einen Rückgabetyp besitzt, werden alle Methoden in der Invocation List ausgeführt, jedoch wird nur das Ergebnis der letzten Methode zurückgegeben.
using System;
class Program
{
delegate int Calculate(int x);
static void Main()
{
Calculate calc = Square;
calc += Double;
int result = calc(5);
Console.WriteLine(result);
// Ausgabe: 10 (Ergebnis der letzten Methode: Double)
}
static int Square(int x)
{
Console.WriteLine("Square aufgerufen");
return x * x;
}
static int Double(int x)
{
Console.WriteLine("Double aufgerufen");
return x * 2;
}
}
Sowohl Square als auch Double werden ausgeführt;
zurückgegeben wird jedoch nur das Ergebnis der zuletzt hinzugefügten Methode.
- Alle Methoden werden in Reihenfolge ausgeführt.
- Nur der Rückgabewert der letzten Methode bleibt erhalten.
- Daher werden Multicast-Delegates meist mit dem Rückgabetyp
voidverwendet (wie bei Events).
Was ist ein Event?
Ein Event ist ein Mechanismus, der signalisiert, dass eine Aktion stattgefunden hat.
Events basieren auf Delegates und bieten kontrollierten Zugriff.
Das Schlüsselwort event wird zur Deklaration verwendet,
und von außen kann nur mit += (Abonnieren)
oder -= (Abbestellen) darauf zugegriffen werden.
Dadurch wird sichergestellt:
- Das Event kann nur innerhalb der deklarierenden Klasse ausgelöst werden.
- Externer Code kann die Invocation List nicht überschreiben.
- Die Kapselung (Encapsulation) bleibt gewahrt.
public class Button
{
// Event-Definition (EventHandler ist ein Standard-Delegate-Typ)
public event EventHandler? Click;
public void SimulateClick()
{
Console.WriteLine("Button wurde geklickt!");
// Event auslösen
Click?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
var btn = new Button();
// Event abonnieren
btn.Click += (s, e) => Console.WriteLine("Event: Button wurde geklickt.");
btn.SimulateClick();
}
}
Der Null-Conditional-Operator (?.) stellt sicher,
dass das Event nur ausgelöst wird, wenn Abonnenten vorhanden sind,
und verhindert eine NullReferenceException.
In diesem Beispiel veröffentlicht die Klasse Button
ein Click-Event.
Der abonnierte Lambda-Ausdruck wird ausgeführt,
sobald das Event ausgelöst wird.
Dieses Muster wird häufig in UI-Frameworks wie WinForms und WPF verwendet.
EventHandler und EventArgs
In C# ist der am häufigsten verwendete Standard-Delegate für Events
EventHandler.
Er folgt dem klassischen Event-Muster:
(object sender, EventArgs e).
Durch Ableiten von EventArgs können zusätzliche,
ereignisspezifische Daten übertragen werden.
Dieses Vorgehen entspricht den offiziellen .NET-Event-Designrichtlinien.
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($"Bestellung erstellt (ID={id}, Betrag={amount})");
// Event auslösen
OrderCreated?.Invoke(this, new OrderEventArgs(id, amount));
}
}
class Program
{
static void Main()
{
var service = new OrderService();
service.OrderCreated += (s, e) =>
{
Console.WriteLine($"Benachrichtigung: Bestellung erhalten (#{e.OrderId}, {e.Amount} USD)");
};
service.CreateOrder(101, 250m);
}
}
In diesem Beispiel löst die Klasse OrderService
das Event OrderCreated aus,
wenn eine neue Bestellung erstellt wird.
Der Abonnent erhält die Bestelldaten über die benutzerdefinierte
Klasse OrderEventArgs.
Dieser Ansatz gewährleistet Typsicherheit und macht ereignisgesteuerte Kommunikation klar und strukturiert.
Delegate vs Event (Kapselung)
Obwohl Events auf Delegates basieren, sind sie nicht dasselbe. Der wesentliche Unterschied liegt in Kapselung und Zugriffskontrolle.
Ein Delegate-Feld kann von außen frei verändert werden, während ein Event den externen Zugriff einschränkt und die Invocation List schützt.
Delegate-Beispiel
public class Publisher
{
public Action? OnChange;
}
Da OnChange ein Delegate-Feld ist:
- Kann externer Code es mit
=neu zuweisen. - Können bestehende Abonnenten versehentlich entfernt werden.
- Kann es auf
nullgesetzt werden. - Kann es von außen direkt ausgelöst werden.
publisher.OnChange = null; // Entfernt alle Abonnenten
publisher.OnChange = SomeMethod; // Ersetzt die Invocation List
publisher.OnChange?.Invoke(); // Kann extern ausgelöst werden
Event-Beispiel
public class Publisher
{
public event Action? OnChange;
}
Da OnChange als Event deklariert ist:
- Kann externer Code nur
+=und-=verwenden. - Kann die Invocation List nicht mit
=überschrieben werden. - Kann das Event nicht direkt auslösen.
- Kann das Event nur innerhalb der deklarierenden Klasse ausgelöst werden.
- Delegates bieten Flexibilität.
- Events bieten kontrollierten Zugriff und Kapselung.
- Für öffentliche Benachrichtigungen sollten Events fast immer Delegate-Feldern vorgezogen werden.
Moderne Alternativen: Action & Func
In modernem C# ist es häufig nicht notwendig, eigene Delegate-Typen zu definieren.
Stattdessen können die integrierten generischen Delegates
Action und Func verwendet werden.
Diese Delegates sind flexibel, kompakt und werden im gesamten .NET-Ökosystem verwendet (einschließlich LINQ, asynchroner Programmierung und taskbasierter APIs).
Action
Action repräsentiert eine Methode, die keinen Rückgabewert besitzt.
Sie kann null oder mehrere Parameter akzeptieren.
Action log = message => Console.WriteLine(message);
log("Application started.");
Func
Func<T, TResult> repräsentiert eine Methode mit Rückgabewert.
Der letzte generische Parameter steht immer für den Rückgabetyp.
Func add = (a, b) => a + b;
int result = add(5, 3); // 8
Actionwird verwendet, wenn kein Rückgabewert benötigt wird.Funcwird verwendet, wenn ein Rückgabewert erforderlich ist.- Die Verwendung integrierter Delegates reduziert Boilerplate-Code.
- Moderne C#-Codebasen bevorzugen
ActionundFuncgegenüber eigenen Delegate-Definitionen.
Abmelden & Speicherlecks
Ein wichtiger Aspekt beim Arbeiten mit Events ist das Abmelden (Unsubscribe). Wenn man sich bei einem Event anmeldet, aber nie wieder abmeldet, kann dies zu unerwarteten Speicherlecks führen.
Events halten eine starke Referenz auf ihre Abonnenten. Solange das Publisher-Objekt existiert, kann der Subscriber nicht vom Garbage Collector entfernt werden.
Warum ist das problematisch?
Stellen Sie sich einen langlebigen Publisher (z. B. einen Service oder Singleton) und einen kurzlebigen Subscriber (z. B. eine UI-Komponente) vor. Wenn sich der Subscriber nicht abmeldet, bleibt er im Speicher, obwohl er nicht mehr benötigt wird.
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()
{
// Wichtig: abmelden!
_publisher.OnChange -= HandleChange;
}
}
Wird Dispose nie aufgerufen,
bleibt die Subscriber-Instanz durch das Event referenziert
und kann nicht vom Garbage Collector entfernt werden.
- Melden Sie sich immer von Events ab, wenn die Lebensdauer des Subscribers endet.
- Dies ist besonders wichtig in UI-Frameworks (WinForms, WPF, Blazor usw.).
- Langlebiger Publisher + kurzlebiger Subscriber = potenzielles Speicherleck.
- Die Implementierung von
IDisposableist eine gängige Lösung für sicheres Abmelden.
Thread-Sicherheit & Best Practices
Beim Arbeiten mit Events in Multithreading-Umgebungen
wird Thread-Sicherheit wichtig.
Ein häufiges Problem tritt auf, wenn vor dem Aufruf eines Events
eine null-Prüfung durchgeführt wird.
Mögliche Race Condition
if (OnChange != null)
{
OnChange(this, EventArgs.Empty);
}
Zwischen der null-Prüfung und dem Aufruf
könnte ein anderer Thread das Event abmelden.
Dies kann zu einer NullReferenceException führen.
Sicheres Aufrufmuster
Um diese Race Condition zu vermeiden, kopieren Sie die Delegate-Referenz vor dem Aufruf in eine lokale Variable:
var handler = OnChange;
handler?.Invoke(this, EventArgs.Empty);
Dadurch wird sichergestellt, dass sich die Invocation List zwischen der Null-Prüfung und dem Methodenaufruf nicht ändern kann.
Moderne Syntax
In den meisten Fällen bietet der Null-Conditional-Operator eine kompakte und sichere Möglichkeit, Events aufzurufen:
OnChange?.Invoke(this, EventArgs.Empty);
- Verwenden Sie sichere Aufrufmuster in Multithreading-Szenarien.
- Bevorzugen Sie
?.Invokefür eine saubere und moderne Syntax. - Events sind Multicast-Delegates — ein fehlerhafter Handler kann die Ausführung stoppen.
- Erwägen Sie try/catch innerhalb einzelner Handler-Aufrufe, wenn hohe Zuverlässigkeit erforderlich ist.
Wann Delegates und Events verwenden?
-
Delegates:
Verwenden Sie sie, um Methoden als Parameter zu übergeben,
Callbacks zu implementieren oder flexible Ausführungsabläufe zu erstellen.
In modernem C# werden meist
ActionundFuncbevorzugt. - Events: Verwenden Sie sie, wenn eine Klasse Benachrichtigungen nach außen geben soll, während die Kapselung gewahrt bleibt. Events sind ideal für UI-Interaktionen, Domain-Events und Zustandsänderungen.
- Best Practice: Verwenden Sie Events für öffentliche Benachrichtigungen und vermeiden Sie es, rohe Delegate-Felder öffentlich zugänglich zu machen. Melden Sie sich bei Bedarf ab und verwenden Sie sichere Aufrufmuster.
TL;DR
delegate: Typsichere Referenzen auf Methoden mit Multicast-Unterstützung.event: Ein kontrollierter Delegate, der Abonnements erlaubt und die Kapselung bewahrt.EventHandlerundEventArgs: Folgen dem standardmäßigen .NET-Event-Muster zur Datenübertragung.ActionundFunc: Moderne Alternativen zu benutzerdefinierten Delegate-Typen.- Um Speicherlecks zu vermeiden, sollte man sich bei Bedarf von Events abmelden.
- Verwenden Sie Events für Benachrichtigungen und Delegates (oder
Action/Func) für Callbacks und Ablaufsteuerung.
Ähnliche Artikel
Interfaces und Abstrakte Klassen in C#
Lernen Sie Interfaces und abstrakte Klassen in C#, ihre Unterschiede und den Einsatz für sauberes, erweiterbares Design.
Klassen, Objekte, Eigenschaften und Methoden in C#
Erlernen Sie die Grundlagen von Klassen, Objekten, Eigenschaften und Methoden in C# für objektorientierte Programmierung.
Lambda-Ausdrücke in C#
Lernen Sie Lambda-Ausdrücke in C#, einschließlich Kurzsyntax, Func- und Action-Delegates und LINQ-Beispiele.
Methoden und Parameterverwendung in C#
Lernen Sie Methoden und die Verwendung von Parametern in C#, einschließlich Wert- und Referenzparametern sowie optionalen Parametern.