Lambdas und Delegates
Kurz gesagt: Ein Lambda ist eine kurze Funktion ohne Namen, die du als Wert übergibst; in C# ist sein Typ ein Delegate wie Func, Action oder Predicate.
Problem
Eine ganze Klasse für einen Vergleich
Die Bestellungen sollen nach Summe sortiert werden. Wie das geht, weißt du aus dem Modul „Collections“: Sort mit einem Objekt einer Klasse, die IComparer<Order> implementiert.
class Order{ public string Customer { get; } public double Total { get; }
public Order(string customer, double total) { Customer = customer; Total = total; }}class ByTotal : IComparer<Order>{ public int Compare(Order a, Order b) { return a.Total.CompareTo(b.Total); }}
var orders = new List<Order>{ new Order("Maria", 75.50), new Order("Tom", 12.90), new Order("Laura", 40.00)};
orders.Sort(new ByTotal());
foreach (Order o in orders){ Console.WriteLine($"{o.Customer}: {o.Total:F2} €");}Tom: 12.90 €Laura: 40.00 €Maria: 75.50 €Es funktioniert. Aber für einen Vergleich schreibst du eine ganze Klasse: class ByTotal : IComparer<Order>, public int Compare, Klammern und dann new ByTotal(). Nur eine Zeile sagt etwas: a.Total.CompareTo(b.Total). Und die Klasse steht weit weg von der Stelle, an der du sie benutzt. Du willst nur die Regel übergeben.
Die Idee
Ein Lambda: nur die Regel
Dieselbe Sortierung in einer Zeile:
orders.Sort((a, b) => a.Total.CompareTo(b.Total));(a, b) => a.Total.CompareTo(b.Total) heißt Lambda-Ausdruck, kurz Lambda. Links von => stehen die Parameter, rechts der Rumpf. Das ist die Methode Compare ohne Namen, ohne Typen und ohne return.
Ein Lambda kann man auf drei Arten schreiben:
- Ein einzelner Parameter darf ohne Klammern stehen:
o => o.Total > 50. Ohne Parameter ist es() => .... - Die Typen der Parameter lässt man meist weg, weil der Compiler sie von
Sortkennt. Du kannst sie auch hinschreiben:(Order a, Order b) => .... - Ein Rumpf mit mehreren Zeilen steht in
{ }, und dann istreturnPflicht.
Comparison<Order> byTotal = (a, b) => a.Total.CompareTo(b.Total);
Comparison<Order> byTotalLong = (Order a, Order b) =>{ double diff = a.Total - b.Total; return diff < 0 ? -1 : diff > 0 ? 1 : 0;};
orders.Sort(byTotalLong);foreach (Order o in orders){ Console.WriteLine($"{o.Customer}: {o.Total:F2} €");}Tom: 12.90 €Laura: 40.00 €Maria: 75.50 €Comparison<Order>. Was das ist, steht auf der nächsten Karte.Die Idee
Ein Delegate: der Typ der Regel
Woher weiß C#, dass (a, b) => ... ein Vergleich von zwei Bestellungen ist? Aus dem Typ: Sort hat eine Variante, die ein Comparison<Order> nimmt. Das ist ein Delegate: ein Typ, der eine Methode beschreibt, ihre Parameter und ihr Ergebnis. Comparison<Order> ist „eine Methode, die zwei Bestellungen nimmt und einen int zurückgibt“. Das Lambda wird der Rumpf so einer Methode.
Du kannst dir ein eigenes Delegate machen. Man schreibt es wie eine Methode, mit dem Wort delegate und ohne Rumpf:
delegate double PriceRule(double price);PriceRule student = price => price * 0.8;PriceRule minusFive = price => price - 5;
Console.WriteLine(student(50));Console.WriteLine(minusFive(50));4045student ist ein Objekt vom Typ PriceRule. Du rufst es wie eine Methode auf, mit Klammern: student(50). Zwei Preisregeln und keine einzige Klasse. C# gibt 40 aus, nicht 40.0: Die Zahl ist ganz.PriceRule student = price => price * 0.8;PriceRule minusFive = price => price - 5;
Console.WriteLine(student(50));Console.WriteLine(minusFive(50));Schritt für Schritt, in Worten
- Das Lambda ist ein Objekt vom Typ PriceRule, und student zeigt darauf. Sein Rumpf läuft noch nicht.
- Eine zweite Regel, ein zweites Objekt. Keine einzige Klasse ist geschrieben.
- student(50) übergibt 50 an das Lambda: price ist 50, und price * 0.8 ergibt 40.
- minusFive(50): dieselbe Zahl, eine andere Regel, 45.
Genauer, für Neugierige
- In Java steht ein Lambda dort, wo ein Interface mit einer Methode (ein funktionales Interface) erwartet wird. In C# kann ein Lambda nicht an ein Interface gehen, selbst wenn es nur eine einzige Methode hat:
interface IPriceRule{ double Apply(double price);}
IPriceRule student = price => price * 0.8;error CS1660: Cannot convert lambda expression to type 'IPriceRule' because it is not a delegate typeDeshalb ist in C# eine Regel, die als Lambda übergeben wird, ein Delegate und kein Interface.
student(50)ist kurz fürstudent.Invoke(50). UndGetType()zeigt, dass das Objekt wirklich vom Typ des Delegates ist:
PriceRule student = price => price * 0.8;Console.WriteLine(student.GetType().Name);Console.WriteLine(student.Invoke(50));PriceRule40Die Idee
Die fertigen: Predicate, Func, Action
Du musst nicht für jede Regel ein Delegate schreiben. System hat fertige für die häufigsten Fälle:
Predicate<T>: nimmt einT, gibt einenboolzurück. Das ist eine Bedingung: „Ist die Bestellung groß?“.Func<T, TResult>: nimmt einT, gibt einTResultzurück. Der letzte Typ in den< >ist immer das Ergebnis. Es macht aus einem etwas anderes: aus einer Bestellung ein Etikett.Action<T>: nimmt einT, gibt nichts zurück. Es tut etwas damit: gibt es aus, speichert es.Func<TResult>: nimmt nichts, gibt einTResultzurück. Es gibt dir ein neues Objekt, wenn du fragst.
Alle werden gleich aufgerufen, mit Klammern, wie eine Methode.
Predicate<Order> big = o => o.Total >= 50;Func<Order, string> label = o => o.Customer.ToUpper();Action<string> print = text => Console.WriteLine("-> " + text);Func<Order> guest = () => new Order("Gast", 0);
var order = new Order("Maria", 75.50);Console.WriteLine(big(order));print(label(order));Console.WriteLine(guest().Customer);True-> MARIAGastIn Java sind Regeln Objekte mit Methoden, die sie zusammensetzen: Bedingungen verbindet man mit and, or und negate („nicht“), und Funktionen reiht man mit andThen hintereinander. Delegates in C# haben solche Methoden nicht. Die neue Regel ist ein neues Lambda, das die alten aufruft: o => big(o) && maria(o). Zwei Funktionen hintereinander setzt man auch mit einem Lambda zusammen, das eine Methode zurückgibt:
static Func<double, double> Then(Func<double, double> f, Func<double, double> g){ return x => g(f(x));}
Func<double, double> withVat = price => price * 1.2;Func<double, double> rounded = price => Math.Round(price, 2);
Func<double, double> finalPrice = Then(withVat, rounded);Console.WriteLine(finalPrice(10.555));12.67Then nimmt zwei Delegates und gibt ein drittes zurück: Es ruft das erste auf und übergibt dessen Ergebnis an das zweite.
Genauer, für Neugierige
- Es gibt Varianten mit mehr Parametern:
Func<T1, T2, TResult>nimmt zwei und gibt ein Ergebnis zurück,Action<T1, T2>nimmt zwei und gibt nichts zurück. Sie gehen bis zu 16 Parametern. Predicate<Order>undFunc<Order, bool>beschreiben dasselbe, sind aber verschiedene Typen. Eine Variable des einen kann man nicht dort übergeben, wo das andere erwartet wird.- Delegates für Aktionen kann man mit
+=zusammenzählen. Dann ruft ein Aufruf alle der Reihe nach auf:
Action<string> notify = text => Console.WriteLine("E-Mail: " + text);notify += text => Console.WriteLine("SMS: " + text);notify("deine Bestellung ist unterwegs");E-Mail: deine Bestellung ist unterwegsSMS: deine Bestellung ist unterwegsProbier es aus
Zusammengesetzte Bedingungen
Was gibt das Programm aus?
Predicate<Order> big = o => o.Total >= 50;Predicate<Order> maria = o => o.Customer == "Maria";Predicate<Order> bigAndMaria = o => big(o) && maria(o);Predicate<Order> bigOrMaria = o => big(o) || maria(o);Predicate<Order> notBig = o => !big(o);
var small = new Order("Maria", 30);var large = new Order("Tom", 80);
Console.WriteLine(bigAndMaria(small));Console.WriteLine(bigOrMaria(small));Console.WriteLine(notBig(large));bigAndMaria False, und bigOrMaria ergibt True, weil sie von Maria ist. Toms Bestellung hat 80 €, sie ist groß, und ! dreht die Antwort um: False.Sieh es dir an
Listen nehmen Lambdas
List<T> hat Methoden, die statt einer Schleife eine Regel nehmen:var cart = new List<string> { "Brot", "", "Milch", " ", "Kaffee" };int removed = cart.RemoveAll(item => string.IsNullOrWhiteSpace(item));cart.ForEach(item => Console.WriteLine(item));Console.WriteLine($"Entfernt: {removed}");
Console.WriteLine(cart.Find(item => item.StartsWith('M')));Console.WriteLine(cart.Exists(item => item.Length > 5));List<int> lengths = cart.ConvertAll(item => item.Length);Console.WriteLine(string.Join(", ", lengths));BrotMilchKaffeeEntfernt: 2MilchTrue4, 5, 6RemoveAll(bedingung)entfernt alle Elemente, für die die Bedingung wahr ist, und gibt zurück, wie viele es entfernt hat. Die Bedingung ist einPredicate.ForEach(aktion)führt die Aktion mit jedem Element aus. Sie ist eineAction.Find(bedingung)gibt das erste Element zurück, für das die Bedingung wahr ist, odernull, wenn es keines gibt.Existssagt, ob es mindestens eines gibt.ConvertAll(umwandlung)macht eine neue Liste aus den Ergebnissen. Hier werden die Wörter zu einer Liste ihrer Längen.
Dictionary hat solche Methoden nicht. computeIfAbsent und forEach aus Java gibt es dort nicht, und byCity.ForEach(...) lässt sich nicht kompilieren:
error CS1061: 'Dictionary<string, List<string>>' does not contain a definition for 'ForEach' and no accessible extension method 'ForEach' accepting a first argument of type 'Dictionary<string, List<string>>' could be found (are you missing a using directive or an assembly reference?)Ein Dictionary durchläufst du mit foreach, und Gruppieren in einer Zeile kommt in der nächsten Lektion.
Die Idee
Eine Methode über ihren Namen, ohne Klammern
s => Console.WriteLine(s). Dann übergib die Methode selbst, über ihren Namen und ohne Klammern:var names = new List<string> { "maria", "tom" };names.ForEach(Console.WriteLine);
Func<string, int> parse = int.Parse;var vips = new List<string> { "maria" };Predicate<string> isVip = vips.Contains;Func<string, string> upper = s => s.ToUpper();Func<List<string>> newList = () => new List<string>();
Console.WriteLine(upper("laura"));Console.WriteLine(parse("42") + 1);Console.WriteLine(isVip("tom"));List<string> list = newList();list.Add("Kaffee");Console.WriteLine(string.Join(", ", list));mariatomLAURA43FalseKaffeeDer Name einer Methode ohne Klammern heißt Methodengruppe (method group): Die Gruppe sind alle Methoden mit diesem Namen. Der Compiler wählt die, die zum Delegate passt. Console.WriteLine hat viele Varianten, und ForEach will ein Action<string>, also nimmt er WriteLine(string). Java hat dafür eine eigene Schreibweise, die Methodenreferenz mit ::. Es gibt vier Arten, aber in C# gehen nur zwei über den Namen:
- eine Methode eines bestimmten Objekts:
vips.Containsistname => vips.Contains(name); - eine statische Methode:
int.Parseists => int.Parse(s), undConsole.WriteLineists => Console.WriteLine(s); - eine Methode des Parameters: Dafür gibt es keine Kurzform.
String::toUpperCaseaus Java ist in C# ein Lambda,s => s.ToUpper(); - ein Konstruktor: auch keine Kurzform.
ArrayList::newist in C#() => new List<string>().
Tut das Lambda etwas mehr, zum Beispiel s => s.Trim().ToUpper(), lass es ein Lambda.
Sieh es dir an
Sortieren nach mehreren Feldern
Das Delegate Comparison<T> hat keine Methoden wie comparing und thenComparing in Java. Die Regel für mehrere Felder ist ein einziges Lambda: Ist der Vergleich nach dem ersten Feld nicht 0, entscheidet er, sonst vergleicht es das nächste.
Die Schüler werden nach Note sortiert, von der höchsten an, und bei gleicher Note nach Name:
var students = new List<Student>{ new Student("Jonas", 5.50), new Student("Anna", 6.00), new Student("Peter", 5.50), new Student("Finn", 4.75)};students.Sort((a, b) =>{ int byGrade = b.Grade.CompareTo(a.Grade); return byGrade != 0 ? byGrade : string.Compare(a.Name, b.Name);});Anna 6Jonas 5.5Peter 5.5Finn 4.75b.Grade.CompareTo(a.Grade) stellt die höhere Note nach vorn. Bei gleicher Note ist byGrade 0, und string.Compare(a.Name, b.Name) vergleicht die Namen alphabetisch. Es liest sich von oben nach unten: „nach Note umgekehrt, und bei Gleichheit nach Name“.
Genauer, für Neugierige
- Ohne Typ links weiß der Compiler nicht, welches Delegate das Lambda ist, weil er nicht weiß, was
aundbsind:
var byTotal = (a, b) => a.Total.CompareTo(b.Total);error CS8917: The delegate type could not be inferred.Die Lösung ist der Typ des Delegates links:
Comparison<Order> byTotal = (a, b) => b.Total.CompareTo(a.Total);orders.Sort(byTotal);- Mit Typen an den Parametern lässt sich
varkompilieren, ergibt aberFunc<Order, Order, int>. Das beschreibt dasselbe wieComparison<Order>, ist aber ein anderer Typ, undSortnimmt es nicht an:
var byTotal = (Order a, Order b) => a.Total.CompareTo(b.Total);orders.Sort(byTotal);error CS1503: Argument 1: cannot convert from 'System.Func<Order, Order, int>' to 'System.Collections.Generic.IComparer<Order>?'Der Compiler probiert die Variante von Sort mit IComparer, deshalb steht sie in der Meldung.
- Sortieren, das sich wie
comparingundthenComparingin Java liest, hat C# in LINQ:OrderByDescending(s => s.Grade).ThenBy(s => s.Name). Das kommt in der nächsten Lektion.
Probier es aus
Was kehrt das Vertauschen von a und b um?
byGradeThenName nach Note, dann nach Name, und Sort bekommt ein Lambda, das es mit vertauschten a und b aufruft.Was gibt das Programm aus?
var students = new List<Student>{ new Student("Jonas", 5.50), new Student("Anna", 6.00), new Student("Peter", 5.50), new Student("Finn", 4.75)};
Comparison<Student> byGradeThenName = (a, b) =>{ int byGrade = a.Grade.CompareTo(b.Grade); return byGrade != 0 ? byGrade : string.Compare(a.Name, b.Name);};
students.Sort((a, b) => byGradeThenName(b, a));students.ForEach(s => Console.WriteLine($"{s.Name} {s.Grade}"));a und b kehrt den ganzen Vergleich um, und der ist „nach Note, dann nach Name“. Also werden beide umgekehrt: Die Noten gehen von der höchsten an, und Peter kommt vor Jonas. Willst du nur die Noten umgekehrt, vertauschst du a und b nur im Vergleich der Noten, wie auf der Karte davor.Probier es aus
Finde den Fehler
1. 12.00 € und so weiter. Es bricht schon beim ersten Preis mit einem Fehler ab. In welcher Zeile ist der Fehler?Wähle die Zeile mit dem Fehler.
Das Lambda liest i, aber nicht in dem Moment, in dem es geschrieben wird:
ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index')Das Lambda sieht die Variable i selbst, nicht eine Kopie ihres Werts. So ein Lambda heißt Closure: Es „schließt“ die Variablen von außen ein, die es benutzt. Die Schleife for hat ein einziges i für alle Durchläufe. Die Lambdas werden erst nach der Schleife aufgerufen, und dann ist i schon 3. prices[3] gibt es nicht, und das Programm bricht ab.
Die Lösung ist eine Kopie im Schleifenrumpf: int n = i; und im Lambda n statt i. Jeder Durchlauf hat sein eigenes n, also merkt sich jedes Lambda seines:
var prices = new List<double> { 12, 75, 40 };var lines = new List<Action>();for (int i = 0; i < prices.Count; i++){ int n = i; lines.Add(() => Console.WriteLine($"{n + 1}. {prices[n]:F2} €"));}lines.ForEach(line => line());1. 12.00 €2. 75.00 €3. 40.00 €foreach hat dieses Problem nicht: Dort hat jeder Durchlauf seine eigene Variable.
Genauer, für Neugierige
Hier unterscheidet sich C# von Java. Dort bekommt ein Lambda eine Kopie des Werts und kann die lokale Variable gar nicht ändern. In C# kann das Lambda sie auch ändern: Die Summe aus Java lässt sich hier kompilieren und gibt 127 aus:
var prices = new List<double> { 12, 75, 40 };double total = 0;prices.ForEach(p => total += p);Console.WriteLine(total);127Und weil es die Variable selbst sieht, sieht das Lambda auch eine Änderung, die nach ihm kommt:
double rate = 0.2;Func<double, double> discount = price => price * (1 - rate);Console.WriteLine(discount(100));
rate = 0.5;Console.WriteLine(discount(100));8050Muss sich die Regel einen Wert merken, übergib ihn als Parameter. Für eine Summe gibt es aber einen klareren Weg, Sum, in der nächsten Lektion.
Übung
Regeln als Werte
1. Sortieren nach mehreren Feldern. Eine Klasse Product mit Name, Kategorie und Preis. Sortiere die Produkte nach Kategorie, innerhalb einer Kategorie vom teuersten an und bei gleichem Preis nach Name, mit einem Sort und einem Lambda. Die Produkte sind: Maus, Zubehör, 25 €; Laptop, Computer, 1299 €; Tastatur, Zubehör, 49,90 €; Tablet, Computer, 399 €; Kopfhörer, Zubehör, 49,90 €.
Computer | Laptop | 1299.00 €Computer | Tablet | 399.00 €Zubehör | Kopfhörer | 49.90 €Zubehör | Tastatur | 49.90 €Zubehör | Maus | 25.00 €2. Bestellungen filtern. Gib Order eine Stadt. Schreib eine Methode Select(List<Order> orders, Predicate<Order> rule), die eine neue Liste nur mit den Bestellungen zurückgibt, für die die Regel wahr ist. Mach zwei Regeln, hamburg und big (mindestens 50 €), und gib die Bestellungen aus, für die beide wahr sind, die außerhalb von Hamburg und die „in Hamburg oder mindestens 100 €“. Jede der drei Regeln ist ein Lambda, das hamburg und big aufruft. Die Bestellungen: Maria, Hamburg, 75,50 €; Tom, Dresden, 12,90 €; Laura, Hamburg, 40 €; Lukas, Berlin, 120 €.
Hamburg, ab 50 €: Maria, Hamburg: 75.50 €Außerhalb von Hamburg: Tom, Dresden: 12.90 € Lukas, Berlin: 120.00 €Kostenlose Lieferung (Hamburg oder ab 100 €): Maria, Hamburg: 75.50 € Laura, Hamburg: 40.00 € Lukas, Berlin: 120.00 €Func<double, double>: einen Studentenrabatt (−10 %), einen Black-Friday-Rabatt (−20 €) und „nicht unter null“. Verkette sie mit der Methode Then(f, g) aus der Lektion in zwei Reihenfolgen: zuerst den Studentenrabatt und zuerst den Freitagsrabatt. Probier es mit 120 € und mit 15 €. Spielt die Reihenfolge eine Rolle?120.00 € -> Student, dann Freitag: 88.00 €; Freitag, dann Student: 90.00 €15.00 € -> Student, dann Freitag: 0.00 €; Freitag, dann Student: 0.00 €Weitere Aufgaben
- Eine Liste aufräumen. Entferne aus einer Liste von E-Mail-Adressen die leeren und die ohne
@mit einem einzigenRemoveAllund gib den Rest mitForEach(Console.WriteLine)aus. - Schüler nach Klasse. Eine Liste von Schülern und ihren Klassen (
"10A","10B"). Gib für jede Klasse die Namen aus, mitFindAllund einem Lambda, das die Klasse vergleicht, undConvertAll, das den Namen nimmt. - Dein eigenes Delegate. Mach
delegate bool Validator(string value);und drei Validatoren als Lambdas: nicht leerer Text, nur Ziffern (char.IsDigitfür jedes Zeichen), Länge bis 10. Probier sie an ein paar Strings aus.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Exceptions · Listen · Dictionary und HashSet · Klassen in Klassen
- Geht weiter in: LINQ · record, switch und Muster · Entwurfsmuster · Tests mit xUnit
- Aufgaben zum Thema: Modernes C#