Wykonywanie drzew wyrażeń

Drzewo wyrażeń to struktura danych, która reprezentuje jakiś kod. Nie jest kompilowany i wykonywalny. Jeśli chcesz wykonać kod platformy .NET reprezentowany przez drzewo wyrażeń, musisz przekonwertować go na instrukcje pliku wykonywalnego IL. Wykonanie drzewa wyrażeń może zwrócić wartość lub po prostu wykonać akcję, taką jak wywołanie metody.

Można wykonywać tylko drzewa wyrażeń wyrażenia lambda. Drzewa wyrażeń reprezentujące wyrażenia lambda są typu LambdaExpression lub Expression<TDelegate>. Aby wykonać te drzewa wyrażeń, wywołaj Compile metodę w celu utworzenia delegata wykonywalnego, a następnie wywołaj delegata.

Uwaga

Jeśli typ delegata nie jest znany, oznacza to, że wyrażenie lambda jest typu LambdaExpression , a nie Expression<TDelegate>, wywołaj metodę DynamicInvoke na delegatu zamiast wywołać go bezpośrednio.

Jeśli drzewo wyrażeń nie reprezentuje wyrażenia lambda, możesz utworzyć nowe wyrażenie lambda, które ma oryginalne drzewo wyrażeń jako jego treść, wywołując metodę Lambda<TDelegate>(Expression, IEnumerable<ParameterExpression>) . Następnie możesz wykonać wyrażenie lambda zgodnie z opisem we wcześniejszej części tej sekcji.

Wyrażenia lambda do funkcji

Możesz przekonwertować dowolną lambdaExpression lub dowolny typ pochodzący z lambdaExpression na plik wykonywalny IL. Inne typy wyrażeń nie mogą być bezpośrednio konwertowane na kod. To ograniczenie ma niewielki wpływ w praktyce. Wyrażenia lambda są jedynymi typami wyrażeń, które chcesz wykonać, konwertując na wykonywalny język pośredni (IL). (Pomyśl o tym, co oznaczałoby bezpośrednie wykonanie elementu System.Linq.Expressions.ConstantExpression. Czy oznaczałoby to coś przydatnego?) Każde drzewo wyrażeń, które jest typem pochodzącym System.Linq.Expressions.LambdaExpressionz LambdaExpression klasy , można przekonwertować na il. Typ System.Linq.Expressions.Expression<TDelegate> wyrażenia jest jedynym konkretnym przykładem w bibliotekach platformy .NET Core. Służy do reprezentowania wyrażenia mapowania na dowolny typ delegata. Ponieważ ten typ jest mapowany na typ delegata, platforma .NET może zbadać wyrażenie i wygenerować il dla odpowiedniego delegata zgodnego z podpisem wyrażenia lambda. Typ delegata jest oparty na typie wyrażenia. Musisz znać typ zwracany i listę argumentów, jeśli chcesz używać obiektu delegata w sposób silnie typizowane. Metoda LambdaExpression.Compile() zwraca Delegate typ. Musisz rzutować go do poprawnego typu delegata, aby mieć narzędzia czasu kompilacji sprawdzić listę argumentów lub typ zwracany.

W większości przypadków istnieje proste mapowanie między wyrażeniem a odpowiadającym mu delegatem. Na przykład drzewo wyrażeń reprezentowane przez Expression<Func<int>> element zostanie przekonwertowane na delegata typu Func<int>. W przypadku wyrażenia lambda z dowolnym typem zwrotnym i listą argumentów istnieje typ delegata, który jest typem docelowym kodu wykonywalnego reprezentowanego przez to wyrażenie lambda.

Typ System.Linq.Expressions.LambdaExpression zawiera elementy i LambdaExpression.CompileToMethodLambdaExpression.Compile elementy członkowskie, których można użyć do przekonwertowania drzewa wyrażeń na kod wykonywalny. Metoda Compile tworzy delegata. Metoda CompileToMethod aktualizuje System.Reflection.Emit.MethodBuilder obiekt za pomocą il, który reprezentuje skompilowane dane wyjściowe drzewa wyrażeń.

Ważne

CompileToMethod program jest dostępny tylko w programie .NET Framework, a nie w programie .NET Core lub .NET 5 lub nowszym.

Opcjonalnie możesz również podać element System.Runtime.CompilerServices.DebugInfoGenerator , który odbiera informacje debugowania symbolu dla wygenerowanego obiektu delegata. Element DebugInfoGenerator zawiera pełne informacje o debugowaniu wygenerowanego delegata.

Wyrażenie można przekonwertować na delegata przy użyciu następującego kodu:

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

Poniższy przykład kodu przedstawia konkretne typy używane podczas kompilowania i wykonywania drzewa wyrażeń.

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.

W poniższym przykładzie kodu pokazano, jak wykonać drzewo wyrażeń reprezentujące podniesienie liczby do potęgi przez utworzenie wyrażenia lambda i wykonanie go. Zostanie wyświetlony wynik reprezentujący liczbę podniesioną do potęgi.

// 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

Wykonywanie i okresy istnienia

