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 function
zeigt 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.Expressions
Namespace 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
?
var body = expression.Body;
var parameters = expression.Parameters;
var firstParam = parameters[0];
var secondParam = parameters[1].
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.