C# Language
Drzewa ekspresji
Szukaj…
Wprowadzenie
Składnia
- Wyrażenie <TDelegate> name = lambdaExpression;
Parametry
Parametr | Detale |
---|---|
TDelegate | Typ delegata, który ma zostać użyty do wyrażenia |
lambdaExpression | Wyrażenie lambda (np. num => num < 5 ) |
Uwagi
Wprowadzenie do drzew wyrażeń
Skąd pochodzimy
W drzewach wyrażeń chodzi o zużycie „kodu źródłowego” w czasie wykonywania. Rozważ metodę, która oblicza podatek należny od decimal CalculateTotalTaxDue(SalesOrder order)
zamówienia sprzedaży decimal CalculateTotalTaxDue(SalesOrder order)
. Korzystanie z tej metody w programie .NET jest łatwe - wystarczy nazwać ją decimal taxDue = CalculateTotalTaxDue(order);
. Co jeśli chcesz zastosować go do wszystkich wyników ze zdalnego zapytania (SQL, XML, zdalny serwer itp.)? Te zdalne źródła zapytań nie mogą wywoływać metody! Tradycyjnie we wszystkich tych przypadkach trzeba odwrócić przepływ. Wykonaj całe zapytanie, zapisz je w pamięci, a następnie przejrzyj wyniki i oblicz podatek dla każdego wyniku.
Jak uniknąć problemów z pamięcią i opóźnieniami inwersji przepływu
Drzewa wyrażeń to struktury danych w formacie drzewa, w których każdy węzeł zawiera wyrażenie. Służą do tłumaczenia skompilowanych instrukcji (takich jak metody stosowane do filtrowania danych) w wyrażeniach, które mogłyby być używane poza środowiskiem programu, np. W zapytaniu do bazy danych.
Problem polega na tym, że zdalne zapytanie nie może uzyskać dostępu do naszej metody . Moglibyśmy uniknąć tego problemu, gdyby zamiast tego wysłaliśmy instrukcje dotyczące metody do zdalnego zapytania. W naszym przykładzie CalculateTotalTaxDue
oznacza to, że wysyłamy te informacje:
- Utwórz zmienną do przechowywania całkowitego podatku
- Pętlę przez wszystkie linie w zamówieniu
- Dla każdej linii sprawdź, czy produkt podlega opodatkowaniu
- Jeśli tak, pomnóż sumę wiersza przez obowiązującą stawkę podatkową i dodaj tę kwotę do sumy
- W przeciwnym razie nic nie rób
Dzięki tym instrukcjom zdalne zapytanie może wykonać pracę podczas tworzenia danych.
Istnieją dwa wyzwania związane z wdrożeniem tego. Jak przekształcić skompilowaną metodę .NET w listę instrukcji i jak sformatować instrukcje w taki sposób, aby mogły zostać wykorzystane przez system zdalny?
Bez drzew wyrażeń można rozwiązać tylko pierwszy problem z MSIL. (MSIL to kod podobny do asemblera stworzony przez kompilator .NET.) Analiza MSIL jest możliwa , ale nie jest łatwa. Nawet jeśli poprawnie go przeanalizujesz, może być trudno ustalić, jakie były intencje oryginalnego programisty w odniesieniu do określonej procedury.
Drzewa ekspresji ratują dzień
Drzewa wyrażeń rozwiązują dokładnie te problemy. Reprezentują one instrukcje programu, strukturę drzewa, w której każdy węzeł reprezentuje jedną instrukcję i zawiera odniesienia do wszystkich informacji potrzebnych do wykonania tej instrukcji. Na przykładMethodCallExpression
ma odniesienie do 1) metody MethodInfo
którą zamierza wywołać, 2) listy Expression
, które przekaże tej metodzie, 3) na przykład metod, Expression
którym wywoła się metodę. Możesz „przejść się po drzewie” i zastosować instrukcje do zdalnego zapytania. Tworzenie drzewek ekspresyjnych
Najłatwiejszym sposobem utworzenia drzewa wyrażeń jest wyrażenie lambda. Te wyrażenia wyglądają prawie tak samo jak normalne metody C #. Ważne jest, aby zdać sobie sprawę, że to magia kompilatora . Kiedy po raz pierwszy tworzysz wyrażenie lambda, kompilator sprawdza, do czego go przypisujesz. Jeśli jest to typDelegate
(w tym Action
lub Func
), kompilator konwertuje wyrażenie lambda na delegata. Jeśli jest to LambdaExpression
(lub Expression<Action<T>>
lub Expression<Func<T>>
które są silnie typowanymi LambdaExpression
), kompilator przekształca je w LambdaExpression
. W tym miejscu zaczyna się magia. Za kulisami kompilator używa interfejsu API drzewa wyrażeń do przekształcenia wyrażenia lambda w wyrażenie LambdaExpression
. Wyrażenia lambda nie mogą tworzyć każdego typu drzewa wyrażeń. W takich przypadkach możesz ręcznie użyć interfejsu API wyrażeń, aby utworzyć potrzebne drzewo. W przykładzie interfejsu API wyrażeń Understanding , tworzymy wyrażenie CalculateTotalSalesTax
przy użyciu interfejsu API.
UWAGA: Nazwy są tu nieco mylące. Wyrażenie lambda (dwa słowa, małe litery) odnosi się do bloku kodu ze wskaźnikiem =>
. Reprezentuje anonimową metodę w C # i jest konwertowany na Delegate
lub Expression
. LambdaExpression
(jedno słowo, PascalCase) odnosi się do typu węzła w Expression API, który reprezentuje metodę, którą można wykonać.
Drzewa ekspresji i LINQ
Jednym z najczęstszych zastosowań drzew wyrażeń jest LINQ i zapytania do baz danych. LINQ łączy drzewo wyrażeń z dostawcą zapytań, aby zastosować instrukcje do docelowego zdalnego zapytania. Na przykład dostawca zapytań LINQ do Entity Framework przekształca drzewo wyrażeń w SQL, które jest wykonywane bezpośrednio na bazie danych.
Łącząc wszystkie elementy, możesz zobaczyć prawdziwą moc LINQ.
- Napisz zapytanie, używając wyrażenia lambda:
products.Where(x => x.Cost > 5)
- Kompilator przekształca to wyrażenie w drzewo wyrażeń za pomocą instrukcji „sprawdź, czy właściwość Cost parametru jest większa niż pięć”.
- Dostawca zapytań analizuje drzewo wyrażeń i generuje prawidłowe zapytanie SQL
SELECT * FROM products WHERE Cost > 5
- ORM wyświetla wszystkie wyniki w POCO i odzyskujesz listę obiektów
Notatki
- Drzewa ekspresji są niezmienne. Jeśli chcesz zmienić drzewo wyrażeń, musisz utworzyć nowe, skopiować istniejące do nowego (aby przejść przez drzewo wyrażeń, możesz użyć
ExpressionVisitor
) i wprowadzić pożądane zmiany.
Tworzenie drzew wyrażeń za pomocą interfejsu API
using System.Linq.Expressions;
// Manually build the expression tree for
// the lambda expression num => num < 5.
ParameterExpression numParam = Expression.Parameter(typeof(int), "num");
ConstantExpression five = Expression.Constant(5, typeof(int));
BinaryExpression numLessThanFive = Expression.LessThan(numParam, five);
Expression<Func<int, bool>> lambda1 =
Expression.Lambda<Func<int, bool>>(
numLessThanFive,
new ParameterExpression[] { numParam });
Kompilowanie drzew wyrażeń
// Define an expression tree, taking an integer, returning a bool.
Expression<Func<int, bool>> expr = num => num < 5;
// Call the Compile method on the expression tree to return a delegate that can be called.
Func<int, bool> result = expr.Compile();
// Invoke the delegate and write the result to the console.
Console.WriteLine(result(4)); // Prints true
// Prints True.
// You can also combine the compile step with the call/invoke step as below:
Console.WriteLine(expr.Compile()(4));
Analizowanie drzew wyrażeń
using System.Linq.Expressions;
// Create an expression tree.
Expression<Func<int, bool>> exprTree = num => num < 5;
// Decompose the expression tree.
ParameterExpression param = (ParameterExpression)exprTree.Parameters[0];
BinaryExpression operation = (BinaryExpression)exprTree.Body;
ParameterExpression left = (ParameterExpression)operation.Left;
ConstantExpression right = (ConstantExpression)operation.Right;
Console.WriteLine("Decomposed expression: {0} => {1} {2} {3}",
param.Name, left.Name, operation.NodeType, right.Value);
// Decomposed expression: num => num LessThan 5
Utwórz drzewa wyrażeń z wyrażeniem lambda
Poniżej znajduje się najbardziej podstawowe drzewo wyrażeń tworzone przez lambda.
Expression<Func<int, bool>> lambda = num => num == 42;
Aby ręcznie stworzyć drzewa wyrażeń, należy użyć klasy Expression
.
Wyrażenie powyżej byłoby równoważne z:
ParameterExpression parameter = Expression.Parameter(typeof(int), "num"); // num argument
ConstantExpression constant = Expression.Constant(42, typeof(int)); // 42 constant
BinaryExpression equality = Expression.Equals(parameter, constant); // equality of two expressions (num == 42)
Expression<Func<int, bool>> lambda = Expression.Lambda<Func<int, bool>>(equality, parameter);
Zrozumienie API wyrażeń
Użyjemy interfejsu API drzewa wyrażeń, aby utworzyć drzewo CalculateSalesTax
. Mówiąc wprost, oto podsumowanie kroków, które należy wykonać, aby utworzyć drzewo.
- Sprawdź, czy produkt podlega opodatkowaniu
- Jeśli tak, pomnóż sumę wiersza przez obowiązującą stawkę podatkową i zwróć tę kwotę
- W przeciwnym razie zwróć 0
//For reference, we're using the API to build this lambda expression
orderLine => orderLine.IsTaxable ? orderLine.Total * orderLine.Order.TaxRate : 0;
//The orderLine parameter we pass in to the method. We specify it's type (OrderLine) and the name of the parameter.
ParameterExpression orderLine = Expression.Parameter(typeof(OrderLine), "orderLine");
//Check if the parameter is taxable; First we need to access the is taxable property, then check if it's true
PropertyInfo isTaxableAccessor = typeof(OrderLine).GetProperty("IsTaxable");
MemberExpression getIsTaxable = Expression.MakeMemberAccess(orderLine, isTaxableAccessor);
UnaryExpression isLineTaxable = Expression.IsTrue(getIsTaxable);
//Before creating the if, we need to create the braches
//If the line is taxable, we'll return the total times the tax rate; get the total and tax rate, then multiply
//Get the total
PropertyInfo totalAccessor = typeof(OrderLine).GetProperty("Total");
MemberExpression getTotal = Expression.MakeMemberAccess(orderLine, totalAccessor);
//Get the order
PropertyInfo orderAccessor = typeof(OrderLine).GetProperty("Order");
MemberExpression getOrder = Expression.MakeMemberAccess(orderLine, orderAccessor);
//Get the tax rate - notice that we pass the getOrder expression directly to the member access
PropertyInfo taxRateAccessor = typeof(Order).GetProperty("TaxRate");
MemberExpression getTaxRate = Expression.MakeMemberAccess(getOrder, taxRateAccessor);
//Multiply the two - notice we pass the two operand expressions directly to multiply
BinaryExpression multiplyTotalByRate = Expression.Multiply(getTotal, getTaxRate);
//If the line is not taxable, we'll return a constant value - 0.0 (decimal)
ConstantExpression zero = Expression.Constant(0M);
//Create the actual if check and branches
ConditionalExpression ifTaxableTernary = Expression.Condition(isLineTaxable, multiplyTotalByRate, zero);
//Wrap the whole thing up in a "method" - a LambdaExpression
Expression<Func<OrderLine, decimal>> method = Expression.Lambda<Func<OrderLine, decimal>>(ifTaxableTernary, orderLine);
Podstawowe drzewo wyrażeń
Drzewa wyrażeń reprezentują kod w drzewiastej strukturze danych, gdzie każdy węzeł jest wyrażeniem
Drzewa wyrażeń umożliwiają dynamiczną modyfikację kodu wykonywalnego, wykonywanie zapytań LINQ w różnych bazach danych oraz tworzenie zapytań dynamicznych. Możesz skompilować i uruchomić kod reprezentowany przez drzewa wyrażeń.
Są one również używane w czasie wykonywania dynamicznego języka (DLR) w celu zapewnienia interoperacyjności między językami dynamicznymi a .NET Framework oraz umożliwienia autorom kompilatorów emitowania drzew wyrażeń zamiast języka pośredniego Microsoft (MSIL).
Drzewa wyrażeń można tworzyć za pośrednictwem
- Anonimowe wyrażenie lambda,
- Ręcznie, używając przestrzeni nazw System.Linq.Expressions.
Drzewa wyrażeń z wyrażeń lambda
Gdy wyrażenie lambda jest przypisane do zmiennej typu Expression, kompilator emituje kod, aby zbudować drzewo wyrażeń reprezentujące wyrażenie lambda.
Poniższe przykłady kodu pokazują, w jaki sposób kompilator C # może utworzyć drzewo wyrażeń reprezentujące wyrażenie lambda num => num <5.
Expression<Func<int, bool>> lambda = num => num < 5;
Drzewa wyrażeń za pomocą interfejsu API
Drzewa wyrażeń utworzono również za pomocą klasy wyrażeń . Ta klasa zawiera statyczne metody fabryczne, które tworzą węzły drzewa wyrażeń określonych typów.
Poniżej kilka rodzajów węzłów drzewa.
- Wyrażenie parametru
- MethodCallExpression
Poniższy przykład kodu pokazuje, jak utworzyć drzewo wyrażeń reprezentujące wyrażenie lambda num => num <5 przy użyciu interfejsu API.
ParameterExpression numParam = Expression.Parameter(typeof(int), "num");
ConstantExpression five = Expression.Constant(5, typeof(int));
BinaryExpression numLessThanFive = Expression.LessThan(numParam, five);
Expression<Func<int, bool>> lambda1 = Expression.Lambda<Func<int, bool>>(numLessThanFive,new ParameterExpression[] { numParam });
Badanie struktury wyrażenia za pomocą narzędzia Visitor
Zdefiniuj nową klasę gości, zastępując niektóre metody ExpressionVisitor :
class PrintingVisitor : ExpressionVisitor {
protected override Expression VisitConstant(ConstantExpression node) {
Console.WriteLine("Constant: {0}", node);
return base.VisitConstant(node);
}
protected override Expression VisitParameter(ParameterExpression node) {
Console.WriteLine("Parameter: {0}", node);
return base.VisitParameter(node);
}
protected override Expression VisitBinary(BinaryExpression node) {
Console.WriteLine("Binary with operator {0}", node.NodeType);
return base.VisitBinary(node);
}
}
Zadzwoń do Visit
aby użyć tego użytkownika na istniejącym wyrażeniu:
Expression<Func<int,bool>> isBig = a => a > 1000000;
var visitor = new PrintingVisitor();
visitor.Visit(isBig);