Freigeben über


Ausdrucksbäume – bestimmende Daten für Code

Ein Ausdrucksbaum ist eine Datenstruktur, die Code definiert. Ausdrucksstrukturen basieren auf denselben Strukturen, die ein Compiler verwendet, um Code zu analysieren und die kompilierte Ausgabe zu generieren. Wie Sie diesen Artikel lesen, bemerken Sie eine ziemliche Ähnlichkeit zwischen Expression Trees und den Typen, die in den Roslyn-APIs zum Erstellen von Analyzern und CodeFixes verwendet werden. (Analyzer und CodeFixes sind NuGet-Pakete, die statische Analysen für Code ausführen und potenzielle Korrekturen für einen Entwickler vorschlagen.) Die Konzepte sind ähnlich, und das Endergebnis ist eine Datenstruktur, die eine sinnvolle Untersuchung des Quellcodes ermöglicht. Expression Trees basieren jedoch auf einer anderen Gruppe von Klassen und APIs als die Roslyn-APIs. Hier ist eine Codezeile:

var sum = 1 + 2;

Wenn Sie den vorherigen Code als Ausdrucksstruktur analysieren, enthält die Struktur mehrere Knoten. Der äußerste Knoten ist eine Variabledeklarationsanweisung mit Zuordnung (var sum = 1 + 2;) Dieser äußerste Knoten enthält mehrere untergeordnete Knoten: eine Variabledeklaration, einen Zuordnungsoperator und einen Ausdruck, der die rechte Seite des Gleichheitszeichens darstellt. Dieser Ausdruck wird weiter in Ausdrücke unterteilt, die den Additionsvorgang darstellen, sowie die linken und rechten Operanden der Addition.

Lassen Sie uns die Ausdrücke, die die rechte Seite des Gleichheitszeichens bilden, genauer betrachten. Der Ausdruck ist 1 + 2, ein binärer Ausdruck. Genauer gesagt ist es ein binärer Additionsausdruck. Ein binäre Additionsausdruck verfügt über zwei untergeordnete Elemente, die den linken und rechten Knoten des Additionsausdrucks darstellen. Hier sind beide Knoten konstanten Ausdrücke: Der linke Operand ist der Wert 1, und der rechte Operand ist der Wert 2.

Optisch ist die gesamte Aussage ein Baum: Man könnte am Wurzelknoten beginnen und zu jedem Knoten im Baum gehen, um den Code zu sehen, der die Aussage ausmacht.

  • Variablendeklaration-Anweisung mit der Zuordnung (var sum = 1 + 2;)
    • Implizite Variablentypdeklaration (var sum)
      • Implizites Var-Schlüsselwort (var)
      • Variablennamendeklaration (sum)
    • Zuweisungsoperator (=)
    • Binärer Additionsausdruck (1 + 2)
      • Linker Operand (1)
      • Additionsoperator (+)
      • Rechter Operand (2)

Der vorgängige Baum mag kompliziert aussehen, aber er ist sehr mächtig. Nach demselben Prozess zersetzen Sie viel kompliziertere Ausdrücke. Betrachten Sie diesen Ausdruck:

var finalAnswer = this.SecretSauceFunction(
    currentState.createInterimResult(), currentState.createSecondValue(1, 2),
    decisionServer.considerFinalOptions("hello")) +
    MoreSecretSauce('A', DateTime.Now, true);

Der vorherige Ausdruck ist auch eine Variabledeklaration mit einer Zuordnung. In dieser Instanz ist die rechte Seite der Zuordnung eine wesentlich kompliziertere Struktur. Sie werden diesen Ausdruck nicht dekompilieren, aber überlegen Sie, was die verschiedenen Knoten sein könnten. Es gibt Methodenaufrufe, die das aktuelle Objekt als Empfänger verwenden: einen mit einem expliziten this Empfänger und einen, der keinen hat. Es gibt Methodenaufrufe mit anderen Empfängerobjekten, es gibt konstante Argumente unterschiedlicher Typen. Und schließlich gibt es einen binären Additionsoperator. Je nach Rückgabetyp von SecretSauceFunction() oder MoreSecretSauce(), kann dieser binäre Additionsoperator ein Methodenaufruf an einen überschriebenen Additionsoperator sein, der einen statischen Methodenaufruf des binären Additionsoperators, der für eine Klasse definiert ist auflöst.

