1. Einführung
Delegate — das ist ein Typ, der die „Signatur“ einer Funktion beschreibt: welche Parameter sie annimmt und was sie zurückgibt. Er ist wie ein Pass für eine Funktion oder ein Clubausweis für Code: passt die Signatur — rein, passt sie nicht — sorry, du kommst nicht rein.
Beispiel:
public delegate int Operation(int a, int b);
So ein Delegate bezeichnet eine Funktion, die zwei int annimmt und ein int zurückgibt. So sah Programmierung „vor den Lambdas“ aus (frisch, aber nicht sehr bequem):
Operation add = delegate (int x, int y)
{
return x + y;
};
Mit dem Einzug der Lambdas wurde der Code deutlich kompakter:
Operation add = (x, y) => x + y;
Interessante Tatsache: In C# ist ein Lambda-Ausdruck einfach eine Möglichkeit, eine anonyme Funktion zu erstellen, die automatisch in eine Instanz des passenden Delegates kompiliert wird. Damit verschwimmt die Grenze zwischen beiden ziemlich schnell!
Eingebaute Delegates: Func, Action, Predicate
Damit man nicht für jeden Fall eigene Delegates erfinden muss, gibt es in C# generische Funktions-Typen:
- Func<T1, T2, ..., TResult> — generischer Delegate, der Parameter (T1, T2, ...) annimmt und einen Wert vom Typ TResult zurückgibt.
- Action<T1, T2, ...> — fast dasselbe, gibt aber nichts zurück (void).
- Predicate<T> — ein Delegate, der einen Parameter annimmt und ein bool zurückgibt (Liebling aller „Filter“).
| Delegate | Rückgabewert | Beispiel-Syntax |
|---|---|---|
|
|
|
|
|
|
|
|
|
Beispiel:
Func<int, int, int> add = (a, b) => a + b;
Action<string> show = s => Console.WriteLine(s);
Predicate<int> isEven = n => n % 2 == 0;
2. Lambda-Ausdrücke "verpackt" als Delegate
Übergabe eines Lambda an eine Methode, die ein Delegate erwartet
Jede Methode, die ein Delegate entgegennimmt, kann auch ein Lambda akzeptieren, sofern dessen Signatur mit der erwarteten übereinstimmt.
public static void CalculateAndShow(int a, int b, Func<int, int, int> operation)
{
int result = operation(a, b);
Console.WriteLine($"Ergebnis: {result}");
}
// Aufruf mit Lambda
CalculateAndShow(5, 7, (x, y) => x * y); // Ergebnis: 35
Beispiel mit Action
void ProcessString(string message, Action<string> processor)
{
processor(message);
}
// Lambda verwenden
ProcessString("Hallo, Welt!", s => Console.WriteLine(s.ToUpper()));
Beispiel mit Predicate
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
List<int> evenNumbers = numbers.FindAll(n => n % 2 == 0);
In solchen Momenten merkt man: wozu eigene Delegates schreiben, wenn es das bewährte "Rad" von Microsoft schon gibt.
Wie Delegates und Lambdas zusammenarbeiten
flowchart TD
A[Lambda-Ausdruck] -->|erstellt| B((Delegate))
B -->|wird als Argument übergeben| C[Methode]
B -->|wird gespeichert| D[Eigenschaft/Feld/Sammlung]
C -->|ruft auf| E[Lambda-Code]
3. Lambda als Delegate an eigene Methoden übergeben
Wenn deine eigene Methode Delegates verwendet, macht ein Lambda die Nutzung extrem flexibel.
Beispiel: unser Mini-Rechner
// Wir definieren eine Methode für Operationen
static int PerformOperation(int a, int b, Func<int, int, int> operation)
{
return operation(a, b);
}
// Verschiedene Operationen anwenden (Lambdas am Werk!)
int sum = PerformOperation(3, 4, (x, y) => x + y);
int multiply = PerformOperation(3, 4, (x, y) => x * y);
int max = PerformOperation(3, 4, (x, y) => x > y ? x : y);
Console.WriteLine(sum); // 7
Console.WriteLine(multiply); // 12
Console.WriteLine(max); // 4
Siehst du, wie elegant sich die Logik ändert, statt drei separate Methoden zu schreiben?
4. Lambdas + Delegates in LINQ (und nicht nur)
In LINQ erwarten die Methoden meist eine „kleine logische Funktion“, also genau ein Delegate. Fast alle Standard-LINQ-Methoden nehmen solche Typen:
- Func<T, bool> fürs Filtern (Where)
- Func<T, TResult> für Transformationen (Select)
- Func<T, TKey> für Sortierungen (OrderBy)
List<string> names = new List<string> { "Anna", "Oleg", "Viktor", "Jana" };
// Nur Namen länger als 3 Zeichen bekommen
var longNames = names.Where(name => name.Length > 3);
// Namen in Großbuchstaben umwandeln
var upperNames = names.Select(name => name.ToUpper());
LINQ ist praktisch „Lambdas auf Speed“, weil alles, was als Delegate übergeben werden kann, auch als Lambda übergeben werden kann!
5. Kombinieren: Speichern und Weitergeben von Lambda-Delegates
Liste von Funktionen
Ein Lambda ist nicht nur eine temporäre anonyme Methode. Du kannst eine Sammlung von Lambdas speichern und sie dynamisch nutzen.
List<Func<int, int, int>> operations = new List<Func<int, int, int>>
{
(x, y) => x + y,
(x, y) => x - y,
(x, y) => x * y,
(x, y) => x / y
};
foreach (var op in operations)
{
Console.WriteLine(op(10, 2));
}
Dieser Trick taucht oft in Tests, Handlern und Plugin-Systemen auf.
Dictionary von Delegates
Wenn du Fan einer plug-and-play-Architektur bist:
var mathFuncs = new Dictionary<string, Func<int, int, int>>
{
{ "plus", (x, y) => x + y },
{ "minus", (x, y) => x - y },
{ "pow", (x, y) => (int)Math.Pow(x, y) }
};
string command = "pow"; // Simulation einer Benutzereingabe
if (mathFuncs.TryGetValue(command, out var operation))
{
Console.WriteLine(operation(2, 5)); // 32
}
else
{
Console.WriteLine("Befehl nicht gefunden");
}
6. Nützliche Feinheiten
Lambda-Delegates: Delegate aus einer Methode zurückgeben
Du kannst Delegates (also auch Lambdas) aus Methoden zurückgeben — das ist fast wie eine kleine Fabrik für Funktionen!
Func<int, int> GetMultiplier(int factor)
{
// Wir geben ein Lambda-Delegate zurück, das mit factor multipliziert
return x => x * factor;
}
var triple = GetMultiplier(3);
Console.WriteLine(triple(5)); // 15
var quadruple = GetMultiplier(4);
Console.WriteLine(quadruple(5)); // 20
So wird ein Closure kein Spielzeug mehr, sondern ein echtes Werkzeug!
Funktionen zusammenstellen: Combine, Delegate.Combine und Multicast-Delegates
Manchmal möchte man, dass ein einziger „Aufruf“ gleich mehrere Funktionen auslöst. Zum Beispiel Event-Handling in GUIs: Button klicken — mehrere Handler reagieren.
Action und Delegates erlauben, mehrere Funktionen „aneinanderzureihen“:
Action<string> pipeline = s => Console.WriteLine("Schritt 1: " + s);
pipeline += s => Console.WriteLine("Schritt 2: " + s.ToUpper());
pipeline += s => Console.WriteLine("Schritt 3: " + s.Length);
pipeline("test");
// Ausgabe:
// Schritt 1: test
// Schritt 2: TEST
// Schritt 3: 4
Praktisch für Ketten von Aktionen oder Event-Handling.
Wichtig: Bei Funktionen, die Werte zurückgeben, erhältst du nur das Ergebnis des letzten Aufrufs (das „Endergebnis“). Bei Action hingegen werden alle aufgerufen, weil sie void zurückgeben.
Lambda-Delegates als Callback-Parameter
Klassischer Anwendungsfall: „Ruf mich an, wenn du fertig bist“.
void DownloadFile(string url, Action<string> onFinish)
{
// Simulation des Dateidownloads...
System.Threading.Thread.Sleep(500); // (Simulation einer langen Arbeit, nicht in echten GUI-Anwendungen verwenden)
onFinish($"Download von '{url}' abgeschlossen!");
}
DownloadFile("http://example.com", msg => Console.WriteLine(msg));
In echten Anwendungen (z.B. beim Arbeiten mit APIs, Datenbanken, Dateien) ist das ein Standard-Pattern.
Higher-Order-Funktion: Beispiel Filter mittels Predicate
Lambdas und Delegates erlauben dir, generische Funktionen zu schreiben, die andere Funktionen als Parameter nehmen. Das ist die Basis von Higher-Order-Functions.
List<int> Filter(List<int> source, Predicate<int> condition)
{
List<int> result = new List<int>();
foreach (var item in source)
if (condition(item)) result.Add(item);
return result;
}
// Verschiedene Lambdas verwenden:
var onlyPositive = Filter(new List<int> { -2, 0, 2, 7 }, x => x > 0);
var onlyEven = Filter(new List<int> { -2, 0, 2, 7 }, x => x % 2 == 0);
Man kann sogar „Fabriken“ für Lambdas bauen:
Func<int, Predicate<int>> GetRangeChecker(int min, int max) =>
x => x >= min && x <= max;
Predicate<int> inRange = GetRangeChecker(1, 10);
var filtered = Filter(new List<int> { 0, 5, 10, 15 }, inRange); // 5, 10
7. Typische Fehler und Feinheiten beim Kombinieren
Wenn die Signatur der Parameter nicht übereinstimmt, meckert der Compiler (CS1660: "Cannot convert lambda expression").
Ein Lambda kann unerwartet Variablen „mitmerken“, die später verändert werden (Closure) — achte auf Variable Captures.
Multicast-Delegates mit Rückgabewerten liefern nur das Ergebnis des letzten Handlers zurück — das überrascht viele Anfänger.
An Stellen, wo ein Expression<Func<int,bool>> verlangt wird, kann man keine statement-Lambda einsetzen — es wird explizit eine expression-Lambda benötigt.
GO TO FULL VERSION