Ausdrucksbäume für Dummies? [geschlossen]


83

Ich bin der Dummy in diesem Szenario.

Ich habe versucht, bei Google zu lesen, was das ist, aber ich verstehe es einfach nicht. Kann mir jemand eine einfache Erklärung geben, was sie sind und warum sie nützlich sind?

Bearbeiten: Ich spreche über die LINQ-Funktion in .Net.


1
Ich weiß, dass dieser Beitrag ziemlich alt ist, aber ich habe mich in letzter Zeit mit Expression Trees befasst. Ich wurde interessiert, nachdem ich anfing, Fluent NHibernate zu verwenden. James Gregory verwendet ausgiebig die sogenannte statische Reflexion und hat ein Intro: jagregory.com/writings/introduction-to-static-reflection Um statische Reflexions- und Ausdrucksbäume in Aktion zu sehen, lesen Sie den Quellcode von Fluent NHibernate ( fluentnhibernate.org) ). Es ist sehr sauber und ein sehr cooles Konzept.
Jim Schubert

Antworten:


89

Die beste Erklärung für Ausdrucksbäume, die ich jemals gelesen habe, ist dieser Artikel von Charlie Calvert.

Etwas zusammenfassen;

Ein Ausdrucksbaum repräsentiert, was Sie tun möchten, nicht wie Sie es tun möchten.

Betrachten Sie den folgenden sehr einfachen Lambda-Ausdruck:
Func<int, int, int> function = (a, b) => a + b;

Diese Aussage besteht aus drei Abschnitten:

  • Eine Erklärung: Func<int, int, int> function
  • Ein gleichwertiger Operator: =
  • Ein Lambda-Ausdruck: (a, b) => a + b;

Die Variable functionzeigt auf ausführbaren Rohcode, der zwei Zahlen hinzufügen kann .

Dies ist der wichtigste Unterschied zwischen Delegaten und Ausdrücken. Sie rufen function(a Func<int, int, int>) auf, ohne jemals zu wissen, was mit den beiden von Ihnen übergebenen Ganzzahlen geschehen soll. Es dauert zwei und gibt eins zurück, das ist das Beste, was Ihr Code wissen kann.

Im vorherigen Abschnitt haben Sie gesehen, wie Sie eine Variable deklarieren, die auf rohen ausführbaren Code verweist. Ausdrucksbäume sind kein ausführbarer Code , sondern eine Form der Datenstruktur.

Nun, im Gegensatz zu Delegierten, Ihr Code kann wissen , was ein Ausdruck Baum zu tun gemeint ist.

LINQ bietet eine einfache Syntax zum Übersetzen von Code in eine Datenstruktur, die als Ausdrucksbaum bezeichnet wird. Der erste Schritt besteht darin, eine using-Anweisung hinzuzufügen, um den Linq.ExpressionsNamespace einzuführen :

using System.Linq.Expressions;

Jetzt können wir einen Ausdrucksbaum erstellen:
Expression<Func<int, int, int>> expression = (a, b) => a + b;

Der im vorherigen Beispiel gezeigte identische Lambda-Ausdruck wird in einen Ausdrucksbaum konvertiert, der als vom Typ deklariert wurde Expression<T>. Der Bezeichner expression ist kein ausführbarer Code. Es ist eine Datenstruktur, die als Ausdrucksbaum bezeichnet wird.

Das heißt, Sie können nicht einfach einen Ausdrucksbaum aufrufen, wie Sie einen Delegaten aufrufen könnten, sondern Sie können ihn analysieren. Was kann Ihr Code also verstehen, wenn Sie die Variable analysieren expression?

// `expression.NodeType` returns NodeType.Lambda.
// `expression.Type` returns Func<int, int, int>.
// `expression.ReturnType` returns Int32.

var body = expression.Body;
// `body.NodeType` returns ExpressionType.Add.
// `body.Type` returns System.Int32.

var parameters = expression.Parameters;
// `parameters.Count` returns 2.

var firstParam = parameters[0];
// `firstParam.Name` returns "a".
// `firstParam.Type` returns System.Int32.

var secondParam = parameters[1].
// `secondParam.Name` returns "b".
// `secondParam.Type` returns System.Int32.

Hier sehen wir, dass es viele Informationen gibt, die wir aus einem Ausdruck erhalten können.

