Ausführen von Ausdrucksbaumstrukturen

Eine Ausdrucksbaumstruktur ist eine Datenstruktur, die Code darstellt. Es ist nicht kompilierter und ausführbarer Code. Wenn Sie den .NET-Code ausführen möchten, der durch eine Ausdrucksbaumstruktur dargestellt wird, müssen Sie ihn in ausführbare IL-Anweisungen konvertieren. Die Ausführung einer Ausdrucksbaumstruktur gibt möglicherweise einen Wert zurück. Es kann jedoch auch nur eine Aktion ausgeführt werden, z.B. das Aufrufen einer Methode.

Nur Ausdrucksbaumstrukturen, die Lambdaausdrücke darstellen, können ausgeführt werden. Ausdrucksbaumstrukturen, die Lambdaausdrücke darstellen, sind vom Typ LambdaExpression oder Expression<TDelegate>. Um diese Ausdrucksbaumstruktur auszuführen, rufen Sie die Compile-Methode auf, um einen ausführbaren Delegaten zu erstellen und diesen anschließend aufzurufen.

Hinweis

Wenn der Typ des Delegaten nicht bekannt ist, d.h. wenn der Lambdaausdruck vom Typ LambdaExpression und nicht Expression<TDelegate> ist, rufen Sie die DynamicInvoke-Methode auf dem Delegaten auf, anstatt sie direkt aufzurufen.

Wenn eine Ausdrucksbaumstruktur keinen Lambdaausdruck darstellt, können Sie einen neuen Lambdaausdruck erstellen, der die ursprüngliche Ausdrucksbaumstruktur als Textkörper hat, indem Sie die Lambda<TDelegate>(Expression, IEnumerable<ParameterExpression>)-Methode aufrufen. Anschließend können Sie den Lambdaausdruck ausführen, wie weiter oben in diesem Abschnitt beschrieben.

Lambdaausdrücke zu Funktionen

Sie können jede LambdaExpression oder jeden Typ, der von LambdaExpression in eine ausführbare IL abgeleitet wurde, konvertieren. Andere Ausdruckstypen können nicht direkt in Code konvertiert werden. Diese Einschränkung wirkt sich kaum auf die Praxis aus. Lambdaausdrücke sind die einzigen Typen von Ausdrücken, die Sie ausführen möchten, indem Sie sie in eine ausführbare Zwischensprache (Intermediate Language, IL) konvertieren. (Überlegen Sie, was es bedeuten würde, eine System.Linq.Expressions.ConstantExpression direkt auszuführen. Wäre das sinnvoll?) Jede Ausdrucksstruktur, die eine System.Linq.Expressions.LambdaExpressionoder ein von LambdaExpression abgeleiteter Typ ist, kann in IL konvertiert werden. Der Ausdruckstyp System.Linq.Expressions.Expression<TDelegate> ist das einzige konkrete Beispiel in den .NET Core-Bibliotheken. Hiermit wird ein Ausdruck dargestellt, der jedem Delegattyp zugeordnet ist. Da dieser Typ einem Delegattyp zugeordnet ist, kann .NET den Ausdruck untersuchen, und die IL für einen entsprechenden Delegaten generieren, der der Signatur des Lambdaausdrucks entspricht. Der Delegattyp basiert auf dem Ausdruckstyp. Sie müssen den Rückgabetyp und die Argumentliste kennen, wenn Sie das Delegatobjekt mit strikter Typzuordnung verwenden möchten. Die LambdaExpression.Compile()-Methode gibt den Delegate-Typ zurück. Sie müssen ihn in den richtigen Delegattyp umwandeln, um Kompilierzeittools die Argumentliste oder den Rückgabetyp überprüfen zu lassen.

In den meisten Fällen ist eine einfache Zuordnung zwischen einem Ausdruck und seinem entsprechenden Delegaten vorhanden. Angenommen, eine durch Expression<Func<int>> dargestellte Ausdrucksbaumstruktur wird in einen Delegaten des Typs Func<int> konvertiert. Für einen Lambdaausdruck mit beliebigem Rückgabetyp und Argumentliste besteht ein Delegattyp, der der Zieltyp für den ausführbaren Code ist, der von diesem Lambdaausdruck dargestellt wird.

