1. Einführung
Wisst ihr, was interessant ist? Ein Lambda-Ausdruck für sich ist wie ein Rezept ohne Topf. Er beschreibt, was zu tun ist, braucht aber einen "Container", um im Code zu leben. In C# gibt es dafür fertige generische Delegates: Func, Action und Predicate.
Stellt sie euch als fertige Formen für eure Lambdas vor — nehmt die passende Form und gießt eure Logik rein. Keine selbstgebauten Lösungen oder eigene Delegate-Typen, wenn das Nötige schon da ist.
Ein bisschen Geschichte
Früher, wenn man eine Funktion als Parameter übergeben wollte, musste man einen eigenen Delegate-Typ deklarieren. Das war langwierig, nervig und sah so aus:
delegate int Calculate(int x, int y);
Calculate adder = (a, b) => a + b;
Als in C# Func, Action und Predicate auftauchten, konnte man in 90% der Fälle auf manuelle Delegate-Definitionen verzichten. Sieht jetzt viel einfacher und universeller aus:
Func<int, int, int> adder = (a, b) => a + b;
2. Func<T, TResult> — Funktion mit Rückgabewert
Syntax und Zweck
Func ist ein generischer Delegate, der von 0 bis 16 Parameter akzeptiert und einen Wert zurückgibt.
Func<int, int, int> sum = (x, y) => x + y;
Func<typ1, typ2, ..., typN, TResult> — alle Typen außer dem letzten sind Argumenttypen, der letzte ist der Rückgabetyp.
Beispiele
1. Summe von zwei Zahlen
Func<int, int, int> sum = (x, y) => x + y;
Console.WriteLine(sum(3, 5)); // 8
2. Quadrieren einer Zahl
Func<int, int> square = x => x * x;
Console.WriteLine(square(4)); // 16
3. Ohne Parameter
Func<string> greet = () => "Hello, lambda!";
Console.WriteLine(greet());
Visuelle Darstellung
| Signatur | Beispiel | Beschreibung |
|---|---|---|
|
|
Nimmt ein int, gibt ein int zurück |
|
|
Zwei int, gibt ein int zurück |
|
|
Nimmt nichts, gibt einen String zurück |
Wie das in deiner Anwendung aussieht
Angenommen, in unserer Mini-Anwendung (konzeptionelles "Benutzerverzeichnis") haben wir eine Liste von Zahlen — wir wollen verschiedene Operationen darauf anwenden. Zum Beispiel quadrieren oder eine Summe mit einer festen Zahl berechnen:
List<int> numbers = new() { 1, 2, 3, 4, 5 };
Func<int, int> square = x => x * x;
var squares = numbers.Select(square);
Console.WriteLine(string.Join(", ", squares)); // 1, 4, 9, 16, 25
3. Action<T> — Aktion ohne Rückgabe
Action ist der generische Delegate für Methoden, die etwas tun (z. B. auf die Konsole schreiben), aber nichts zurückgeben.
Er kann von 0 bis 16 Parameter annehmen, gibt aber immer void zurück.
Beispiele
1. Auf die Konsole schreiben
Action<string> print = text => Console.WriteLine("Data: " + text);
print("Hello, world!");
2. Aktion ohne Parameter
Action greet = () => Console.WriteLine("Good welcome!");
greet();
3. Aktion mit mehreren Parametern
Action<int, int> showSum = (a, b) => Console.WriteLine($"Sum: {a + b}");
showSum(2, 3); // Sum: 5
Visuelle Darstellung
| Signatur | Beispiel | Beschreibung |
|---|---|---|
|
|
Keine Parameter, keine Rückgabe |
|
|
Ein Parameter |
|
|
Mehrere Parameter |
In unserer Anwendung
Fügen wir dem Benutzerverzeichnis eine Methode hinzu, um alle Namen zu drucken:
List<string> names = new() { "Anna", "Boris", "Vika" };
Action<string> printName = name => Console.WriteLine("User: " + name);
names.ForEach(printName);
// oder so: names.ForEach(name => Console.WriteLine(name));
4. Predicate<T> — Ja oder Nein?
Wenn du eine Funktion brauchst, die für einen Parameter nur true oder false zurückgibt, verwende Predicate<T>. Das ist einfach ein Delegate, der einen Parameter entgegennimmt und ein bool zurückgibt.
Predicate ist quasi die offizielle "wir brauchen eine boolsche Prüfung"-Hülle für Lambdas.
Beispiele
1. Prüfen, ob eine Zahl größer als 5 ist
Predicate<int> isGreaterThanFive = x => x > 5;
Console.WriteLine(isGreaterThanFive(3)); // false
Console.WriteLine(isGreaterThanFive(7)); // true
2. Verwendung mit List<T>.Find
List<int> values = new() { 2, 4, 7, 10 };
int found = values.Find(isGreaterThanFive); // verwendet Predicate<int>
Console.WriteLine(found); // 7
3. Sind alle erwachsen?
List<int> ages = new() { 12, 19, 34 };
bool allAdults = ages.TrueForAll(age => age >= 18);
// TrueForAll erwartet Predicate<int>
Worin unterscheidet es sich von Func<T, bool>?
Tatsächlich sind sie weitgehend austauschbar. Selbst die Microsoft-Dokumentation sagt: "Predicate<T> ist einfach Func<T, bool> für spezielle APIs". Aber Standardbibliotheksmethoden verlangen manchmal genau Predicate.
5. Wie Lambdas in Func, Action, Predicate "passen"
Wenn du ein Lambda schreibst, analysiert C#: "Oh, seine Form passt zum geforderten Delegate — kann eingesetzt werden!"
Func<int, int> f1 = x => x * 2;
Action<string> a1 = text => Console.WriteLine(text);
Predicate<int> p1 = x => x < 10;
Überall Lambdas! Unter der Haube sind das drei verschiedene Delegates mit unterschiedlichen Signaturen.
Anwendung im "echten" Code-Beispiel
List<User> users = new() {
new User("Anna", 24),
new User("Boris", 17),
new User("Vika", 31),
};
// Funktion, die nur erwachsene Nutzer zurückgibt (Predicate<User>)
List<User> adults = users.FindAll(user => user.Age >= 18);
Console.WriteLine("Liste der Erwachsenen: " + string.Join(", ", adults.Select(u => u.Name)));
Und wenn wir die Namen aller Nutzer ausgeben wollen mit Action<User>:
users.ForEach(user => Console.WriteLine(user.Name));
Ihre Namen erhalten (Func<User, string>):
IEnumerable<string> names = users.Select(user => user.Name);
Tabelle zur Übersicht
| Delegate | Signatur | Beispiel-Lambda | Wo verwendet |
|---|---|---|---|
|
T → U | |
Select, beliebige Transformationen |
|
T → void | |
ForEach, Aktions-Methoden |
|
T → bool | |
Find, Exists, Filter |
6. Beispiele mit der internen Anwendung: Schritt für Schritt
Erweitern wir unsere kleine Anwendung "Benutzerverzeichnis". Angenommen, wir haben die Klasse User:
public class User
{
public string Name { get; }
public int Age { get; }
public bool IsActive { get; set; }
public User(string name, int age)
{
Name = name;
Age = age;
IsActive = true;
}
}
1. Func<User, bool> — prüfen, ob ein Benutzer volljährig ist
Func<User, bool> isAdult = user => user.Age >= 18;
Verwendung in LINQ:
var adults = users.Where(isAdult);
2. Predicate<User> — aktiven Benutzer finden
Predicate<User> isActive = user => user.IsActive;
User found = users.Find(isActive);
3. Action<User> — Benutzer deaktivieren
Action<User> deactivate = user => user.IsActive = false;
users.ForEach(deactivate);
4. Func<User, string> — kurze Beschreibung holen
Func<User, string> describe = user => $"{user.Name} ({user.Age})";
var descriptions = users.Select(describe);
All diese Lambdas sind wirklich "Code als Daten", die an Methoden übergeben, in Variablen gespeichert und kombiniert werden können.
7. Nicht offensichtliche Feinheiten und typische Fehler
1. Das Lambda muss zur Signatur des Delegates passen. Wenn die Signatur nicht übereinstimmt, gibt es einen Compiler-Fehler.
Func<int, string> wrong = x => x * 2; // Fehler: string erwartet, int geliefert
// Richtig:
Func<int, string> right = x => (x * 2).ToString();
2. Auf void vs return achten. Action gibt keinen Wert zurück — ein Versuch wie Action<int> a = x => x * x; wird nicht funktionieren, weil ein Wert zurückgegeben wird, obwohl keiner erwartet wird.
3. Predicate<T> und Func<T, bool> sind oft austauschbar, aber nicht immer. Manchmal erwarten Collection-Methoden genau Predicate<T>, manchmal Func<T, bool>. Direkte Zuweisung klappt nicht ohne explizite Umhüllung.
Predicate<int> pred = x => x > 0;
Func<int, bool> func = pred; // Fehler
// Aber:
Func<int, bool> func2 = x => x > 0;
Predicate<int> pred2 = new Predicate<int>(func2); // kann über den Konstruktor konvertiert werden
GO TO FULL VERSION