Ausdrucksbaumstrukturen: Daten, die Code definieren

Eine Ausdrucksbaumstruktur ist eine Datenstruktur, die Code darstellt. Ausdrucksbaumstrukturen basieren auf denselben Strukturen, die ein Compiler verwendet, um Code zu analysieren und die kompilierte Ausgabe zu generieren. Wenn Sie diesen Artikel lesen, werden Sie feststellen, dass eine Ähnlichkeit zwischen Ausdrucksbaumstrukturen und den Typen in den Roslyn-APIs vorhanden ist, um Analysetools und CodeFixes zu erstellen. (Analysetools und CodeFixes sind NuGet-Pakete, die statische Analysen für Code durchführen und potenzielle Korrekturen für einen Entwickler vorschlagen können.) Die Konzepte sind ähnlich, das Endergebnis ist eine Datenstruktur, die eine sinnvolle Untersuchung des Quellcodes ermöglicht. Ausdrucksbaumstrukturen basieren jedoch auf einem anderen Satz von Klassen und APIs als die Roslyn-APIs. Hier ist eine Codezeile:

var sum = 1 + 2;

Wenn Sie den obigen Code als eine Ausdrucksbaumstruktur analysieren, stellen Sie fest, dass die Struktur mehrere Knoten enthält. Der äußerste Knoten ist eine Variablendeklaration-Anweisung mit der Zuordnung (var sum = 1 + 2;). Dieser äußerste Knoten enthält mehrere untergeordnete Knoten: eine Variablendeklaration, ein Zuweisungsoperator und ein Ausdruck, der die rechte Seite des Gleichheitszeichens darstellt. Dieser Ausdruck wird weiter unterteilt in Ausdrücke, die den Additionsvorgang und linken und rechten Operanden der Addition darstellen.

Lassen Sie uns die Ausdrücke etwas genauer ansehen, die die rechte Seite neben dem Gleichheitszeichen bilden. 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 handelt es sich bei beiden Knoten um konstante Ausdrücke: Der linke Operand ist der Wert 1, und der rechte Operand ist der Wert 2.

Visuell ist die gesamte Anweisung eine Struktur: Sie können beim Stammknoten beginnen und zu jedem Knoten in der Struktur navigieren, um den Code anzuzeigen, der die Anweisung bildet:

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

Die obige Struktur mag kompliziert aussehen, ist aber sehr leistungsfähig. Mit demselben Verfahren können Sie wesentlich kompliziertere Ausdrücke zerlegen. Betrachten Sie diesen Ausdruck:

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

Der obige Ausdruck ist auch eine Variablendeklaration mit einer Zuordnung. In dieser Instanz ist die rechte Seite der Zuordnung eine wesentlich kompliziertere Struktur. Wir werden diesen Ausdruck nicht zerlegen, aber beachten Sie, was die verschiedenen Knoten sein könnten. Es sind Methodenaufrufe vorhanden, die das aktuelle Objekt als Empfänger verwenden, einen, der einen expliziten this-Empfänger hat und einen, der das nicht hat. Es sind Methodenaufrufe vorhanden, die andere Empfängerobjekte verwenden. Es sind konstante Argumente verschiedener Typen vorhanden. Schließlich gibt es auch 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 spürbaren Komplexität erstellt der obige Ausdruck eine Baumstruktur, in der so einfach wie im ersten Beispiel navigiert werden kann. 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 einer Ausdrucksbaumstruktur ist sehr konsistent. Sobald Sie mit den Grundlagen vertraut sind, verstehen Sie auch den komplexesten Code, wenn er als Ausdrucksbaumstruktur dargestellt wird. Die Eleganz in der Datenstruktur erläutert, wie der C#-Compiler die komplexesten C#-Programme analysiert und eine korrekte Ausgabe aus diesem komplizierten Quellcode erstellt.

Sobald Sie mit der Struktur von Ausdrucksbaumstrukturen vertraut sind, stellen Sie fest, dass dieses Wissen Ihnen ermöglicht, mit mehr erweiterten Szenarios zu arbeiten. Die Leistung von Ausdrucksbaumstrukturen ist unglaublich.

Zusätzlich zum Übersetzen von Algorithmen, die in anderen Umgebungen ausgeführt werden sollen, erleichtern Ausdrucksbaumstrukturen das Schreiben von Algorithmen, die Code überprüfen, bevor Sie ihn ausführen. Sie schreiben eine Methode, deren Argumente Ausdrücke sind, und überprüfen diese Ausdrücke anschließend vor dem Ausführen des Codes. Die Ausdrucksbaumstruktur ist eine vollständige Darstellung des Codes: Sie sehen Werte von beliebigen Unterausdrücken. Sie sehen Methoden- und Eigenschaftennamen. Sie sehen den Wert jedes konstanten Ausdrucks. Sie konvertieren auch eine Ausdrucksbaumstruktur in einen ausführbaren Delegaten und führen den Code aus.

Mit den APIs für Ausdrucksbaumstrukturen können Sie Strukturen erstellen, die fast jeden gültigen Codekonstrukt darstellen. Um die Dinge so einfach wie möglich zu halten, können jedoch einige C#-Ausdrücke nicht in einer Ausdrucksbaumstruktur erstellt werden. Ein Beispiel sind asynchrone Ausdrücke (mithilfe der async- und await-Schlüsselwörter). Wenn Ihre Bedürfnisse asynchrone Algorithmen erfordern, müssten Sie die Task-Objekte direkt bearbeiten, anstatt sich auf die Unterstützung des Compilers zu verlassen. Ein weiteres Beispiel ist die Erstellung von Schleifen. Normalerweise erstellen Sie diese Schleifen mithilfe der Schleifen for, foreach, while oder do. 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.

Eine Sache, die für Sie nicht möglich ist, ist die Änderung einer Ausdrucksbaumstruktur. Ausdrucksbaumstrukturen sind unveränderliche Datenstrukturen. Wenn Sie eine Ausdrucksbaumstruktur ändern möchten, müssen Sie eine neue Struktur erstellen, die eine Kopie des Originals ist, aber mit den gewünschten Änderungen.