Der System.Linq.Expressions.LambdaExpression-Typ enthält LambdaExpression.Compile- und LambdaExpression.CompileToMethod-Member, die Sie verwenden würden, um eine Ausdrucksbaumstruktur in ausführbaren Code zu konvertieren. Die Compile-Methode erstellt einen Delegaten. Die CompileToMethod-Methode aktualisiert ein System.Reflection.Emit.MethodBuilder-Objekt mit der IL, das die kompilierte Ausgabe der Ausdrucksbaumstruktur darstellt.

Wichtig

CompileToMethod ist nur in .NET Framework verfügbar, nicht in .NET Core oder .NET 5 und höher.

Optional können Sie auch einen System.Runtime.CompilerServices.DebugInfoGenerator angeben, der das Debuginformationensymbol für das generierte Delegatobjekt empfängt. Der DebugInfoGenerator stellt vollständige Debuginformationen zum generierten Delegaten bereit.

Sie würden einen Ausdruck mithilfe des folgenden Code in einen Delegaten konvertieren:

Expression<Func<int>> add = () => 1 + 2;
var func = add.Compile(); // Create Delegate
var answer = func(); // Invoke Delegate
Console.WriteLine(answer);

Im folgenden Codebeispiel werden die konkreten Typen veranschaulicht, die beim Kompilieren und Ausführen einer Ausdrucksstruktur verwendet werden.

Expression<Func<int, bool>> expr = num => num < 5;

// Compiling the expression tree into a delegate.
Func<int, bool> result = expr.Compile();

// Invoking the delegate and writing the result to the console.
Console.WriteLine(result(4));

// Prints True.

// You can also use simplified syntax
// to compile and run an expression tree.
// The following line can replace two previous statements.
Console.WriteLine(expr.Compile()(4));

// Also prints True.

Im folgenden Codebeispiel wird veranschaulicht, wie eine Ausdrucksbaumstruktur ausgeführt wird, die das Potenzieren darstellt, indem ein Lambdaausdruck erstellt und ausgeführt wird. Das Ergebnis, das die potenzierte Zahl darstellt, wird angezeigt.

// The expression tree to execute.
BinaryExpression be = Expression.Power(Expression.Constant(2d), Expression.Constant(3d));

// Create a lambda expression.
Expression<Func<double>> le = Expression.Lambda<Func<double>>(be);

// Compile the lambda expression.
Func<double> compiledExpression = le.Compile();

// Execute the lambda expression.
double result = compiledExpression();

// Display the result.
Console.WriteLine(result);

// This code produces the following output:
// 8

Ausführung und Lebensdauer

Sie führen den Code durch Aufrufen des Delegaten aus, den Sie beim Aufrufen von LambdaExpression.Compile() erstellt haben. Der vorangehende Code gibt einen add.Compile()-Delegaten zurück. Sie rufen diesen Delegaten auf, indem Sie func() aufrufen, wodurch der Code ausgeführt wird.

Dieser Delegat stellt den Code in der Ausdrucksbaumstruktur dar. Sie können das Handle für diesen Delegaten beibehalten und es später aufrufen. Sie müssen die Ausdrucksbaumstruktur nicht jedes Mal kompilieren, wenn Sie den Code ausführen möchten, den sie darstellt. (Beachten Sie, dass Ausdrucksbaumstrukturen unveränderlich sind und das Kompilieren derselben Ausdrucksbaumstruktur später einen Delegaten erstellt, der denselben Code ausführt.)

Achtung

Erstellen Sie keine weiteren anspruchsvollen Zwischenspeicherungsmechanismen, um die Leistung zu erhöhen, indem Sie unnötige Kompilieraufrufe vermeiden. Das Vergleichen zwei beliebiger Ausdrucksbaumstrukturen, um festzustellen, ob sie denselben Algorithmus darstellen, ist ein zeitaufwändiger Vorgang. Die Rechenzeit zum Ausführen von Code, der bestimmt, ob zwei verschiedene Ausdrucksbaumstrukturen zum selben ausführbaren Code führen, überschreitet wahrscheinlich die Rechenzeit, die Sie durch Vermeiden zusätzlicher Aufrufe von LambdaExpression.Compile() sparen.

Vorbehalte

Das Kompilieren eines Lambdaausdrucks in einen Delegaten und das Aufrufen dieses Delegaten, ist einer der einfachsten Vorgänge, die Sie mit einer Ausdrucksbaumstruktur ausführen können. Trotz dieses einfachen Vorgangs gibt es jedoch Hinweise, die Sie beachten müssen.

