Anonyme Methoden, Lambda-Ausdrücke und Ausdrucksbäume in

Werbung
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
Herunterladen