Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Drzewo wyrażeń to struktura danych, która reprezentuje jakiś kod. Nie jest to kod 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ń lambda. Drzewa wyrażeń reprezentujące wyrażenia lambda są typu LambdaExpression lub Expression<TDelegate>. Aby wykonać te drzewa wyrażeń, użyj metody Compile do utworzenia wykonywalnego delegata, a następnie wywołaj tego delegata.
Uwaga / Notatka
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.
Przekształcanie wyrażeń lambda w funkcje
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 System.Linq.Expressions.LambdaExpression, lub typem pochodzącym od klasy LambdaExpression
, można przekonwertować na IL. Typ wyrażenia System.Linq.Expressions.Expression<TDelegate> jest jedynym konkretnym przykładem w bibliotekach .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 sygnaturą wyrażenia lambda. Typ delegata jest oparty na typie wyrażenia. Musisz znać typ zwracany i listę argumentów, aby móc używać obiektu delegata w sposób silnie typizowany. Metoda LambdaExpression.Compile()
zwraca Delegate
typ. Musisz rzutować ją na właściwy typ delegata, aby narzędzia czasu kompilacji mogły 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>>
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 LambdaExpression.Compile i LambdaExpression.CompileToMethod 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 skompilowany wynik drzewa wyrażeń.
Ważne
CompileToMethod
jest dostępny tylko w .NET Framework, a nie w .NET Core lub .NET 5 i później.
Opcjonalnie możesz również podać element System.Runtime.CompilerServices.DebugInfoGenerator, który odbiera informacje o debugowaniu symboli dla wygenerowanego obiektu delegata. Element DebugInfoGenerator
zawiera pełne informacje o debugowaniu wygenerowanego delegata.
Możesz przekształcić wyrażenie w delegat za pomocą 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. Wyświetlany jest wynik, który reprezentuje 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, add.Compile()
, zwraca delegata. Ten delegat jest wywoływany przez wywołanie metody func()
, 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).
Ostrzeżenie
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()
, jest prawdopodobnie w pełni wykorzystywany podczas wykonywania kodu, który ustala, czy dwa różne drzewa wyrażeń prowadzą do tego samego kodu wykonywalnego.
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 zapewnić, że wszelkie zmienne, które będą częścią delegata, są dostępne w miejscu, w którym wywołujesz metodę Compile
oraz gdy wykonujesz wynikowego delegata. Kompilator zapewnia, że zmienne znajdują się w zakresie. Jednak jeśli wyrażenie uzyskuje dostęp do zmiennej, która implementuje IDisposable
, możliwe jest, że 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 IDisposable
elementu :
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 lokalnej zmiennej constant
. 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ą (raczej wymyśloną) klasę, która implementuje System.IDisposable:
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, uzyskasz System.ObjectDisposedException podczas wykonywania kodu, do którego odnosi się właściwość Resource.Argument
.
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ę dziwne, że błąd czasu wykonywania reprezentuje konstrukcję czasu kompilacji, ale taki jest świat, w który wkraczasz, gdy pracujesz 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. To zestawienie musi być dostępne, kiedy wyrażenie jest zdefiniowane, kiedy jest skompilowane, i kiedy wynikowy delegat jest wywołany. 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 uruchomić. 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ń.