.NET Framework
Arbres d'expression
Recherche…
Remarques
Les arbres d'expression sont des structures de données utilisées pour représenter des expressions de code dans .NET Framework. Ils peuvent être générés par code et parcourus par programme pour traduire le code dans une autre langue ou l'exécuter. Le générateur le plus populaire des arbres d'expression est le compilateur C # lui-même. Le compilateur C # peut générer des arborescences d'expression si une expression lambda est affectée à une variable de type Expression <Func <... >>. Habituellement, cela se produit dans le contexte de LINQ. Le consommateur le plus populaire est le fournisseur LINQ d’Entity Framework. Il consomme les arbres d'expression donnés à Entity Framework et génère un code SQL équivalent qui est ensuite exécuté sur la base de données.
Arbre d'expression simple généré par le compilateur C #
Considérons le code C # suivant
Expression<Func<int, int>> expression = a => a + 1;
Comme le compilateur C # voit que l'expression lambda est affectée à un type d'expression plutôt qu'à un type délégué, il génère un arbre d'expression à peu près équivalent à ce code
ParameterExpression parameterA = Expression.Parameter(typeof(int), "a");
var expression = (Expression<Func<int, int>>)Expression.Lambda(
Expression.Add(
parameterA,
Expression.Constant(1)),
parameterA);
La racine de l'arbre est l'expression lambda qui contient un corps et une liste de paramètres. Le lambda a 1 paramètre appelé "a". Le corps est une expression unique du type CLR BinaryExpression et NodeType of Add. Cette expression représente l'addition. Il a deux sous-expressions désignées par Gauche et Droite. Left est l'expression de paramètre pour le paramètre "a" et right est une expression constante avec la valeur 1.
L'utilisation la plus simple de cette expression consiste à l'imprimer:
Console.WriteLine(expression); //prints a => (a + 1)
Qui imprime le code C # équivalent.
L'arbre d'expression peut être compilé en un délégué C # et exécuté par le CLR
Func<int, int> lambda = expression.Compile();
Console.WriteLine(lambda(2)); //prints 3
Habituellement, les expressions sont traduites dans d'autres langages tels que SQL, mais peuvent également être utilisées pour invoquer des membres privés, protégés et internes de types publics ou non publics, en alternative à Reflection.
construire un prédicat de champ de formulaire == valeur
Pour construire une expression comme _ => _.Field == "VALUE"
à l'exécution.
Étant donné un prédicat _ => _.Field
et une valeur de chaîne "VALUE"
, créez une expression qui vérifie si le prédicat est vrai ou non.
L'expression convient pour:
-
IQueryable<T>
,IEnumerable<T>
pour tester le prédicat. - framework d'entité ou
Linq
toSQL
pour créer une clauseWhere
qui teste le prédicat.
Cette méthode va générer une expression Equal
appropriée qui teste si Field
est égal ou non à "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;
}
Le prédicat peut être utilisé en incluant le prédicat dans une méthode d'extension Where
.
var predicate = PredicateExtensions.BuildEqualPredicate<Entity>(
_ => _.Field,
"VALUE");
var results = context.Entity.Where(predicate).ToList();
Expression pour récupérer un champ statique
Ayant un exemple de type comme ceci:
public TestClass
{
public static string StaticPublicField = "StaticPublicFieldValue";
}
Nous pouvons récupérer la valeur de StaticPublicField:
var fieldExpr = Expression.Field(null, typeof(TestClass), "StaticPublicField");
var labmda = Expression.Lambda<Func<string>>(fieldExpr);
Il peut alors être compilé dans un délégué pour récupérer la valeur du champ.
Func<string> retriever = lambda.Compile();
var fieldValue = retriever();
// Le résultat de fieldValue est StaticPublicFieldValue
Classe InvocationExpression
La classe InvocationExpression permet l'invocation d'autres expressions lambda qui font partie du même arbre d'expression.
Vous les créez avec la méthode statique Expression.Invoke
.
Problème Nous voulons obtenir les éléments qui ont "voiture" dans leur description. Nous devons vérifier la valeur null avant de chercher une chaîne à l'intérieur, mais nous ne voulons pas qu'elle soit appelée de manière excessive, car le calcul pourrait être coûteux.
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
);
}
}
Sortie
element => Invoke(extracted => (Not(IsNullOrEmpty(extracted)) AndAlso extracted.Contains("car")), Invoke(e => e.Description, element))
car
cargo
Madagascar
Predicate is called 5 times.
La première chose à noter est la manière dont l’accès à la propriété, enveloppé dans un Invoke:
Invoke(e => e.Description, element)
, et c'est la seule partie qui touche e.Description
, et à la place, le paramètre extracted
de type string
est passé au suivant:
(Not(IsNullOrEmpty(extracted)) AndAlso extracted.Contains("car"))
Une autre chose importante à noter ici est AndAlso
. Il ne calcule que la partie gauche, si la première partie renvoie "false". C'est une erreur courante d'utiliser l'opérateur binaire «And» au lieu de cela, qui calcule toujours les deux parties, et qui échouerait avec une exception NullReferenceException dans cet exemple.