Wird geladen...

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 void verwendet (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:


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:


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:

  • 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
  • Action wird verwendet, wenn kein Rückgabewert benötigt wird.
  • Func wird verwendet, wenn ein Rückgabewert erforderlich ist.
  • Die Verwendung integrierter Delegates reduziert Boilerplate-Code.
  • Moderne C#-Codebasen bevorzugen Action und Func gegenü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 IDisposable ist 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 ?.Invoke fü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?


TL;DR

  • delegate: Typsichere Referenzen auf Methoden mit Multicast-Unterstützung.
  • event: Ein kontrollierter Delegate, der Abonnements erlaubt und die Kapselung bewahrt.
  • EventHandler und EventArgs: Folgen dem standardmäßigen .NET-Event-Muster zur Datenübertragung.
  • Action und Func: 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