Aber warum sollten wir das brauchen?

Sie haben gelernt, dass ein Ausdrucksbaum eine Datenstruktur ist, die ausführbaren Code darstellt. Bisher haben wir jedoch nicht die zentrale Frage beantwortet, warum man eine solche Umstellung vornehmen möchte. Dies ist die Frage, die wir zu Beginn dieses Beitrags gestellt haben, und jetzt ist es Zeit, sie zu beantworten.

Eine LINQ to SQL-Abfrage wird in Ihrem C # -Programm nicht ausgeführt. Stattdessen wird es in SQL übersetzt, über eine Leitung gesendet und auf einem Datenbankserver ausgeführt. Mit anderen Worten, der folgende Code wird in Ihrem Programm niemals ausgeführt:
var query = from c in db.Customers where c.City == "Nantes" select new { c.City, c.CompanyName };

Es wird zuerst in die folgende SQL-Anweisung übersetzt und dann auf einem Server ausgeführt:
SELECT [t0].[City], [t0].[CompanyName] FROM [dbo].[Customers] AS [t0] WHERE [t0].[City] = @p0

Der in einem Abfrageausdruck gefundene Code muss in eine SQL-Abfrage übersetzt werden, die als Zeichenfolge an einen anderen Prozess gesendet werden kann. In diesem Fall handelt es sich bei diesem Prozess zufällig um eine SQL Server-Datenbank. Es wird offensichtlich viel einfacher sein, eine Datenstruktur wie einen Ausdrucksbaum in SQL zu übersetzen, als rohen IL oder ausführbaren Code in SQL zu übersetzen. Um die Schwierigkeit des Problems etwas zu übertreiben, stellen Sie sich vor, Sie würden versuchen, eine Reihe von Nullen und Einsen in SQL zu übersetzen!

Wenn es Zeit ist, Ihren Abfrageausdruck in SQL zu übersetzen, wird der Ausdrucksbaum, der Ihre Abfrage darstellt, zerlegt und analysiert, genau wie wir unseren einfachen Lambda-Ausdrucksbaum im vorherigen Abschnitt auseinander genommen haben. Zugegeben, der Algorithmus zum Parsen des LINQ to SQL-Ausdrucksbaums ist viel ausgefeilter als der von uns verwendete, aber das Prinzip ist dasselbe. Sobald die Teile des Ausdrucksbaums analysiert wurden, überlegt LINQ sie und entscheidet, wie eine SQL-Anweisung am besten geschrieben wird, die die angeforderten Daten zurückgibt.

Ausdrucksbäume wurden erstellt, um Code wie einen Abfrageausdruck in eine Zeichenfolge zu konvertieren, die an einen anderen Prozess übergeben und dort ausgeführt werden kann. So einfach ist das. Hier gibt es kein großes Geheimnis, keinen Zauberstab, der geschwenkt werden muss. Man nimmt einfach Code, konvertiert ihn in Daten und analysiert dann die Daten, um die Bestandteile zu finden, die in eine Zeichenfolge übersetzt werden, die an einen anderen Prozess übergeben werden kann.

Da die Abfrage an den Compiler geht, der in einer solchen abstrakten Datenstruktur gekapselt ist, kann der Compiler sie fast beliebig interpretieren. Es ist nicht gezwungen, die Abfrage in einer bestimmten Reihenfolge oder auf eine bestimmte Weise auszuführen. Stattdessen kann es den Ausdrucksbaum analysieren, herausfinden, was Sie tun möchten, und dann entscheiden, wie es getan werden soll. Zumindest theoretisch hat es die Freiheit, eine beliebige Anzahl von Faktoren zu berücksichtigen, wie z. B. den aktuellen Netzwerkverkehr, die Belastung der Datenbank, die aktuellen verfügbaren Ergebnismengen usw. In der Praxis berücksichtigt LINQ to SQL nicht alle diese Faktoren , aber es ist theoretisch frei, so ziemlich das zu tun, was es will. Darüber hinaus könnte man diesen Ausdrucksbaum an einen benutzerdefinierten Code übergeben, den Sie von Hand schreiben, um ihn zu analysieren und in etwas zu übersetzen, das sich von dem unterscheidet, was von LINQ to SQL erzeugt wird.

