.NET Framework
Drzewa ekspresji
Szukaj…
Uwagi
Drzewa wyrażeń to struktury danych używane do reprezentowania wyrażeń kodu w .NET Framework. Można je generować za pomocą kodu i programowo przeglądać w celu przetłumaczenia kodu na inny język lub wykonania. Najpopularniejszym generatorem drzew wyrażeń jest sam kompilator C #. Kompilator C # może generować drzewa wyrażeń, jeśli wyrażenie lambda jest przypisane do zmiennej typu Expression <Func <... >>. Zwykle dzieje się tak w kontekście LINQ. Najpopularniejszym konsumentem jest dostawca LINQ Entity Framework. Zużywa drzewa wyrażeń podane w Entity Framework i generuje równoważny kod SQL, który jest następnie wykonywany dla bazy danych.
Proste drzewo wyrażeń generowane przez kompilator C #
Rozważ następujący kod C #
Expression<Func<int, int>> expression = a => a + 1;
Ponieważ kompilator C # widzi, że wyrażenie lambda jest przypisane do typu wyrażenia, a nie do typu delegowanego, generuje drzewo wyrażeń w przybliżeniu równoważne z tym kodem
ParameterExpression parameterA = Expression.Parameter(typeof(int), "a");
var expression = (Expression<Func<int, int>>)Expression.Lambda(
Expression.Add(
parameterA,
Expression.Constant(1)),
parameterA);
Korzeń drzewa to wyrażenie lambda, które zawiera ciało i listę parametrów. Sonda lambda ma 1 parametr o nazwie „a”. Treść jest pojedynczym wyrażeniem typu CLR BinaryExpression i NodeType of Add. To wyrażenie reprezentuje dodanie. Ma dwa podwyrażenia oznaczone jako Lewy i Prawy. Lewy to ParameterExpression dla parametru „a”, a prawy to ConstantExpression o wartości 1.
Najprostszym zastosowaniem tego wyrażenia jest wydrukowanie go:
Console.WriteLine(expression); //prints a => (a + 1)
Który wypisuje równoważny kod C #.
Drzewo wyrażeń można skompilować w delegata C # i wykonać przez CLR
Func<int, int> lambda = expression.Compile();
Console.WriteLine(lambda(2)); //prints 3
Zazwyczaj wyrażenia są tłumaczone na inne języki, takie jak SQL, ale mogą być również używane do wywoływania prywatnych, chronionych i wewnętrznych członków typów publicznych lub niepublicznych jako alternatywy dla Reflection.
budowanie predykatu pola formularza == wartość
Aby zbudować wyrażenie takie jak _ => _.Field == "VALUE"
w czasie wykonywania.
Biorąc pod uwagę predykat _ => _.Field
i wartość ciągu "VALUE"
, utwórz wyrażenie, które sprawdza, czy predykat jest prawdziwy.
Wyrażenie jest odpowiednie dla:
-
IQueryable<T>
,IEnumerable<T>
do testowania predykatu. - framework encji lub
Linq
doSQL
aby utworzyć klauzulęWhere
, która testuje predykat.
Ta metoda zbuduje odpowiednie wyrażenie Equal
które sprawdza, czy Field
jest równe "VALUE"
.
public static Expression<Func<T, bool>> BuildEqualPredicate<T>(
Expression<Func<T, string>> memberAccessor,
string term)
{
var toString = Expression.Convert(Expression.Constant(term), typeof(string));
Expression expression = Expression.Equal(memberAccessor.Body, toString);
var predicate = Expression.Lambda<Func<T, bool>>(
expression,
memberAccessor.Parameters);
return predicate;
}
Predykat można wykorzystać, włączając predykat do metody rozszerzenia Where
.
var predicate = PredicateExtensions.BuildEqualPredicate<Entity>(
_ => _.Field,
"VALUE");
var results = context.Entity.Where(predicate).ToList();
Wyrażenie do pobierania pola statycznego
Mając taki przykład:
public TestClass
{
public static string StaticPublicField = "StaticPublicFieldValue";
}
Możemy pobrać wartość StaticPublicField:
var fieldExpr = Expression.Field(null, typeof(TestClass), "StaticPublicField");
var labmda = Expression.Lambda<Func<string>>(fieldExpr);
Następnie można go skompilować w delegata w celu pobrania wartości pola.
Func<string> retriever = lambda.Compile();
var fieldValue = retriever();
// wynikiem fieldValue jest StaticPublicFieldValue
InvocationExpression Class
Klasa InvocationExpression umożliwia wywoływanie innych wyrażeń lambda, które są częścią tego samego drzewa wyrażeń.
Tworzysz je za pomocą statycznej metody Expression.Invoke
.
Problem Chcemy dostać się do przedmiotów, które mają w opisie słowo „samochód”. Musimy sprawdzić, czy nie ma wartości null przed wyszukaniem ciągu wewnątrz, ale nie chcemy, aby był on wywoływany nadmiernie, ponieważ obliczenia mogą być kosztowne.
using System;
using System.Linq;
using System.Linq.Expressions;
public class Program
{
public static void Main()
{
var elements = new[] {
new Element { Description = "car" },
new Element { Description = "cargo" },
new Element { Description = "wheel" },
new Element { Description = null },
new Element { Description = "Madagascar" },
};
var elementIsInterestingExpression = CreateSearchPredicate(
searchTerm: "car",
whereToSearch: (Element e) => e.Description);
Console.WriteLine(elementIsInterestingExpression.ToString());
var elementIsInteresting = elementIsInterestingExpression.Compile();
var interestingElements = elements.Where(elementIsInteresting);
foreach (var e in interestingElements)
{
Console.WriteLine(e.Description);
}
var countExpensiveComputations = 0;
Action incCount = () => countExpensiveComputations++;
elements
.Where(
CreateSearchPredicate(
"car",
(Element e) => ExpensivelyComputed(
e, incCount
)
).Compile()
)
.Count();
Console.WriteLine("Property extractor is called {0} times.", countExpensiveComputations);
}
private class Element
{
public string Description { get; set; }
}
private static string ExpensivelyComputed(Element source, Action count)
{
count();
return source.Description;
}
private static Expression<Func<T, bool>> CreateSearchPredicate<T>(
string searchTerm,
Expression<Func<T, string>> whereToSearch)
{
var extracted = Expression.Parameter(typeof(string), "extracted");
Expression<Func<string, bool>> coalesceNullCheckWithSearch =
Expression.Lambda<Func<string, bool>>(
Expression.AndAlso(
Expression.Not(
Expression.Call(typeof(string), "IsNullOrEmpty", null, extracted)
),
Expression.Call(extracted, "Contains", null, Expression.Constant(searchTerm))
),
extracted);
var elementParameter = Expression.Parameter(typeof(T), "element");
return Expression.Lambda<Func<T, bool>>(
Expression.Invoke(
coalesceNullCheckWithSearch,
Expression.Invoke(whereToSearch, elementParameter)
),
elementParameter
);
}
}
Wynik
element => Invoke(extracted => (Not(IsNullOrEmpty(extracted)) AndAlso extracted.Contains("car")), Invoke(e => e.Description, element))
car
cargo
Madagascar
Predicate is called 5 times.
Pierwszą rzeczą, na którą należy zwrócić uwagę, jest faktyczny dostęp do właściwości, zawinięty w Invoke:
Invoke(e => e.Description, element)
, i jest to jedyna część, która dotyka e.Description
, a zamiast niego extracted
parametr typu string
jest przekazywany do następnej:
(Not(IsNullOrEmpty(extracted)) AndAlso extracted.Contains("car"))
Inną ważną rzeczą, na którą warto tutaj zwrócić uwagę, jest AndAlso
. Oblicza tylko lewą część, jeśli pierwsza część zwraca „fałsz”. Powszechnym błędem jest użycie zamiast tego operatora bitowego „I”, który zawsze oblicza obie części i kończy się niepowodzeniem z wyjątkiem NullReferenceException w tym przykładzie.