1. Callback und asynchrones Programmieren
Callback (callback) — das ist der Mechanismus, mit dem du eine Methode übergibst, die nach Abschluss einer Operation aufgerufen werden soll. Sehr häufig verwendet bei asynchronen Operationen, Timern, Datenverarbeitung und in UIs.
Beispiel 1: Asynchrone Operation mit Callback
Angenommen, wir haben eine App, in der der Benutzer eine Anfrage eingibt und das Ergebnis verzögert kommt (z.B. aus dem Netz). Nach Eintreffen der Daten wollen wir den Bildschirm aktualisieren.
// Delegat für den Callback
public delegate void DataReceivedHandler(string result);
// Mechanismus zum asynchronen Laden von Daten (Simulation)
public void DownloadDataAsync(DataReceivedHandler callback)
{
// Angenommen, das Laden dauert etwas (imitieren wir mit einem Timer)
Task.Delay(1000).ContinueWith(_ =>
{
string data = "Suchergebnisse: <daten>";
callback(data); // Aufruf des Delegate-Callbacks
});
}
// Verwendung:
DownloadDataAsync(result =>
{
Console.WriteLine("Erhalten: " + result);
});
Dieser Ansatz erlaubt sehr flexiblen Code, bei dem die Logik nach Erhalt des Ergebnisses komplett von der Mechanik des Datenerhalts getrennt ist.
2. Delegates als Methodenparameter: Strategy und Comparatoren
Häufige Aufgabe: dem Nutzer erlauben, eigene "Logik" (Funktion) an deine Methode zu übergeben, damit er selbst bestimmen kann, wie Elemente verglichen, gefiltert oder transformiert werden.
Beispiel 2: Strategy-Pattern über Delegates
Angenommen, wir haben eine Sortierfunktion, aber wir wollen, dass der Nutzer unterschiedlich sortieren kann — nach Name, Datum, Größe usw.
public delegate bool CompareFunc(int a, int b);
public void BubbleSort(int[] arr, CompareFunc compare)
{
for (int i = 0; i < arr.Length; i++)
{
for (int j = 0; j < arr.Length - 1; j++)
{
if (compare(arr[j], arr[j + 1]))
{
// Vertauschen
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
// Sortierung von groß nach klein
CompareFunc descending = (a, b) => a < b;
// Verwendung
int[] numbers = { 3, 1, 4, 2 };
BubbleSort(numbers, descending);
Console.WriteLine(string.Join(", ", numbers)); // Gibt aus: 4, 3, 2, 1
So ein Pattern ist ein universeller Weg, um deine "Strategie" in fremden Code zu injizieren, ohne diesen ändern zu müssen.
3. Anonyme Methoden, Lambda-Ausdrücke und Delegates
Mit der Weiterentwicklung von C# ist es mühselig geworden, für jede kleine Aufgabe eine eigene Klasse oder Methode zu schreiben. Glücklicherweise gibt es anonyme Methoden und Lambda-Ausdrücke, mit denen du Delegates "on the fly" erzeugen kannst.
Beispiel 3: Lambda als Delegate
Func<int, int, int> operation = (x, y) => x * y;
int result = operation(3, 5); // 15
Beispiel 4: Auswahl der Operation per Name (switch + Delegates)
Func<int, int, int> op;
string userInput = "sum"; // "sub", "mul", "div"
switch (userInput)
{
case "sum": op = (a, b) => a + b; break;
case "sub": op = (a, b) => a - b; break;
case "mul": op = (a, b) => a * b; break;
case "div": op = (a, b) => a / b; break;
default: throw new Exception("Unbekannte Operation!");
}
Console.WriteLine(op(6, 2));
Validiere die Eingaben nicht zu vergessen — Delegates geben hier Flexibilität und Lesbarkeit.
4. Delegates und Verarbeitungsketten ("Chain of Responsibility")
Da Delegates Multicast unterstützen, kannst du damit leicht Ketten von Handlern bauen.
Beispiel 5: Kette von Filtern
Stell dir vor, wir haben "Filter", die eine Zeichenkette nacheinander verarbeiten sollen.
public delegate string StringFilter(string input);
string RemoveDigits(string input) => new string(input.Where(ch => !char.IsDigit(ch)).ToArray());
string ToUpper(string input) => input.ToUpper();
StringFilter filters = RemoveDigits;
filters += ToUpper;
// Der Delegate gibt die Zeichenkette durch alle Filter
string text = "Hallo123";
foreach (StringFilter filter in filters.GetInvocationList())
{
text = filter(text);
}
Console.WriteLine(text); // Gibt aus: "HALLO"
Wichtig: wenn du einfach filters(text) aufrufst, wird nur der Wert des letzten Handlers zurückgegeben, nicht der ganze Fluss! Wenn du ein "Durchreichen" des Werts brauchst, iteriere explizit über GetInvocationList(), wie oben gezeigt.
5. Delegates für dynamisches Binden von Verhalten zur Laufzeit
Früher musstest du oft separate Klassen und Interfaces erfinden, um Verhalten auszutauschen. Mit Delegates und Lambdas kannst du einen großen Teil dieses "feinen" Polymorphismus als Funktionen ausdrücken.
Beispiel 6: Verhalten eines Roboters mit dynamischer Command
public class Robot
{
public event Action<string>? OnCommandReceived;
public void ReceiveCommand(string command)
{
OnCommandReceived?.Invoke(command);
}
}
// Verwendung:
var robot = new Robot();
robot.OnCommandReceived += cmd => Console.WriteLine($"Roboter führt aus: {cmd}");
robot.OnCommandReceived += cmd =>
{
if (cmd == "Einschalten")
Console.WriteLine("System startet...");
};
// Test
robot.ReceiveCommand("Einschalten");
robot.ReceiveCommand("Vorwärts bewegen");
Dieser Trick wird oft in Tests, Prototypen, in DI (dependency injection) Containern und zum Weiterreichen von Business-Logik über Parameter verwendet.
6. Delegates als Subscriptions auf Zustand
Angenommen, wir haben eine Klasse, die einen Zustand hält, und bei Änderung dieses Zustands wollen wir alle Subscriber benachrichtigen. Mit Delegates (und Events) ist das trivial.
Beispiel 7: Klasse mit Subscription auf Änderung
public class Notifier<T>
{
private T _value = default!;
public event Action<T>? ValueChanged;
public T Value
{
get => _value;
set
{
if (!Equals(_value, value))
{
_value = value;
ValueChanged?.Invoke(_value);
}
}
}
}
// Verwendung:
var intValue = new Notifier<int>();
intValue.ValueChanged += v => Console.WriteLine($"Neuer Wert: {v}");
intValue.Value = 5; // Event wird ausgelöst
intValue.Value = 10;
Dieser Ansatz ist quasi "reaktives Programmieren in klein", die Basis für MVVM, Data Binding und viele moderne UI-Frameworks.
7. Delegates, Closures und lexikalischer Gültigkeitsbereich
Lambda-Ausdrücke und anonyme Methoden können Variablen aus dem umgebenden Kontext capturen (Closure). Das ist praktisch, führt aber manchmal zu überraschenden Fehlern.
Beispiel 8: Capture einer Variable und die "Loop-Falle"
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
{
actions[i] = () => Console.WriteLine(i);
}
foreach (var a in actions)
a(); // Gibt dreimal 3 aus (!)
Warum? Weil das Closure auf dieselbe Variable i zeigt, die nach der Schleife den Wert 3 hat. Und wenn du die tatsächlichen Werte 0, 1, 2 behalten willst?
for (int i = 0; i < 3; i++)
{
int loopValue = i; // friert den aktuellen Wert ein
actions[i] = () => Console.WriteLine(loopValue);
}
Jetzt funktioniert der Code wie erwartet. Solche Fallen sind eine der häufigsten Fehlerquellen für Anfänger beim Einsatz von Lambdas!
8. Kombinieren von Delegates
Multicast-Delegates enthalten eine Liste von Methoden, und du kannst Handler hinzufügen (+=) oder entfernen (-=).
Besonderheit: Entfernen erfolgt nach Referenz und Signatur
void Handler1() => Console.WriteLine("1");
void Handler2() => Console.WriteLine("2");
Action a = Handler1;
a += Handler2;
a -= Handler1; // Lässt nur Handler2 übrig
a?.Invoke(); // Gibt "2" aus
Beispiel: Dynamisches Management von Handlers
Action a = Handler1;
a += Handler1;
a -= Handler1; // Jetzt bleibt EIN Handler1 in der Liste!
9. Delegates, Erweiterbarkeit und Inversion of Control (IoC)
In großen Anwendungen braucht man oft Komponenten, die fremden Code "aufrufen" können, ohne tight coupling. Delegates helfen dabei, Erweiterungen, Plugins und Callbacks einzubauen, ohne enge Kopplung.
Beispiel: Injection von Verhalten im Konstruktor
public class Greeter
{
private readonly Func<string> _getName;
public Greeter(Func<string> getName)
{
_getName = getName;
}
public void Greet() => Console.WriteLine($"Hallo, {_getName()}!");
}
// Unterschiedliches Verhalten injizieren:
var greeter1 = new Greeter(() => "Anja");
var greeter2 = new Greeter(() => DateTime.Now.ToShortTimeString());
greeter1.Greet(); // "Hallo, Anja!"
greeter2.Greet(); // "Hallo, 14:35!"
Im echten Leben wird das Muster oft für testbaren und wartbaren Code genutzt.
10. Nützliche Nuancen
Delegates in Standardinterfaces und LINQ
Du wirst Delegates zwangsläufig begegnen, wenn du mit LINQ, Collections oder Asynchronität arbeitest.
- Viele Methoden wie List<T>.Find, Array.Sort, Where, Select nehmen Delegates entgegen (Func<T, bool>, Comparison<T> usw.).
- LINQ-Methoden erlauben, Filter-, Transformations- und Aggregationslogik zu übergeben — ohne separate Klassen zu schreiben.
Beispiel: Comparator zum Sortieren von Objekten
var people = new[] { "Ivan", "Maria", "Peter" };
Array.Sort(people, (a, b) => a.Length.CompareTo(b.Length));
Console.WriteLine(string.Join(", ", people)); // Ivan, Peter, Maria
Delegates und Currying (teilweise Anwendung von Argumenten)
Mit anonymen Methoden/Lambdas kannst du einen Teil der Parameter "festlegen" und so eine neue Funktion erzeugen.
Beispiel: Partielle Anwendung
Func<int, int, int> sum = (x, y) => x + y;
// Wir erstellen eine Funktion, die immer 10 addiert
Func<int, int> add10 = y => sum(10, y);
Console.WriteLine(add10(5)); // 15
Besonderheit beim Vergleich von Delegates
In C# kann man Delegates auf Gleichheit (==) prüfen, wenn sie dieselbe Invocation-List haben.
void Handler1() { }
void Handler2() { }
Action a1 = Handler1;
Action a2 = Handler1;
Console.WriteLine(a1 == a2); // True
Action a3 = Handler1; a3 += Handler2;
Action a4 = Handler1; a4 += Handler2;
Console.WriteLine(a3 == a4); // True
Wenn ein Delegate allerdings auf einer anonymen Methode oder Lambda basiert, werden Instanzen verglichen.
Serialisierung von Delegates
Delegates lassen sich serialisieren, aber nur wenn die referenzierten Methoden in serialisierbaren Klassen definiert sind und alle Typen verfügbar sind. Seit .NET 8 ist der BinaryFormatter standardmäßig deaktiviert und gilt als veraltet; in Produktionsszenarien wird Delegate-Serialisierung praktisch nicht verwendet.
Interaktion von Delegates und Events: Wann Delegate, wann Event?
- Ein Delegate ist ein Typ/eine Variable, die man direkt aufrufen kann.
- Ein Event (event) beschränkt den Zugriff auf einen Delegate: extern darf man nur abonnieren/abbestellen (+=/-=), aufrufen darf nur die Klasse selbst.
- Ein Event ist immer vom Delegate-Typ, aber nicht jeder Delegate ist ein Event.
Wie anwenden? Wenn du willst, dass Logik "außerhalb" der Klasse definiert werden kann, nutze Delegates. Wenn du Subscription/Unsubscription kontrollieren und die Variable schützen willst, nutze ein Event.
11. Typische Fehler beim Arbeiten mit Delegates
Fehler Nr.1: Verwechslung zwischen Delegate und Event.
Ein öffentliches Delegate-Feld (public Action MyAction;) statt eines Events (public event Action MyAction;) bricht Kapselung. Externer Code kann versehentlich oder absichtlich alle Subscriber überschreiben (instance.MyAction = null;) oder sie direkt aufrufen, was die Logik der Klasse stört.
Fehler Nr.2: Falscher Umgang mit Rückgabewerten bei Multicast-Delegates.
Wenn ein Delegate einen Rückgabewert hat (z.B. Func<string, int>), dann liefert ein normaler Aufruf (myDelegate("test")) nur das Ergebnis des letzten Methods in der Kette. Um alle Ergebnisse aller Subscriber zu bekommen, musst du über die Invocation-Liste iterieren (GetInvocationList()).
// Beispiel: Ergebnisse aller Subscriber durchgehen
var list = myDelegate.GetInvocationList();
foreach (var d in list)
{
var r = ((Func<string, int>)d)("test");
Console.WriteLine(r);
}
Fehler Nr.3: Capture der Schleifenvariable im Closure.
Klassische Falle für Anfänger: ein Lambda, das innerhalb einer for-Schleife erstellt wurde, captured den Iterator selbst, nicht dessen aktuellen Wert.
// Falsch: alle Actions geben den letzten Wert von i aus
for (int i = 0; i < 3; i++)
{
actions[i] = () => Console.WriteLine(i);
}
// Richtig: für jede Iteration eine lokale Kopie erstellen
for (int i = 0; i < 3; i++)
{
int copy = i;
actions[i] = () => Console.WriteLine(copy);
}
Fehler Nr.4: Memory Leaks durch Subscriptions.
Wenn eine Instanzmethode sich an einen Delegate eines langlebigen Objekts subscribed und sich nicht wieder abmeldet, entsteht ein Leak. Das langlebige Objekt hält eine Referenz auf den Subscriber, und der Garbage Collector kann ihn nicht freigeben. Achte auf Abmeldungen, besonders in Klassen mit begrenztem Lebenszyklus (z.B. UI-Komponenten).
GO TO FULL VERSION