Wieder einmal sehen wir, dass die Ausdrucksbäume es uns ermöglichen, darzustellen (auszudrücken?), Was wir tun möchten. Und wir verwenden Übersetzer, die entscheiden, wie unsere Ausdrücke verwendet werden.


2
Eine der besseren Antworten.
Johnny

4
ausgezeichnete Antwort. Ein kleiner Aspekt, der dieser brillanten Erklärung hinzugefügt werden muss, ist: Eine andere Verwendung von Ausdrucksbäumen besteht darin, dass Sie den Ausdrucksbaum zur Laufzeit nach Belieben ändern können, bevor Sie ihn zur Ausführung füttern, was manchmal äußerst nützlich ist.
Yan D

41

Ein Ausdrucksbaum ist ein Mechanismus zum Übersetzen von ausführbarem Code in Daten. Mithilfe eines Ausdrucksbaums können Sie eine Datenstruktur erstellen, die Ihr Programm darstellt.

In C # können Sie mit dem von Lambda-Ausdrücken erzeugten Ausdrucksbaum mithilfe der Expression<T>Klasse arbeiten.


In einem herkömmlichen Programm schreiben Sie Code wie folgt:

double hypotenuse = Math.Sqrt(a*a + b*b);

Dieser Code veranlasst den Compiler, eine Zuweisung zu generieren, und das war's. In den meisten Fällen ist das alles, was Sie interessiert.

Mit herkömmlichem Code kann Ihre Anwendung nicht rückwirkend zurückgehen und überprüfen hypotenuse, ob sie durch Ausführen eines Math.Sqrt()Aufrufs erstellt wurde. Diese Informationen sind einfach nicht Teil dessen, was enthalten ist.

Betrachten Sie nun einen Lambda-Ausdruck wie den folgenden:

Func<int, int, double> hypotenuse = (a, b) => Math.Sqrt(a*a + b*b);

Das ist etwas anders als zuvor. Jetzt hypotenuseist eigentlich ein Verweis auf einen Block von ausführbarem Code . Wenn Sie anrufen

hypotenuse(3, 4);

Sie erhalten den zurückgegebenen Wert 5.

Wir können Ausdrucksbäume verwenden , um den Block ausführbaren Codes zu untersuchen, der erzeugt wurde. Versuchen Sie stattdessen Folgendes:

Expression<Func<int, int, int>> addTwoNumbersExpression = (x, y) => x + y;
BinaryExpression body = (BinaryExpression) addTwoNumbersExpression.Body;
Console.WriteLine(body);

Dies erzeugt:

(x + y)

Fortgeschrittenere Techniken und Manipulationen sind mit Ausdrucksbäumen möglich.


7
OK, ich war bis zum Ende bei dir, aber ich verstehe immer noch nicht wirklich, warum das eine große Sache ist. Es fällt mir schwer, an Anwendungen zu denken.

1
Er benutzte ein vereinfachtes Beispiel; Die wahre Stärke liegt in der Tatsache, dass Ihr Code, der den Ausdrucksbaum untersucht, auch dafür verantwortlich gemacht werden kann, ihn zu interpretieren und dem Ausdruck eine semantische Bedeutung zuzuweisen.
Pierreten

2
Ja, diese Antwort wäre besser gewesen, wenn er / sie erklärt hätte, warum (x + y) für uns tatsächlich nützlich war. Warum sollten wir erforschen wollen (x + y) und wie machen wir das?
Paul Matthews

Sie müssen es nicht untersuchen, Sie tun es nur, um zu sehen, was Ihre Abfrage ist und was in diesem Fall in eine andere Sprache in SQL übersetzt wird
stanimirsp

15

Ausdrucksbäume sind eine speicherinterne Darstellung eines Ausdrucks, z. B. ein arithmetischer oder boolescher Ausdruck. Betrachten Sie zum Beispiel den arithmetischen Ausdruck

a + b*2

Da * eine höhere Operatorrangfolge als + hat, wird der Ausdrucksbaum folgendermaßen erstellt:

    [+]
  /    \
 a     [*]
      /   \
     b     2

Mit diesem Baum kann er für alle Werte von a und b ausgewertet werden. Darüber hinaus können Sie es in andere Ausdrucksbäume umwandeln, um beispielsweise den Ausdruck abzuleiten.

