Jürgen Bayer Anonyme Methoden, Lambda-Ausdrücke und Ausdrucksbäume in .NET Inhaltsverzeichnis 1 Einleitung 1 2 Anonyme Methoden 2 3 Lambda-Ausdrücke 4 4 Ausdrucksbäume 6 4.1 Was sind Ausdrucksbäume? 6 4.2 Der Sinn von Ausdrucksbäumen 7 4.3 Ausdrucksbäume in .NET 8 4.4 Erstellung eines Ausdrucksbaums 8 1 Einleitung Das .NET-Framework enthält mit anonymen Methoden, Lambda-Ausdrücken und Ausdrucksbäumen wichtige Features der funktionalen Programmierung. Um dieses interessante Konzept (das ich hier nicht weiter ausführe) zu verstehen oder auch umsetzen zu können, und um mit modernen .NET-Features wie LINQ arbeiten zu können, sollten Sie sich mit diesen Themen auskennen. Da ich denke, dass das Verständnis besonders von Ausdrucksbäumen sehr wichtig ist, behandle ich diese zwar eher theoretisch, aber so umfangreich, dass Sie später wissen, wie z. B. LINQ to SQL einen Lambda-Ausdruck in eine SQL-Anweisung umsetzt. Einleitung 1 2 Anonyme Methoden Anonyme Methoden sind Methoden, die nur über einen Delegaten referenziert werden. Sie besitzen keinen Namen und können deswegen auch nicht über diesen aufgerufen werden. Eine Anonyme Methode kann (auf die alte Art) über das delagate-Schlüsselwort erzeugt werden. Die Syntax dazu ist: delegate([Argumentliste]) { Anweisungsblock } Das Ganze wird einfacher in einem Beispiel. Dieses Beispiel demonstriert auch gleich den wesentlichen Aspekt der funktionalen Programmierung. Lassen Sie es auf sich wirken ☺. Angenommen, eine Methode erwartet ein Array aus Integerwerten und einen Delegaten auf eine Methode, die für alle Elemente des Arrays aufgerufen wird: private static void TestValuesForCondition(int[] values, Func<int, bool> testMethod) { foreach (var value in values) { if (testMethod(value) == true) { Console.WriteLine("Der Wert " + value + " wurde bestätigt"); } } } Listing 2.1: Methode, die Integer-Werte auf eine Bedingung testet, die in Form eines Delegaten übergeben wird Der Delegat für die Testmethode entspricht in diesem Beispiel dem generischen Delegaten System.Func mit einem int-Argument und bool als Rückgabetyp. In der Anwendung könnten Sie nun die Testmethode mit einem Integer-Array und einer anonymen Methode aufrufen, die die Werte daraufhin überprüft, ob diese gerade sind: // Erzeugen des Beispiel-Arrays int[] testValues = new int[10]; Random random = new Random(); for (int i = 0; i < 10; i++) { testValues[i] = random.Next(1, 100); } // Aufruf der Testmethode mit einer anonymen Methode, // die die Werte daraufhin überprüft, ob diese // gerade sind Console.WriteLine("Gerade Zahlen: "); TestValuesForCondition(testValues, delegate(int value) { return value % 2 == 0; }); Console.WriteLine(); Listing 2.2: Verwendung einer anonymen Methode als Argument einer Methode Was Sie jetzt einsetzen, ist funktionale Programmierung. In diesem Beispiel wird die Bedingung, die dazu führt, dass ein Integer-Wert bestätigt wird, über eine Funktion (eigentlich ja Methode …) definiert, die beim Aufruf der Testmethode übergeben wird. Die Funktion bestimmt die Regeln. Das macht dieses Konzept sehr mächtig. Die übergebene (oder sonst wie verwendete) Funktion kann schließlich unendlich vielfältig programmiert werden. So ist z. B. auch eine Prüfung auf ungerade oder Primzahlen auf eine ziemlich direkte Weise möglich ohne die Testmethode umschreiben zu müssen: Anonyme Methoden 2 // Aufruf der Testmethode mit einer anonymen Methode, // die die Werte daraufhin überprüft, ob diese // ungerade sind Console.WriteLine("Ungerade Zahlen: "); TestValuesForCondition(testValues, delegate(int value) { return value % 2 != 0; }); Console.WriteLine(); // Aufruf der Testmethode mit einer anonymen Methode, // die die Werte daraufhin überprüft, ob Primzahlen sind Console.WriteLine("Primzahlen: "); TestValuesForCondition(testValues, delegate(int value) { if (value < 3) { return false; } for (int divisor = 2; divisor < value; divisor++) { if (value % divisor == 0) { return false; } } return true; }); Listing 2.3: Weiterer Einsatz anonymer Methoden in der funktionalen Programmierung Statt der anonymen Methoden könnten Sie natürlich auch »richtige« Methoden übergeben. Diese müssten dann aber deklariert werden, was recht aufwändig wäre. Eine anonyme Methode ist, besonders in Form eines Lambda-Ausdrucks (der im nächsten Abschnitt behandelt wird) wesentlich einfacher zu erzeugen. Einfache anonyme Methoden besitzen zudem den Vorteil, dass an der Stelle des Aufrufs sehr schnell erkannt werden kann, was dort passiert. Ich würde anonyme Methoden auch nur dann einsetzen, wenn diese relativ einfach oder – bei komplexen Methoden – wenn die Programmlogik nicht an mehreren Stellen verwendet wird. Die im Beispiel verwendete anonyme Methode zur Ermittlung, ob eine Zahl eine Primzahl ist, ist ein Grenzfall, da der Programmcode auf jeden Fall noch verbessert werden könnte. Würde diese Methode mehrfach benötigt, würde ich dazu eher eine echte Methode schreiben, die dann am testMethod-Argument der Testmethode (in Form einer Instanz von Func<int, bool>) übergeben wird. Anonyme Methoden 3 3 Lambda-Ausdrücke Lambda-Ausdrücke sind ein neues Feature von C# 3.0 bzw. des .NET-Framework 3.5. LambdaAusdrücke werden sehr intensiv mit LINQ eingesetzt, können aber (wie in dem Beispiel dieses Abschnitts) auch für eigene (funktionale) Programmierung verwendet werden. Ein Lambda-Ausdruck ist ein Ausdruck, der zum einen in einen in einen sogenannten Ausdrucksbaum konvertiert werden kann. Ausdrucksbäume werden in Kapitel 4 behandelt. Lambda-Ausdrücke können zum anderen aber auch in einen Delegaten konvertiert werden. Dieser Delegat referenziert eine anonyme Methode, die implizit über den Lambda-Ausdruck erzeugt wird, sobald dieser als Delegat verwendet wird. C# 3.0 bietet eine spezielle Syntax zur Erzeugung eines Lambda-Ausdrucks. Dazu werden eventuelle Argumente der Methode vor dem Operator => angegeben. Rechts davon stehen dann die Anweisungen, die die Methode ausmachen: [Argumentliste] => Ausdruck | Anweisungsblock In der Argumentliste geben Sie alle Argumente an, die in der Methode verwendet werden. Besitzt die Methode keine Argumente, wird natürlich auch keine Argumentliste angegeben. Ist keines oder mehr als ein Argument vorhanden, muss die Argumentliste in Klammern angegeben werden. Ist nur ein Argument vorhanden, können Sie die Klammern weglassen. Sie können die Argumente ohne Typ angeben, dann entspricht der Typ des Arguments dem des entsprechenden Arguments des Delegaten, dem Sie den Lambda-Ausdruck zuweisen. In den wenigen Fällen, in denen Sie einen anderen Typ angeben müssen, können Sie den Typ wie bei Variablen vor das Argument schreiben. Rechts vom Lambda-Operator steht dann entweder ein Ausdruck oder ein Anweisungsblock. Ausdrücke werden direkt angegeben. Die Rückgabe des Lambda-Ausdrucks entspricht dann dem Ergebnis des Ausdrucks. Listing 3.1 zeigt einige Beispiele für Lambda-Ausdrücke, die mit Ausdrücken arbeiten. Um das Ganze kompilieren zu können, weist das Beispiel die erzeugten Lambda-Ausdrücke Variablen zu, deren Typ ein Delegat ist, der dem Lambda-Ausdruck entspricht. // Lambda-Ausdruck mit Ausdruck ohne Parameter Func<int> f1 = () => 1 + 1; // Lambda-Ausdruck mit Ausdruck mit einem implizit typisierten Parameter Func<int, int> f2 = x => x + 1; // Lambda-Ausdruck mit Ausdruck und zwei explizit typisierten Parametern Func<int, int, int> f3 = (int x, int y) => x + y; Listing 3.1: Beispiele für Lambda-Ausdrücke, die mit Ausdrücken arbeiten Lambda-Ausdrücke 4 Werden Anweisungsblöcke verwendet, müssen diese immer in geschweifte Klammern eingetragen werden, auch wenn es sich nur um eine Anweisung handelt. Soll (bzw. muss) der Lambda-Ausdruck einen Wert zurückgeben, muss dieser wie bei einer normalen Methode über return zurückgegeben werden. Func<int, int> f4 = (x) => { if (x < 10) { return 1; } else if (x < 100) { return 2; } else { return 3; } }; Listing 3.2: Lambda-Ausdruck mit Anweisungsblock Mit diesem Wissen können wir die Testmethoden aus dem Beispiel des vorhergehenden Abschnitts einfacher darstellen als mit anonymen Methoden: // Aufruf der Testmethode mit einem Lambda-Ausdruck, // der die Werte daraufhin überprüft, ob diese gerade sind Console.WriteLine("Gerade Zahlen: "); TestValuesForCondition(testValues, value => value % 2 == 0); Console.WriteLine(); // Aufruf der Testmethode mit einem Lambda-Ausdruck, // der die Werte daraufhin überprüft, ob diese ungerade sind Console.WriteLine("Ungerade Zahlen: "); TestValuesForCondition(testValues, value => value % 2 != 0 ); Console.WriteLine(); // Aufruf der Testmethode mit einem Lambda-Ausdruck, // der die Werte daraufhin überprüft, ob Primzahlen sind Console.WriteLine("Primzahlen: "); TestValuesForCondition(testValues, value => { if (value < 3) { return false; } for (int divisor = 2; divisor < value; divisor++) { if (value % divisor == 0) { return false; } } return true; }); Console.WriteLine(); Listing 3.3: Funktionale Programmierung mit Lambda-Ausdrücken Beachten Sie, dass die Lambda-Ausdrücke in den Beispielen einem Delegat-Argument zugewiesen werden. Deswegen werden die Lambda-Ausdrücke implizit in eine anonyme Methode konvertiert, die über den Delgat aufgerufen werden kann. Besonders die einfachen Ausdrücke zeigen, dass Lambda-Ausdrücke wesentlich einfacher zu implementieren sind als anonyme Methoden. Und wenn Sie sich einmal daran gewöhnt haben, sind diese auch einfacher zu lesen. Bald. Versprochen ☺. Lambda-Ausdrücke 5 4 Ausdrucksbäume Ausdrucksbäume (Expression Trees) sind ein neues Feature im .NET-Framework 3.5, allerdings keine neues Feature in der allgemeinen Informatik. Das Prinzip von Ausdrucksbäumen ist in der Informatik (bzw. in der Mathematik) schon seit längerem bekannt. 4.1 Was sind Ausdrucksbäume? Über Ausdrucksbäume können Ausdrücke dargestellt bzw. gespeichert werden. Der folgende Ausdruck zur Berechnung eines Bruttowerts: net * (1 + (vat / 100)) kann z. B. wie in Abbildung 4.1 in einem Ausdrucksbaum dargestellt werden. Abbildung 4.1: Ein Ausdrucksbaum zur Berechnung eines Bruttobetrags Der Baum im Beispiel besteht aus drei Berechnungs-Ausdrücken (Multiplikation, Addition und Division), zwei konstanten Ausdrücken für die in der Berechnung verwendeten Konstanten 1 und 100 und zwei Parameterausdrücken für die Parameter net und vat. Bei der späteren Berechnung werden die Parameterausdrücke mit Werten versehen. Jeder Baum beginnt bei einem Wurzelausdruck. Im Beispiel ist das der Multiplikationsausdruck. Die Berechnungs-Ausdrücke im Beispiel besitzen einen linken und einen rechten Operanden. Der linke Operand des Multiplikationsausdrucks ist der Parameterausdruck für den Parameter net, der rechte der Additionssausdruck. Der linke Operand des Additionsausdrucks ist der Konstantenausdruck für den Wert 1, der rechte der Divisionsausdruck. Etc. Ein Ausdrucksbaums wird ausgehend vom Wurzelknoten ausgewertet. Dabei werden zuerst die Knoten an den Enden ausgerechnet, dann die darüberliegenden etc. Im Beispiel ergäbe das die folgenden (mathematisch korrekten) Einzelschritte: • Zwischenergebnis1 = vat / 100 Ausdrucksbäume 6 • Zwischenergebnis2 = 1 + Zwischenergebnis1 • Endergebnis = net * Zwischenergebnis2 Die Parameter müssen für die Berechnung natürlich mit Werten versehen werden. 4.2 Der Sinn von Ausdrucksbäumen Mit Ausdrucksbäumen können beliebige Ausdrücke in einem Programm gespeichert und ausgewertet werden. Das ist noch nichts Besonderes, weil Sie Ausdrücke ja auch direkt in Programmen verwenden können. Ausdrucksbäume erlauben aber zum einen das dynamische Zusammensetzen eines Ausdrucks und zum anderen spezielle Auswertungen eines Ausdrucks. Das dynamische Zusammensetzen eines Ausdrucks wird z. B. in Ausdrucksparsern benötigt, die eine Benutzereingabe in einen auswertbaren Ausdruck umwandeln. Stellen Sie sich vor, Sie müssten eine einfache Anwendung schreiben, in die der Anwender einen mathematischen Ausdruck eingeben kann, der danach ausgewertet werden soll. Wenn Sie die Eingabe parsen, müssen Sie die einzelnen Teile des Gesamtausdrucks so speichern, dass Sie diese später auswerten können. Ein Ausdrucksbaum ist dafür ideal geeignet. Sie bräuchten lediglich Klassen, die die unterstützten atomaren Ausdrücke (Addition, Subtraktion, Multiplikation etc.) abbilden und die Referenzen auf ihre Operanden(ausdrücke) erlauben. Das .NET-Framework stellt dazu im Namensraum System.Linq.Expressions entsprechende Klassen zur Verfügung, gemeinsam mit der Möglichkeit, einen Ausdruck dynamisch auszuführen. Sie müssten »nur« noch den Parser schreiben (was allerdings nicht allzu einfach ist). Neben dem dynamischen Zusammensetzen und dem Ausführen eines Ausdrucks erlauben Ausdrucksbäume auch eine dynamische Auswertung. Dieses Feature wird z. B. in LINQ to SQL genutzt: LINQ erlaubt zunächst die Verwendung der Where-Methode auf allen Auflistungen, die IEnumerable implementieren. Dieser Methode wird eine Instanz des Delegate Func<TSource, bool> oder Func<TSource, int, bool> übergeben, wobei TSource der Typ der in der Auflistung verwalteten Objekte und bool der Rückgabetyp ist. Die übergebene Methode überprüft eine Bedingung über die Auswertung des übergebenen Objekts und gibt true zurück, wenn die Bedingung erfüllt ist. Dazu wird natürlich zur Vereinfachung ein Lambda-Ausdruck verwendet. Das folgende Beispiel sucht in einer Auflistung von Personen nach denen, die in Dublin wohnen: List<Person> persons = GetPersons(); foreach (Person personFromDublin in persons.Where<Person>( person => person.City == "Dublin")) { Console.WriteLine(personFromDublin.FirstName + " " + personFromDublin.LastName); } LINQ verwendet die übergebene Prüf-Methode zum Test, ob die einzelnen Objekte in der Auflistung der Bedingung entsprechen. Das hat noch nichts mit Ausdrucksbäumen zu tun. Wird aber LINQ to SQL verwendet, sieht das Ganze anders aus. Die Abfrage der Daten erfolgt dann gegen eine Auflistung, die in einem DataContext verwaltet wird. Ich will hier nicht näher auf dieses separate Thema eingehen. Der DataContext kümmert sich im Wesentlichen um die Abfrage und Aktualisierung der Daten. Eine zum vorhergehenden Beispiel äquivalente Abfrage würde in etwa folgendermaßen aussehen: foreach (Person personFromDublin in dataContext.Persons.Where<Person>( person => person.City == "Dublin")) { Console.WriteLine(personFromDublin.FirstName + " " + personFromDublin.LastName); } Würden bei LINQ to SQL allerdings zunächst immer erst alle Objekte aus der Datenquelle abgefragt und dann erst gegen die Prüfmethode geprüft werden, wäre dies sehr ineffizient. Ausdrucksbäume 7 Deswegen geht LINQ to SQL anders vor: Die Where-Methode wertet den Ausdrucksbaum aus, den der übergebene Lambda-Ausdruck ergibt, und setzt diesen in eine entsprechende SQLWHERE-Klausel um. Diese wird dann verwendet, um die Daten gezielt abzufragen. Damit werden nur die Daten abgefragt, die der übergebenen Bedingung entsprechen. Das ist effizient und damit ein guter Grund für den Einsatz von Ausdrucksbäumen. 4.3 Ausdrucksbäume in .NET Das .NET-Framework unterstützt Ausdrucksbäume über Klassen im Namensraum System.Linq.Expressions. Der Namensraum deutet darauf hin, dass Ausdrucksbäume vorwiegend in LINQ verwendet werden, was auch richtig ist. Ausdrucksbäume können allerdings (natürlich) überall dort verwendet werden, wo sie sinnvoll erscheinen. Anwendungsbereiche zu finden ist allerdings nicht allzu einfach wenn Sie in der »normalen« ( objektorientierten) Programmierung zuhause sind, um nicht in der funktionalen. LINQ to SQL nutzt Ausdrucksbäume sehr intensiv, indem die bei der Abfrage von Daten verwendeten Lambda-Ausdrücke in passende SQL-Anweisungen umgesetzt werden. Das Verständnis dieser Technik war der Hauptgrund dafür, Ausdrucksbäume in diesem Artikel zu behandeln. Aber vielleicht finden Sie auch weitere Verwendung dafür. 4.4 Erstellung eines Ausdrucksbaums Ein Lambda-Ausdruck, der einem Delegate D entspricht, entspricht immer einer Instanz der Klasse System.Linq.Expressions.Expression<D>. Ausdruckbäume können also implizit über einen Lambda-Ausdruck erzeugt werden (was ja z. B. bei LINQ to SQL genutzt wird). Die Bruttoberechnung kann z. B. folgendermaßen über einen Lambda-Ausdruck erzeugt werden: Expression<Func<double, double, double>> grossLambdaExpression = (net, vat) => net * (1 + (vat / 100)); Sie können Ausdrucksbäume aber auch explizit über die Klassen des Namensraums System.Linq.Expressions erzeugen. Das erscheint Ihnen vielleicht etwas komplex und u. U. unsinnig. Wie gesagt: Anwendungsbereiche dafür zu finden ist nicht so einfach. Aber ich denke, um die Möglichkeiten einschätzen zu können, sollten wir mit Ausdrucksbäumen umgehen können. Ausdrucksbäume 8 System.Linq.Expressions enthält einige Klassen zur Definition von Ausdrücken (Tabelle 4.1). Expression ist dabei die (abstrakte) Basisklasse aller Ausdrucksklassen. Die spezialisierten Ausdrucksklassen wie z. B. BinaryExpression können nicht direkt instanziert werden, sondern müssen über statische Methoden der Expression-Klasse erzeugt werden. Klasse Bedeutung Expression Expression ist die abstrakte Basisklasse aller speziellen Ausdrucksklassen. Sie stellt die Eigenschaft NodeType zur Verfügung, die den Typ des Knotens definiert, den der jeweilige Ausdruck darstellt. Die Eigenschaft Type gibt den Typ des Ausdrucks an. Expression enthält außerdem eine Menge statischer Methoden zur Erzeugung spezialisierter Ausdruck-Objekte, wie z. B. Add zur Erzeugung eines BinaryExpression-Objekts für eine Addition. Expression<TDelegate> Diese generische Klasse erwartet eine Typangabe in Form eines Delegate. Sie verwaltet den Ausdrucksbaum eines Lambda-Ausdrucks. Ein Lambda-Ausdruck, der dem Delegate TDelegate entspricht, kann in eine Instanz von Expression<TDelegate> konvertiert werden, um den Ausdrucksbaum auswerten zu können, den der LambdaAusdruck ergibt. Expression<TDelegate> ist von LambdaExpression abgeleitet und kann deswegen kompiliert und ausgeführt werden. BinaryExpression Diese Klasse steht für Ausdrücke mit zwei Operanden. Die Operanden werden in den Eigenschaften Left und Right verwaltet und referenzieren Expression-Instanzen, also wieder alle möglichen anderen Ausdrucks-Objekte. BinaryExpression-Instanzen werden über verschiedene statische Methoden der Expression-Klasse, wie z. B. Add, Divide, Modulo, Multiply, Power, Subtract, And und Or erzeugt. ConditionalExpression Diese Klasse ermöglicht die Definition einer Bedingung im Ausdrucksbaum. Die Bedingung wird in der Eigenschaft Test verwaltet. Ergibt die Bedingung true, wird der Baum an dem Ausdruck weiter ausgeführt, der in der Eigenschaft IfTrue referenziert wird, im anderen Fall an dem Ausdruck, den die Eigenschaft IfFalse referenziert. ConditionalExpression-Instanzen erzeugen Sie über die statische Condition-Methode der Expression-Klasse. ConstantExpression Instanzen dieser Klasse verwalten in ihrer Eigenschaft Value konstante Werte, die in einem Ausdruck verwendet werden. LambdaExpression LambdaExpression stellt einen Lambda-Ausdruck dar. Im Wesentlichen handelt es sich dabei um einen Ausdrucksbaum mit einer Auflistung der verwendeten Parameter. Der Ausdrucksbaum wird in der Eigenschaft Body verwaltet, die Parameter in der Eigenschaft Parameters. LambdaExpression-Instanzen erlauben das Kompilieren und das Ausfüllen des Ausdrucksbaums. MethodCallExpression Diese Klasse steht für Ausdrücke, die einen Methodenaufruf darstellen. Die aufzurufende Methode wird in der Eigenschaft Method verwaltet. Die Eigenschaft Object referenziert das Ausdrucksbäume 9 Objekt, auf dem die Methode ausgeführt werden soll. Object ist null wenn es sich um eine statische Methode handelt. Die Eigenschaft Arguments verwaltet die Argumente, die der Methode übergeben werden sollen. MethodCallExpression-Instanzen werden über die statischen Methoden Call, ArrayIndex, oder ArrayIndex der Expression-Klasse erzeugt. ParameterExpression ParameterExpression-Objekte verwalten die in einem Ausdruck verwendeten Parameter. Der Name des Parameters wird in der Eigenschaft Name verwaltet, der Typ in der (von Expression geerbten) Eigenschaft Type. UnaryExpression Diese Klasse repräsentiert einen unären Ausdruck, also einen Ausdruck mit nur einem Operanden. Das kann z. B. ein Vorzeichenwechsel sein. Der Operand wird in der Eigenschaft Operand verwaltet. Tabelle 4.1: Die wichtigen Klassen zur Darstellung von Ausdrucksbäumen Neben den in Tabelle 4.1 angegebenen Klassen enthält der Namensraum System.Linq.Expressions noch die Klassen InvocationExpression, ListInitExpression, MemberExpression, MemberInitExpression, NewArrayExpression, NewExpression und TypeBinaryExpression, die hier nicht weiter beschreiben kann. Kommentar [JB1]: Überprüfen Um die Arbeit mit Ausdrucksbäumen zu demonstrieren, erzeugt das folgende Beispiel ein Ausdrucksbaum für die Brutto-Berechnung vom Anfang dieses Abschnitts. ParameterExpression netParameterExpression = Expression.Parameter(typeof(double), "net"); ParameterExpression vatParameterExpression = Expression.Parameter(typeof(double), "vat"); Expression grossExpression = Expression.Multiply( netParameterExpression, Expression.Add( Expression.Constant(1D), Expression.Divide( vatParameterExpression, Expression.Constant(100D)))); Listing 4.1: Explizite Erzeugung eines Ausdrucksbaums für eine Bruttoberechnung Zu diesem Beispiel ist anzumerken, dass die verwendeten ParameterExpression-Instanzen deswegen separat erzeugt werden, weil diese später, beim Kompilieren und Ausführen des Ausdrucks, noch einmal benötigt werden. Den erzeugten Baum können Sie sich über einen Visual-Studio-Visualisierer anschauen. Halten Sie das Programm dazu an einer Anweisung hinter der Erzeugung des Expression-Objekts an, bewegen Sie den Cursor auf die Variable und wählen Sie den EXPRESSION TREE VISUALIZER. Dieser Visualisierer zeigt den Ausdrucksbaum in seiner grafischen Struktur an. Ausdrucksbäume 10 Kommentar [JB2]: Ist der Expression Tree Visualizer Bestandteil von VS? Ansonsten Installation über die LINQ-Samples beschreiben Abbildung 4.2: Anzeige des Bruttoberechnungs-Ausdrucksbaums über den Expression Tree Visualizer Ausdrucksbäume können Sie nun ausführen oder auswerten. Zum Ausführen eines Ausdrucksbaums muss dieser allerdings in eine LambdaExpression-Instanz eingehüllt werden. Eine solche erlaubt das Kompilieren des Ausdrucks über die Compile-Methode. Compile erzeugt aus dem Ausdruck eine Methode und gibt eine Delegate- Referenz zurück. Über die DynamicInvoke-Methode des Delagate können Sie die Ausdrucks-Methode dann ausführen. Eine LambdaExpression-Instanz verwaltet denen auszuführenden Ausdrucksbaum in der Eigenschaft Body. Da Ausdrucksbäume auch Parameter enthalten können, die an beliebigen Stellen im Baum angelegt sein können, müssen diese auch übergeben werden können. Dazu erwartet eine LambdaExpression-Instanz zunächst eine Auflistung der verwendeten ParameterExpression-Objekte in der Eigenschaft Parameters. Diese Auflistung bestimmt lediglich die Reihenfolge der Parameter, in der deren Werte später der DynamicInvokeMethode übergeben werden. Den Ausdrucksbaum und die Parameter übergeben Sie der statischen Lambda-Methode der Expression-Klasse um eine LambdaExpression-Instanz zu erzeugen. Ausdrucksbäume 11 Kommentar [JB3]: Name checken Die an die LambdaExpression-Instanz übergebenen ParameterExpressionInstanzen müssen dieselben sein, die in dem Ausdrucksbaum verwendet werden. Laut einem Posting1 von Anders Hejlsberg, dem Chef-Entwickler von C#, werden Parameterausdrücke nicht über deren Namen identifiziert, sondern über die ObjektReferenz. Der Name hat laut Anders nur informativen Charakter. Wenn Sie stattdessen neue ParameterExpression-Instanzen (mit denselben Namen) übergeben, erhalten Sie beim Kompilieren des Ausdrucks u. U.2 den Fehler »Lambda Parameter not in scope«. Das ist in meinen Augen sehr verwirrend und fehlerträchtig. Aber sei's drum ... Kommentar [JB4]: Überprüfen Der Beispiel-Ausdrucksbaum kann also folgendermaßen ausgeführt werden: ParameterExpression[] parameters = new ParameterExpression[] { netParameterExpression, vatParameterExpression }; LambdaExpression grossLambdaExpression = Expression.Lambda(grossExpression, parameters); double net = 1000; double vat = 19; object result = grossLambdaExpression.Compile().DynamicInvoke(net, vat); Listing 4.2: Ausführen eines LambdaExpression-Objekts Wenn ein Ausdrucksbaum nicht ausgeführt, sondern so ausgewertet werden soll, dass es in eine spezielle Sprache übersetzt wird (z. B. in SQL), könne Sie dies erreichen, indem Sie die einzelnen Knoten des Baums vom Wurzel-Ausdruck aus durchgehen. Jeden Knoten müssen Sie dann auf die von Ihrem Konvertierer unterstützten Expression-Klassen überprüfen, in diese umwandeln und ggf. die weiteren Informationen der jeweiligen Instanz auswerten. Da dieser Vorgang recht komplex ist, zeige ich in Listing 4.3 nur beispielhaft, wie ein Ausdrucksbaum in sein SQL-Äquivalent umgesetzt werden könnte. Das Beispiel ist allerdings unvollständig und müsste für die Praxis noch wesentlich erweitert werden. private static string EvaluateExpressionAsSql(Expression expression) { string result = null; // Überprüfen auf die unterstützten Typen if (expression is BinaryExpression) { BinaryExpression binaryExpression = expression as BinaryExpression; // Ermittlung des Operators string op = null; switch (binaryExpression.NodeType) { case ExpressionType.Add: op = "+"; break; case ExpressionType.And: op = "AND"; break; case ExpressionType.Divide: op = "/"; break; case ExpressionType.Equal: op = "="; break; 1 • Auch wenn der Link nicht für ewig forums.microsoft.com/MSDN/ShowPost.aspx?PostID=1349121&SiteID=1 funktionieren wird: 2 • In meinen Tests trat der Fehler nur dann auf, wenn der Ausdrucksbaum mehr als einen Parameter enthielt Ausdrucksbäume 12 case ExpressionType.GreaterThan: op = ">"; break; case ExpressionType.GreaterThanOrEqual: op = ">="; break; case ExpressionType.LessThan: op = "<"; break; case ExpressionType.LessThanOrEqual: op = "<="; break; case ExpressionType.Modulo: op = "mod"; break; case ExpressionType.Multiply: op = "*"; break; case ExpressionType.Not: op = "NOT"; break; case ExpressionType.NotEqual: op = "<>"; break; case ExpressionType.Or: op = "OR"; break; case ExpressionType.Power: break; case ExpressionType.Subtract: op = "-"; break; default: throw new NotSupportedException( "Nicht unterstützer Operator '" + expression.NodeType + "'"); } // Rekursiver Aufruf mit dem linken und dem rechten Operanden result = "(" + EvaluateExpressionAsSql(binaryExpression.Left) + ") " + op + " (" + EvaluateExpressionAsSql(binaryExpression.Right) + ")"; } else if (expression is ConstantExpression) { // Auslesen des Konstantenwerts ConstantExpression constantExpression = expression as ConstantExpression; if (constantExpression.Type.Name.StartsWith("Int") || constantExpression.Type.Name.StartsWith("UInt") || constantExpression.Type == typeof(float) || constantExpression.Type == typeof(double) || constantExpression.Type == typeof(decimal)) { result = String.Format(CultureInfo.CreateSpecificCulture("en"), "{0}", constantExpression.Value); } else if (constantExpression.Type == typeof(string)) { result = "'" + constantExpression.Value.ToString() + "'"; } else { throw new NotSupportedException("Der Konstantentyp '" + constantExpression.Type.Name + "' wird nicht unterstützt"); } } else if (expression is ParameterExpression) { // Auslesen des Namens Ausdrucksbäume 13 ParameterExpression parameterExpression = expression as ParameterExpression; result = "@" + parameterExpression.Name; } else { throw new NotSupportedException("Der Ausdruckstyp '" + expression.GetType().Name + "' wird nicht unterstützt"); } return result; } Listing 4.3: Beispiel für die Auswertung eines Ausdrucksbaums (als SQL-String) Ausdrucksbäume 14