Trotz dieser wahrgenommenen Komplexität erstellt der vorherige Ausdruck eine Baumstruktur, die ebenso einfach zu navigieren ist wie das erste Beispiel. Sie lassen untergeordnete Knoten durchlaufen, um Blattknoten im Ausdruck zu finden. Übergeordnete Knoten verfügen über Verweise auf ihre untergeordneten Elemente, und jeder Knoten verfügt über eine Eigenschaft, die beschreibt, welche Art von Knoten es ist.

Die Struktur eines Ausdrucksbaums ist sehr konsistent. Nachdem Sie die Grundlagen kennengelernt haben, verstehen Sie sogar den komplexesten Code, wenn er als Ausdrucksstruktur dargestellt wird. Die Eleganz in der Datenstruktur erklärt, wie der C#-Compiler die komplexesten C#-Programme analysiert und eine ordnungsgemäße Ausgabe aus diesem komplizierten Quellcode erzeugt.

Sobald Sie mit der Struktur von Ausdrucksbäumen vertraut sind, stellen Sie fest, dass das erworbene Wissen Sie schnell in die Lage versetzt, mit vielen fortgeschrittenen Szenarien zu arbeiten. Die Leistung von Ausdrucksbaumstrukturen ist unglaublich.

Neben der Übersetzung von Algorithmen, die in anderen Umgebungen ausgeführt werden, erleichtern Ausdrucksstrukturen das Schreiben von Algorithmen, die Code überprüfen, bevor sie ausgeführt werden. Sie schreiben eine Methode, deren Argumente Ausdrücke sind, und untersuchen diese Ausdrücke, bevor Sie den Code ausführen. Der Ausdrucksbaum ist eine vollständige Darstellung des Codes: Sie können die Werte jedes Unterausdrucks sehen. Methoden- und Eigenschaftennamen werden angezeigt. Der Wert von konstanten Ausdrücken wird angezeigt. Sie konvertieren auch eine Ausdrucksbaumstruktur in einen ausführbaren Delegaten und führen den Code aus.

Mit den APIs für Ausdrucksstrukturen können Sie Strukturen erstellen, die fast jedes gültige Codekonstrukt darstellen. Um die Dinge jedoch so einfach wie möglich zu halten, können einige C#-Idiome nicht in einem Ausdrucksbaum erstellt werden. Ein Beispiel sind asynchrone Ausdrücke (unter Verwendung der Schlüsselwörter async und await). Wenn Ihre Anforderungen asynchrone Algorithmen erfordern, müssen Sie die Task Objekte direkt bearbeiten, anstatt die Compilerunterstützung zu verwenden. Ein weiteres Beispiel ist die Erstellung von Schleifen. In der Regel erstellen Sie diese Schleifen mithilfe von for, foreach, while oder do-Schleifen. Wie Sie später in dieser Reihe sehen, unterstützen die APIs für Ausdrucksbaumstrukturen einen einzelnen Schleifenausdruck mit break- und continue-Ausdrücken, die die Wiederholung der Schleife steuern.

Die eine Sache, die Sie nicht tun können, ist das Ändern eines Ausdrucksbaums. Ausdrucksbäume sind unveränderliche Datenstrukturen. Wenn Sie einen Ausdrucksbaum mutieren (verändern) möchten, müssen Sie einen neuen Baum erstellen, der eine Kopie des Originals ist, jedoch mit den gewünschten Änderungen.