Lambdaausdrücke erstellen Closures über lokale Variablen, auf die im Ausdruck verwiesen wird. Sie müssen sicherstellen, dass alle Variablen, die Teil des Delegaten wären, am Speicherort verwendet werden können, wo Sie Compile aufrufen sowie beim Ausführen des resultierenden Delegats. Der Compiler stellt sicher, dass sich Variablen im Bereich befinden. Wenn jedoch der Ausdruck auf eine Variable zugreift, die IDisposable implementiert, ist es möglich, dass der Code das Objekt löschen kann, während es weiterhin in der Ausdrucksbaumstruktur aufrechterhalten wird.

Angenommen, dieser Code funktioniert gut, da int nicht IDisposable implementiert:

private static Func<int, int> CreateBoundFunc()
{
    var constant = 5; // constant is captured by the expression tree
    Expression<Func<int, int>> expression = (b) => constant + b;
    var rVal = expression.Compile();
    return rVal;
}

Der Delegat hat einen Verweis auf die lokale Variable constant erfasst. Auf diese Variable wird später zugegriffen, wenn die von CreateBoundFunc zurückgegebene Funktion ausgeführt wird.

Jedoch sollten Sie die folgende (ausgedachte) Klasse beachten, die System.IDisposable implementiert:

public class Resource : IDisposable
{
    private bool _isDisposed = false;
    public int Argument
    {
        get
        {
            if (!_isDisposed)
                return 5;
            else throw new ObjectDisposedException("Resource");
        }
    }

    public void Dispose()
    {
        _isDisposed = true;
    }
}

Bei Verwendung in einem Ausdruck, wie im folgenden Code dargestellt, erhalten Sie eine System.ObjectDisposedException beim Ausführen von Code, auf den die Resource.Argument-Eigenschaft verweist:

private static Func<int, int> CreateBoundResource()
{
    using (var constant = new Resource()) // constant is captured by the expression tree
    {
        Expression<Func<int, int>> expression = (b) => constant.Argument + b;
        var rVal = expression.Compile();
        return rVal;
    }
}

Der Delegat, der von dieser Methode zurückgegeben wurde, wurde über das constant-Objekt geschlossen, das verworfen wurde. (Es wurde verworfen, da es in einer using-Anweisung deklariert wurde.)

Wenn Sie nun den von dieser Methode zurückgegebenen Delegaten ausführen, wird zum Zeitpunkt der Ausführung eine ObjectDisposedException ausgelöst.

Es erscheint sonderbar, dass ein Laufzeitfehler ein Kompilierzeitkonstrukt darstellt, aber das ist nun mal gang und gäbe, wenn Sie mit Ausdrucksbaumstrukturen arbeiten.

Da dieses Problem in vielen Kombinationen auftritt, ist es schwierig, eine allgemeine Anleitung zu bieten, um dies zu vermeiden. Seien Sie mit dem Zugriff auf lokale Variablen beim Definieren von Ausdrücken vorsichtig. Dies gilt auch beim Zugreifen auf Status im aktuellen Objekt (dargestellt durch this), wenn Sie eine Ausdrucksbaumstruktur erstellen, die durch eine öffentliche API zurückgegeben wird.

Der Code in einem Ausdruck kann auf Methoden oder Eigenschaften in anderen Assemblys verweisen. Der Zugriff auf diese Assembly muss möglich sein, wenn der Ausdruck definiert ist, wenn sie kompiliert wird und der resultierende Delegat aufgerufen wird. In Fällen, wo dies nicht der Fall ist, tritt eine ReferencedAssemblyNotFoundException auf.

Zusammenfassung

Ausdrucksbaumstrukturen, die Lambdaausdrücke darstellen, können kompiliert werden, um einen Delegaten zu erstellen, den Sie ausführen können. Ausdrucksbaumstrukturen bieten einen Mechanismus, um den durch eine Ausdrucksbaumstruktur dargestellten Code auszuführen.

Die Ausdrucksbaumstruktur stellt den Code dar, der für jedes angegebene Konstrukt ausgeführt werden würde, das Sie erstellen. Solange die Umgebung, in der Sie den Code kompilieren und ausführen, der Umgebung entspricht, in der Sie den Ausdruck erstellen, funktioniert alles wie erwartet. Wenn dies nicht der Fall ist, sind die Fehler vorhersehbar, und sie werden in den ersten Tests von Code mithilfe der Ausdrucksbaumstrukturen abgefangen.