Wenn Sie einen Ausdruck Baum implementieren, würde ich vorschlagen , eine Basisklasse erstellen Expression . Daraus abgeleitet würde die Klasse BinaryExpression für alle binären Ausdrücke wie + und * verwendet. Dann könnten Sie eine VariableReferenceExpression in Referenzvariablen (wie a und b) und eine andere Klasse ConstantExpression (für die 2 aus dem Beispiel) einführen .

Der Ausdrucksbaum wird in vielen Fällen als Ergebnis der Analyse einer Eingabe erstellt (direkt vom Benutzer oder aus einer Datei). Für die Auswertung des Ausdrucksbaums würde ich empfehlen, das Besuchermuster zu verwenden .


15

Kurze Antwort: Es ist schön, dieselbe Art von LINQ-Abfrage schreiben und auf eine beliebige Datenquelle verweisen zu können. Sie könnten keine "Language Integrated" -Abfrage ohne sie haben.

Lange Antwort: Wie Sie wahrscheinlich wissen, transformieren Sie den Quellcode beim Kompilieren von einer Sprache in eine andere. Normalerweise von einer Hochsprache (C #) bis zu einem niedrigeren Hebel (IL).

Grundsätzlich gibt es zwei Möglichkeiten, dies zu tun:

  1. Sie können den Code mit Suchen und Ersetzen übersetzen
  2. Sie analysieren den Code und erhalten einen Analysebaum.

Letzteres ist das, was alle Programme, die wir als "Compiler" kennen, tun.

Sobald Sie einen Analysebaum haben, können Sie ihn problemlos in eine andere Sprache übersetzen. Dies ermöglichen uns Ausdrucksbäume. Da der Code als Daten gespeichert ist, können Sie alles tun, was Sie wollen, aber wahrscheinlich möchten Sie ihn nur in eine andere Sprache übersetzen.

In LINQ to SQL werden die Ausdrucksbäume nun in einen SQL-Befehl umgewandelt und dann über die Leitung an den Datenbankserver gesendet. Soweit ich weiß, machen sie beim Übersetzen des Codes nichts wirklich Besonderes, aber sie könnten es . Beispielsweise könnte der Abfrageanbieter abhängig von den Netzwerkbedingungen unterschiedlichen SQL-Code erstellen.


6

IIUC, ein Ausdrucksbaum ähnelt einem abstrakten Syntaxbaum, aber ein Ausdruck ergibt normalerweise einen einzelnen Wert, während ein AST ein gesamtes Programm darstellen kann (mit Klassen, Paketen, Funktionen, Anweisungen usw.).

Für den Ausdruck (2 + 3) * 5 lautet der Baum jedenfalls:

    *
   / \ 
  +   5
 / \
2   3

Bewerten Sie jeden Knoten rekursiv (von unten nach oben), um den Wert am Wurzelknoten zu erhalten, dh den Wert des Ausdrucks.

Sie können natürlich auch unäre (Negation) oder trinäre (Wenn-Dann-Sonst) Operatoren und Funktionen (n-ary, dh eine beliebige Anzahl von Operationen) haben, wenn Ihre Ausdruckssprache dies zulässt.

Die Bewertung von Typen und die Typsteuerung erfolgt über ähnliche Bäume.


5

Die DLR-
Ausdrucksbäume sind eine Ergänzung zu C #, um die Dynamic Language Runtime (DLR) zu unterstützen. Das DLR ist auch dafür verantwortlich, uns die "var" -Methode zur Deklaration von Variablen zu geben. ( var objA = new Tree();)

Mehr zum DLR .

Im Wesentlichen wollte Microsoft die CLR für dynamische Sprachen wie LISP, SmallTalk, Javascript usw. öffnen. Dazu mussten sie in der Lage sein, Ausdrücke im laufenden Betrieb zu analysieren und auszuwerten. Das war vor dem DLR nicht möglich.

Zurück zu meinem ersten Satz: Ausdrucksbäume sind eine Ergänzung zu C #, die die Möglichkeit eröffnet, das DLR zu verwenden. Zuvor war C # eine viel statischere Sprache - alle Variablentypen mussten als bestimmter Typ deklariert und der gesamte Code zur Kompilierungszeit geschrieben werden.

Die Verwendung mit Datenausdrucksbäumen
öffnet die Schleusentore für dynamischen Code.

Angenommen, Sie erstellen eine Immobilienseite. Während der Entwurfsphase kennen Sie alle Filter, die Sie anwenden können. Um diesen Code zu implementieren, haben Sie zwei Möglichkeiten: Sie können eine Schleife schreiben, die jeden Datenpunkt mit einer Reihe von Wenn-Dann-Prüfungen vergleicht. Sie können auch versuchen, eine Abfrage in einer dynamischen Sprache (SQL) zu erstellen und diese an ein Programm weiterzuleiten, das die Suche für Sie (die Datenbank) durchführen kann.

Mit Ausdrucksbäumen können Sie jetzt den Code in Ihrem Programm im laufenden Betrieb ändern und die Suche durchführen. Insbesondere können Sie dies über LINQ tun.

(Weitere Informationen : MSDN: Gewusst wie: Verwenden von Ausdrucksbäumen zum Erstellen dynamischer Abfragen ).

Über Daten hinaus
Die Hauptverwendung für Ausdrucksbäume ist die Verwaltung von Daten. Sie können jedoch auch für dynamisch generierten Code verwendet werden. Wenn Sie also eine dynamisch definierte Funktion (z. B. Javascript) möchten, können Sie einen Ausdrucksbaum erstellen, kompilieren und die Ergebnisse auswerten.

Ich würde etwas tiefer gehen, aber diese Seite macht einen viel besseren Job:

Ausdrucksbäume als Compiler

Zu den aufgeführten Beispielen gehören das Erstellen generischer Operatoren für Variablentypen, das manuelle Rollen von Lambda-Ausdrücken, das flache Klonen mit hoher Leistung und das dynamische Kopieren von Lese- / Schreibeigenschaften von einem Objekt in ein anderes.

Zusammenfassung
Ausdrucksbäume sind Darstellungen von Code, der zur Laufzeit kompiliert und ausgewertet wird. Sie ermöglichen dynamische Typen, was für die Datenmanipulation und die dynamische Programmierung nützlich ist.


Ja, ich weiß, dass ich zu spät zum Spiel komme, aber ich wollte diese Antwort schreiben, um sie selbst zu verstehen. (Diese Frage tauchte in meiner Internetsuche hoch auf.)
Richard

Gute Arbeit. Das ist eine gute Antwort.
Rich Bryant

5
Das Schlüsselwort "var" hat nichts mit DLR zu tun. Sie verwechseln es mit der "Dynamik".
Yarik

Dies ist eine gute, kleine Antwort auf var hier, die zeigt, dass Yarik richtig ist. Dankbar für den Rest der Antwort. quora.com/...
johnny

1
Das ist alles falsch. varist ein syntaktischer Zucker zur Kompilierungszeit - er hat nichts mit Ausdrucksbäumen, DLR oder der Laufzeit zu tun. var i = 0wird so kompiliert, als ob Sie geschrieben hätten int i = 0, sodass Sie keinen varTyp darstellen können, der in der Kompilierungszeit nicht bekannt ist. Ausdrucksbäume sind keine "Ergänzung zur Unterstützung des DLR", sondern werden in .NET 3.5 eingeführt, um LINQ zu ermöglichen. DLR hingegen wird in .NET 4.0 eingeführt, um dynamische Sprachen (wie IronRuby) und das dynamicSchlüsselwort zuzulassen . Ausdrucksbäume werden vom DLR tatsächlich zur Bereitstellung von Interop verwendet, nicht umgekehrt.
Şafak Gür

-3

Ist der Ausdrucksbaum, auf den Sie sich beziehen, ein Ausdrucksbewertungsbaum?

Wenn ja, handelt es sich um einen vom Parser erstellten Baum. Parser verwendete den Lexer / Tokenizer, um die Token aus dem Programm zu identifizieren. Parser erstellt den Binärbaum aus den Token.

Hier ist die detaillierte Erklärung


Zwar funktioniert ein Ausdrucksbaum, auf den sich das OP bezieht, ähnlich und mit demselben zugrunde liegenden Konzept wie ein Analysebaum. Er wird jedoch zur Laufzeit dynamisch mit Code ausgeführt. Beachten Sie jedoch bei der Einführung des Roslyn-Compilers die Zeile von Die Trennung zwischen den beiden wurde wirklich verschwommen, wenn sie nicht vollständig entfernt wurde.
yoel halb
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.