Kod jest wykonywany przez wywołanie delegata utworzonego podczas wywoływania polecenia LambdaExpression.Compile(). Powyższy kod zwraca add.Compile()delegata. Ten delegat jest wywoływany przez wywołanie func()metody , która wykonuje kod.

Ten delegat reprezentuje kod w drzewie wyrażeń. Możesz zachować dojście do tego delegata i wywołać go później. Nie musisz kompilować drzewa wyrażeń za każdym razem, gdy chcesz wykonać kod, który reprezentuje. (Pamiętaj, że drzewa wyrażeń są niezmienne, a kompilowanie tego samego drzewa wyrażeń później powoduje utworzenie delegata wykonującego ten sam kod).

Uwaga

Nie twórz bardziej zaawansowanych mechanizmów buforowania, aby zwiększyć wydajność, unikając niepotrzebnych wywołań kompilacji. Porównanie dwóch dowolnych drzew wyrażeń w celu określenia, czy reprezentują ten sam algorytm, jest czasochłonną operacją. Czas obliczeniowy, który można zaoszczędzić, unikając dodatkowych wywołań LambdaExpression.Compile() , są prawdopodobnie bardziej niż używane przez czas wykonywania kodu, który określa, czy dwa różne drzewa wyrażeń powodują ten sam kod wykonywalny.

Zastrzeżenia

Kompilowanie wyrażenia lambda do delegata i wywoływanie tego delegata jest jedną z najprostszych operacji, które można wykonać za pomocą drzewa wyrażeń. Jednak nawet w przypadku tej prostej operacji istnieją zastrzeżenia, o których musisz pamiętać.

Wyrażenia lambda tworzą zamknięcia wszystkich zmiennych lokalnych, które są przywoływane w wyrażeniu. Należy zagwarantować, że wszystkie zmienne, które będą częścią delegata, będą używane w lokalizacji, w której wywołujesz Compilemetodę , i podczas wykonywania wynikowego delegata. Kompilator zapewnia, że zmienne znajdują się w zakresie. Jeśli jednak wyrażenie uzyskuje dostęp do zmiennej, która implementuje IDisposableelement , kod może usunąć obiekt, gdy jest on nadal przechowywany przez drzewo wyrażeń.

Na przykład ten kod działa poprawnie, ponieważ int nie implementuje IDisposableelementu :

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;
}

Delegat przechwycił odwołanie do zmiennej constantlokalnej . Ta zmienna jest dostępna w dowolnym momencie później, gdy funkcja zwracana przez CreateBoundFunc funkcję jest wykonywana.

Należy jednak wziąć pod uwagę następującą klasę (raczej contrived), która implementuje System.IDisposableelement :

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;
    }
}

Jeśli używasz go w wyrażeniu, jak pokazano w poniższym kodzie, po System.ObjectDisposedException wykonaniu Resource.Argument kodu, do którego odwołuje się właściwość :

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;
    }
}

Delegat zwrócony z tej metody został zamknięty nad obiektem constant , który został usunięty. (Został usunięty, ponieważ został zadeklarowany w oświadczeniu using ).

Teraz po wykonaniu delegata zwróconego z tej metody otrzymasz zgłoszenie ObjectDisposedException w momencie wykonywania.

Wydaje się, że podczas pracy z drzewami wyrażeń występuje błąd czasu wykonywania reprezentujący konstrukcję czasu kompilacji, ale to jest świat, który wprowadzasz podczas pracy z drzewami wyrażeń.

Istnieje wiele permutacji tego problemu, więc trudno jest zaoferować ogólne wskazówki, aby go uniknąć. Należy zachować ostrożność podczas uzyskiwania dostępu do zmiennych lokalnych podczas definiowania wyrażeń i zachować ostrożność podczas uzyskiwania dostępu do stanu w bieżącym obiekcie (reprezentowanym przez this) podczas tworzenia drzewa wyrażeń zwracanego za pośrednictwem publicznego interfejsu API.

Kod w wyrażeniu może odwoływać się do metod lub właściwości w innych zestawach. Ten zestaw musi być dostępny po zdefiniowaniu wyrażenia, skompilowaniu i wywołaniu wynikowego delegata. Spotkasz się z elementem ReferencedAssemblyNotFoundException w przypadkach, w których nie jest obecny.

Podsumowanie

Drzewa wyrażeń reprezentujące wyrażenia lambda można skompilować, aby utworzyć delegata, który można wykonać. Drzewa wyrażeń zapewniają jeden mechanizm wykonywania kodu reprezentowanego przez drzewo wyrażeń.

Drzewo wyrażeń reprezentuje kod, który będzie wykonywany dla każdej utworzonej konstrukcji. Jeśli środowisko, w którym kompilujesz i wykonujesz kod, jest zgodne ze środowiskiem, w którym tworzysz wyrażenie, wszystko działa zgodnie z oczekiwaniami. Gdy tak się nie stanie, błędy są przewidywalne i są przechwytywane w pierwszych testach dowolnego kodu przy użyciu drzew wyrażeń.