Während Lambda Expressions den meisten Entwicklern vertraut sein sollten, sind Expression Trees eher unbekannt. Expression Trees bieten die Möglichkeit, ausführbaren Code in hierarchischen Datenstrukturen abzubilden. Jeder Knoten wird dabei durch eine Lambda Expression dargestellt. Dieser kann zur Laufzeit geändert werden, wodurch sich interessante Möglichkeiten ergeben.
Lambda Expressions sind die Grundlage von Expression Trees. Ein sicherer Umgang mit Lambda Expressions ist Grundvoraussetzung für das Verständnis von Epression Trees. Deshalb folgt im 1. Teil eine kurze, aber (hoffentlich) vollständige Erläuterung zu Lambda Expressions. Dieser Post ist quasi auch die Essenz aller Beiträge, die ich zu diesem Thema bisher gelesen habe.
Grundsätzlich unterscheidet man zwei Arten von Lambda Expressions. Die Statement Lambdas (Anweisungs Lambdas) und die Expression Lambdas (Ausdrucks Lambdas).
Statement Lambdas (Anweisungs Lambdas)
Statement Lambdas kommen immer dort zum Einsatz, wo auch Delegates verwendet werden. Sie sind vergleichbar mit anonymen Methoden, besitzen aber eine kürzere Schreibweise. Hier ein einfaches Beispiel, bei der folgender Delegate vorausgesetzt wird:
public delegate void PrintString(int x, string y);
Die Umsetzung als anonyme Methode sieht wie folgt aus:
PrintString myDelegate = delegate(int x, string y) { string s = "Message(" + x + "): " + y; Console.WriteLine(s); }; myDelegate(100, "Exception");
Das gleiche Beispiel als Statement Lambda:
PrintString myDelegate = (int x, string y) => { string s = "Message(" + x + "): " + y; Console.WriteLine(s); }; myDelegate(100, "Exception");
Das Schlüsselwort delegate entfällt. Stattdessen wird hinter den Eingangsparametern der Operator => angegeben (gesprochen: ‘geht nach’ bzw. ‘goes to’). Danach folgt der Anweisungsblock, in der die eigentliche Logik implementiert wird.
Der Operator => ist rechtsassoziativ und hat die gleiche Rangfolge wie die Zuweisung (=). Dank Type Inference (Typrückschluss) können die Typangaben bei den Eingangsparametern entfallen:
PrintString myDelegate = (x, y) => { string s = "Message(" + x + "): " + y; Console.WriteLine(s); }; myDelegate(100, "Exception");
Wird nur ein Parameter übergeben, so kann auf die runde Klammer um die Eingangsparameter verzichtet werden:
public delegate void PrintString(int x); PrintString myDelegate = x => { string s = "Message(" + x + ")"; Console.WriteLine(s); };
Bei einer leeren Parameterliste werden jedoch die runden Klammern wieder angegeben:
public delegate void PrintString(); PrintString myDelegate = () => { string s = "Message"; Console.WriteLine(s); };
Die Schreibweise der Statement Lambdas ist etwas kompakter als die der anonymen Methoden.
Expression Lambdas (Ausdrucks Lambdas)
Ein Expression Lambda ist eine besondere Form der Statement Lambdas. Ein Expression Lambda enthält nur eine Anweisung und liefert immer einen Wert zurück.
public delegate bool IsStringTooLong(int x, string y); IsStringTooLong myDelegate = (x, y) => { return y.Length > x; }; bool ret = myDelegate(10, "Message");
Die gleiche Funktionalität kann durch ein Expression Lambda deutlich einfacher dargestellt werden:
IsStringTooLong myDelegate = (x, y) => y.Length > x; bool ret = myDelegate(10, "Message");
Es besteht auch die Möglichkeit statt der Anweisung einen Methodenaufruf anzugeben:
IsStringTooLong myDelegate = (x, y) => SomeFunction(x, y); bool ret = myDelegate(10, "Message"); static public bool SomeFunction(int x, string y) { return y.Length > x; }
Expression Lambdas können auch dann angegeben werden, wenn bei einer Methode als Parametertyp Expression<Func> erwartet wird. Ein gutes Beispiel ist das Interface IQueryable.
List<string> list = new List<string>() { "Johann", "Otto", "Karl" }; IQueryable query = list.AsQueryable(); string a = list.First(x => { return x.EndsWith("o"); }); string b = query.First(x => { return x.EndsWith("o"); }); // Fehler!!!
Die Methode First() der Klasse List<string> erwartet als Parameter ein Delegate vom Typ Func<string, bool>. Das Interface IQueryable hingegen, erwartet bei der Methode First() den Datentyp Expression<Func<string, bool>>. Aus diesem Grund kann das obige Beispiel nicht compiliert werden.
Wird als Parameter ein Expression Lambda angegeben (also ohne Klammern und ohne return), so lässt sich das Programm übersetzen.
string c = query.First(x => x.EndsWith("s") );
Lambda Expressions lassen sich in zwei Arten aufteilen. Zum einen die Lambda Statements, die Delegates und anonyme Methoden erzeugen und zum anderen die Expression Lambdas, die Instanzen von Expression<> zurückliefern. Expression Lambdas und die Klasse System.Linq.Expressions.Expression finden Anwendung bei den Expression Trees. Dazu später mehr.
Prädikat
Liefert ein Lambda Expression einen boolschen Wert zurück, so spricht man auch von einem Prädikat. Die Variable temperatur ist bei dem folgenden Beispiel vom Typ Integer.
(temperatur) => temperatur > 21;
Projektion
Unterscheidet sich der Datentyp des Rückgabewerts von dem Datentyp des Parameters, so wird dieses als Projektion bezeichnet. Bei dem folgenden Beispiel ist ex vom Datentyp Exception.
(ex) => ex.Message;
Delegates Action<>(), Func<>() und Predicate<>()
In den bisherigen Beispielen wurde immer ein eigener Delegate definiert. Das .NET Framework definiert einige hilfreiche Delegates. Hier die drei wichtigsten Varianten, die ich immer wieder gerne einsetze:
delegate void Action(); delegate void Action<T1>(T1 parameter1); delegate void Action<T1, T2>(T1 parameter1, T2 parameter2); delegate void Action<T1, T2, T3>(T1 parameter1, T2 parameter2, T3 parameter3); ... delegate TResult Func<TResult>(); delegate TResult Func<T1, TResult>(T1 parameter1); delegate TResult Func<T1, T2, TResult>(T1 parameter1, T2 parameter2); delegate TResult Func<T1, T2, T3, TResult>(T1 parameter1, T2 parameter2, T3 parameter3); ... delegate bool Predicate<T1>(T1 parameter1);
Der entscheidende Unterschied bei diesen drei Varianten ist der Rückgabewert. Action<>() gibt immer void zurück, Predicate<>() immer bool und der Rückgabewert bei Func<>() kann frei angegeben werden. Für die Delegates Action<>() und Func<>() gibt es jeweils Überladungen für bis zu 16 Parametern.
Beispiel für eigene Implementierung
Das folgende Beispiel fügt der Klasse List die Erweiterungsmethode MyWhere<T> hinzu. Die neue Methode soll sich in etwa so verhalten, wie die sicher schon bekannte Methode Where<T>.
class Program { static void Main(string[] args) { new Program().Run(); } void Run() { List<int> list = new List<int>() { 3, 1, 9, 4, 8, 1, 0, 6, 9 }; IEnumerable<int> myFilteredList = list.MyWhere<int>(x => x < 5); foreach (var x in myFilteredList) Console.WriteLine(x); } } public static class ExMethods { public static List<T> MyWhere<T>(this List<T> source, Predicate<T> predicate) { List<T> list = new List<T>(); foreach (T item in source) if (predicate(item)) list.Add(item); return list; } }
In der Erweiterungsmethode wird für jedes Element der Delegate predicate aufgerufen. Über diesen Delegate wird der Teil der Lambda Expression ausgeführt, die sich rechts vom Lambda Operator befinden. In diesem Beispiel: x < 5. Ist die Bedingung erfüllt, so liefert der Delegate True zurück. Dadurch wird in der Erweiterungsmethode das entsprechende Element in eine neue Liste eingefügt, die am Ende der Methode zurückgegeben wird.
Insbesondere LINQ macht von Erweiterungsmethoden und Lambda Expressions reichlich Gebrauch. Die Lesbarkeit der einzelnen Abfragen wird dadurch deutlich erhöht.
Type Inference (Typrückschluss)
Bei der Angabe einer Lambda Expression können oftmals die Typangaben bei den Eingangsparametern und bei den Rückgabeparametern (falls vorhanden) entfallen. Der Compiler ermittelt an Hand der Signatur des Delegate die notwendigen Informationen. Dabei sind einige Punkte zu beachten:
Alle Typangaben bei den Eingangsparametern müssen entweder komplett implizit oder explizit angegeben werden:
PrintString myDelegate = (int x, string y) => { ... }; // ok PrintString myDelegate = (x, y) => { ... }; // ok PrintString myDelegate = (x, string y) => { ... }; // Fehler
– Wird bei den Eingangsparametern kein Datentyp angegeben, so muss dieser implizit in den entsprechenden Datentyp konvertierbar sein.
– Der Rückgabewert (falls vorhanden) muss ebenfalls implizit in den Rückgabewert des Delegates konvertierbar sein.
– Anzahl und Reihenfolge der Eingangsparameter muss mit der Deklaration des Delegates übereinstimmen. Ausnahmen sind optionale und benannte Parameter.
Optionale und benannte Parameter
– Bei dem Aufruf des Delegate können die Eingangsparameter per Name angegeben werden. Dadurch ist die Reihenfolge frei wählbar:
myDelegate(x: 400, y: "Exception"); myDelegate(y: "Exception", x: 500);
– Optionale Parameter sind ebenfalls nutzbar. Die Angabe des Standardwerts erfolgt bei der Deklaration des Delegate:
public delegate void PrintString(int x = 0, string y = "default"); PrintString myDelegate = (x, y) => { ... }; myDelegate(); myDelegate(200, "Exception"); myDelegate(300);
Weitere Infos zu optionalen und benannten Parametern sind im folgenden Post von mir zu finden: Benannte und optionale Parameter
Closures
Eine gern benutzte Beschreibung von Closure ist:
“Eine Closure verwendet beim Aufruf einen Teil ihres Gültigkeitsbereichs, auch wenn dieser Gültigkeitsbereich außerhalb der Funktion schon nicht mehr existiert.”
Alles klar? Ich glaube ein einfaches Beispiel ist hilfreicher:
static void Main(string[] args) { var calculate = GetCalculation(); var result = calculate(13); } public static Func<int, int> GetCalculation() { var value = 99; return x => { value++; return x * value; }; }
Über die Methode GetCalculation() wird ein Delegate in Form eines Lambda Statements zurückgegeben. Die Variable value wird als lokale Variable von GetCalculation() mit in den Delegate gegeben. Anschließend wird die Methode verlassen. Man sollte jetzt annehmen, dass die Variable value nicht mehr gültig ist und ihren Wert verliert.
Beim Aufruf der anonymen Methode wird zuerst der Inhalt von value um eins erhöht und dann mit x, in diesem Fall13, multipliziert. Als Ergebnis wird 1300 zurückgegeben. Wie kann das sein?
Beim Verlassen der Methode GetCalculation() wird der Speicherplatz von value nicht wie üblich abgeräumt, weil ja in der anonymen Methode auf ihn verwiesen wird. Der Kontext, in dem value erzeugt wurde, existiert nicht mehr, trotzdem ist die Variable nach wie vor erhalten und steht auch ohne ihren Kontext zur Verfügung. Die Variable value ist an die anonyme Methode gebunden.
Im 2. Teil geht es dann um die Grundlagen von Expression Trees.
Eine supergeniale Zusammenfassung. Ich bin sehr, sehr gespannt auf den zweiten Teil!
Tolle Zusammenfassung. Die Trennung von Lamda Statements und Expressions wird in der Literatur oftmals nicht klar dargestellt. Freue mich auf den 2. Teil. Wann wird dieser erscheinen?
Gruß,
Thomas Sczyrba
Wirklich sehr gut, bin gespannt auf Teil 2 – vielen Dank!!!
“Lambda Expressions und Expression Trees – Teil 1 Stefan Henneken” truly enables me personally contemplate
a small bit more. I appreciated each and every particular element of this blog post.
Many thanks -Catherine