Quelques définitions en préambule
Avant de rentrer dans les détails des arbres d’expression (i.e. expression trees), il faut définir quelques termes.
Delegate
Il s’agit du type d’une référence vers une méthode comportant une signature particulière. Le delegate définit donc le type de la référence et non pas la référence elle-même. Par exemple, en C# un delegate peut se définir de cette façon:
public delegate int AddDelegate(int a, int b);
La méthode suivante possède une définition compatible avec le delegate en raison de sa signature:
public static int Add(int a, int b)
{
return a + b;
}
On peut donc instancier le delegate et l’exécuter de cette façon:
AddDelegate delegateInstance = Add;
int result = delegateInstance(3, 5);
A partir du C# 2.0, il est possible d’avoir une notation plus directe pour définir les delegates:
AddDelegate delegateInstance = delegate(int a, int b)
{
return a + b;
};
Expression lambda
Une expression lambda est une notation permettant de créer des types de delegates ou d’arbres d’expression. Les expressions lambda utilise une notation utilisant l’opérateur “=>”.
Si on prends l’exemple précédent, on peut utiliser une expression lambda pour définir le delegate:
AddDelegate delWithLambda = (a, b) => a + b;
Cette notation est un raccourci pour:
AddDelegate delWithLambda = (a, b) => { return a + b; };
Le delegate s’exécute de la même façon que précédemment:
int result = delWithLambda(3, 5);
Les expressions lambda sont apparues avec C# 3.0.
Action<T> et Func<T, TResult>
Il s’agit de delegates prédéfinis pour faciliter l’utilisation de delegates et d’expression lambda. L’inconvénient de l’exemple précédent est qu’il nécessite la définition du delegate AddDelegate
:
public delegate int AddDelegate(int a, int b);
Pour éviter de définir des delegates avant d’utiliser des expressions lambda, on peut utiliser Action
et Func
:
Action<T>
correspond à des delegates de méthodes (par de type de retour) de 0 ou plusieurs arguments.Func<T, TResult>
correspond à des delegates de fonctions de 0 ou plusieurs arguments avec un résultat.
Dans l’exemple précédent, si on utilise Func<T, TResult>:
Func<int, int, int> addWithFunc = (a, b) => a + b;
Une autre notation est équivalente (peu utilisée car plus lourde):
Func<int, int, int> addWithFunc = delegate(a, b) { return a + b; };
Les types Action<T>
et Func<T, TResult>
sont apparus avec le framework .NET 3.5.
Expression
En C#, le type Expression désigne un objet permettant de représenter une expression lambda sous la forme d’un arbre d’expressions (i.e. expression tree). Ce type se trouve dans le namespace System.Linq.Expressions
, il s’utilise sous la forme:
Expression<Func<TResult>>
ou Expression<TDelegate>
où TDelegate
est un delegate définit au préalable.
Ainsi Expression<Func<TResult>>
correspond à la représentation fortement typée d’une expression lambda, elle ne contient pas seulement sa définition mais aussi toute sa description.
Expression<TDelegate>
dérive de la classe abstraite System.Linq.Expressions.LambdaExpression
qui correspond à la classe de base pour représenter une expression lambda sous forme d’un arbre d’expressions:
public sealed class Expression<TDelegate> : LambdaExpression
Expression<TDelegate>
et Expression
Il ne faut pas confondre les classes Expression<TDelegate>
et Expression
car Expression<TDelegate>
dérive de LambdaExpression
et LambdaExpression
dérive de Expression
.
Intérêt de Expression<Func<TResult>> par rapport à Func<TResult>
L’intérêt de ce type est de pouvoir intervenir sur cette description pour l’utiliser pour comprendre ce que contient l’expression, la modifier ou simplement l’exécuter. A la différence, un delegate Func<TResult>
peut seulement être exécuté. Par exemple, il sera plus compliqué de modifier l’implémentation d’un delegate Func<TResult>
au runtime alors qu’il est possible de la faire avec Expression<Func<TResult>>
.
Il n’est pas facile de comprendre la différence entre Expression<Func<TResult>>
et Func<TResult>
:
Func<TResult>
désigne un delegate prédéfini correspondant à un fonction sans arguments et retournant un paramètre de type TResult.Expression<Func<TResult>>
désigne toute la description du delegateFunc<TResult>
.
Par exemple, si on considère l’expression:
Expression<Func<int, int, int>> expression = (a, b) => a + b;
On peut voir que le contenu de l’objet expression par rapport à un Func équivalente.
Par exemple le contenu de:
Func<int, int, int> func = (a, b) => a + b;
est:
On peut voir que func
est bien un delegate, il n’y a plus de traces de l’implémentation d’origine: (a, b) => a + b
.
Le contenu de l’objet expression est:
Dans cet objet, il y a tout le détail de l’implémentation de l’expression lambda (a, b) => a + b
.
Ainsi, Expression<Func<TResult>>
décrit ce que fait l’expression lambda alors que Func<TResult>
permet seulement de l’exécuter.
Manipulation des Expression<Func<TResult>>
Construction
Il est possible de construire un arbre d’expressions de 2 façons: avec une expression lambda ou avec l’API Expressions.
Par exemple, en utilisant une expression lambda:
Expression<Func<int, int, int>> expression = (a, b) => a + b;
De même en utilisant l’API:
Using System.Linq.Expressions;
// ...
// "a" ne sert que pour la documentation
ParameterExpression aParam = Expression.Parameter(typeof(int), "a");
ParameterExpression bParam = Expression.Parameter(typeof(int), "b");
BinaryExpression add = Expression.Add(aParam, bParam);
Expression<Func<int, int, int>> expression =
Expression.Lambda<Func<int, int, int>>(add, new ParameterExpression[] { aParam, bParam });
L’API permet d’implémenter un grand nombre de fonctions avec les fonctions statiques de la classe System.Linq.Expressions.Expression
.
Par exemple:
Fonction | Permet de définir |
---|---|
Expression.Parameter() |
Un argument |
Expression.Constant() |
Une constante |
Expression.Assign() |
Affecter une expression à un argument |
Expression.Add() |
Des opérations sur les arguments |
Expression.Label() |
Un label qui va permettre d’effectuer des sauts dans le code (GOTO) |
Expression.Goto() |
Un saut dans le code (GOTO) |
Expression.LessThan() |
Des opérations de comparaison sur les arguments |
Expression.Lambda() |
Permet de construire un delegate |
Expression.Block() |
Permet de construire le corps d’une méthode |
Expression.Loop() |
Permet de définir une boucle de programmation. Par exemple:Expression.Loop([Expression contenant le code à évaluer], Expression.Break(...)) |
Expression.Break() |
Arrête une boucle Loop et effectue un saut à un “Label” particulier. |
Expression.IfThen() |
Permet de créer une expression conditionnelle |
Expression.IsFalse() |
Evalue une expression |
Expression.New() |
Crée une expression permettant d’effectuer un appel à un constructeur. |
Expression.TryCatch() |
Un bloc try…catch…finally |
Etc… |
La liste exhaustive des fonctions se trouve sur MSDN.
Convertir un arbre d’expression en delegate (Expression<Func> en Func)
On peut convertir un objet Expression<Func<TResult>>
en Func<TResult>
avec la méthode Expression.Compile()
.
Par exemple:
Expression<Func<int, int, int>> expression = (a, b) => a + b;
Func<int, int, int> funcFromExpression = expression.Compile();
On peut évaluer le delegate avec:
int result = funcFromExpression(3, 9);
Un delegate est une méthode compilée, pour obtenir l’arbre d’expressions correspondant à ce delegate, il faudrait décompiler la méthode et la convertir en arbre d’expressions. Il n’y a pas de méthodes directes pour effectuer ces étapes.
Convertir un delegate en arbre d’expression (Func en Expression<Func>)
On ne peut pas convertir un delegate Func<TResult>
en Expression<Func<TResult>>
car les éléments définissant l’arbre d’expressions sont perdus lorsqu’on compile le delegate en faisant Expression.Compile()
.
On peut toutefois construire une expression qui appelle un delegate en faisant:
Func<int, int, int> func = (a, b) => a + b;
Expression<Func<int>> expressionCallingFunc = Expression.Lambda<Func<int, int, int>>(
Expression.Call(func.Method));
L’expression obtenue ne fait qu’appeler un delegate. Si le delegate a été obtenu à partir d’une expression avec Expression.Compile()
, on ne peut pas retrouver de cette façon l’expression d’origine.
Analyser un arbre d’expressions
Comme indiquer un des intérêts des arbres d’expressions est de pouvoir analyser une expression lambda avec des instructions. On peut ainsi décomposer l’expression lambda en utilisant les propriétés de la classe System.Linq.Expressions.LambdaExpression
(la classe Expression<TDelegate>
dérive de la classe LambdaExpression
):
LambdaExpression.Body
: pour obtenir une expression contenant le corps de l’expression lambda.LambdaExpression.Parameters
: pour obtenir les arguments.
Par exemple:
Expression<Func<int, int, int>> expression = (a, b) => a + b;
BinaryExpression addOperation = (BinaryExpression)expression.Body;
ParameterExpression aParam = (ParameterExpression)addOperation.Left;
ParameterExpression bParam = (ParameterExpression)addOperation.Right;
Manipuler un arbre d’expression avec ExpressionVisitor
Il est possible d’explorer et d’analyser un arbre d’expressions plus facilement qu’en utiliser des propriétés avec un objet de type ExpressionVisitor
. Cet objet utilise le design pattern Visiteur.
Pour utiliser cette méthode, il faut dériver de la classe abstraite System.Linq.Expressions.ExpressionVisitor
et surcharger les fonctions virtuelles correspondant aux parties de l’expression qu’on souhaite analyser.
Par exemple:
ExpressionVisitor.VisitBinary()
: sera exécutée pour les nœuds de l’arbre qui sont des opérations (i.e.BinaryExpression
).ExpressionVisitor.VisitParameter()
: sera exécutée pour les arguments d’une expression (i.e.ParameterExpression
).ExpressionVisitor.VisitBlock()
: sera exécutée pour les nœuds correspondant au corps d’une fonction (i.e.BlockExpression
).- Etc…
Se reporter à MSDN pour avoir la liste exhaustive de toutes les méthodes virtuelles.
Pour que les méthodes de la classe soient exécutées, il faut appeler la méthode ExpressionVisitor.Visit()
.
Par exemple si on dérive de ExpressionVisitor
:
public class CustomExpressionVisitor : ExpressionVisitor
{
protected override Expression VisitBinary(BinaryExpression node)
{
// ...
return node;
}
protected override Expression VisitParameter(ParameterExpression node)
{
// ...
return base.VisitParameter(node);
}
}
Les fonctions surchargées seront exécutées en faisant:
Expression<Func<int, int, int>> addExpression = (a, b) => a + b;
var expressionVisitor = new CustomExpressionVisitor();
expressionVisitor.Visit(addExpression.Body);
L’exécution des méthodes se fera dans l’ordre des expressions dans l’arbre d’expressions.
Par exemple, si on définit le “visiteur”:
public class CustomExpressionVisitor : ExpressionVisitor
{
protected override Expression VisitBinary(BinaryExpression node)
{
Console.Write("(");
this.Visit(node.Left);
Console.Write(" {0} ", node.NodeType);
this.Visit(node.Right);
Console.Write(")");
return node;
}
protected override Expression VisitParameter(ParameterExpression node)
{
Console.Write("parameter({0})", node.Name);
return base.VisitParameter(node);
}
}
A l’exécution, on aura:
((parameter(a) Add (parameter(b))
Arbre d’expressions immutable
Les arbres d’expressions sont immutables c’est-à-dire qu’il n’est pas possible de le modifier après sa création. Toutefois pour profiter de l’intérêt des arbres, on peut utiliser l’API pour créer des arbres différents suivant les besoins ou utiliser un “visiteur” ExpressionVisitor
pour dupliquer un arbre et effectuer des modifications pendant cette duplication.
Par exemple, en utilisant l’API on peut créer plusieurs arbres:
ParameterExpression aParam = Expression.Parameter(typeof(int), "a");
ParameterExpression bParam = Expression.Parameter(typeof(int), "b");
BinaryExpression add = Expression.Add(aParam, bParam);
ParameterExpression cParam = Expression.Parameter(typeof(int), "c");
BinaryExpression divide = Expression.Divide(add, cParam);
Expression<Func<int, int, int>> firstExpression =
Expression.Lambda<Func<int, int, int>>(add, new ParameterExpression[] { aParam, bParam });
Expression<Func<int, int, float>> secundExpression =
Expression.Lambda<Func<int, int, float>>(divide, new ParameterExpression[] { aParam, bParam, cParam });
Comparaison entre Expression<Func<TResult>> et Func<TResult>
La plupart du temps, on utilisera plutôt Func<TResult>
car il est rarement nécessaire d’analyser une expression lambda. Toutefois dans certains cas, l’utilisation de Func<TResult>
peut avoir des conséquences inattendues.
IEnumerable et IQueryable
Ces 2 types d’objet sont des structures de liste qui conviennent dans des cas assez différents. Ainsi dans le cadre de Entity Framework ou LinQ-to-SQL, certaines méthodes d’extension sur le type IEnumerable
possèdent des arguments de type Func<TResult>
.
Par exemple la signature de la fonction System.Linq.Enumerable.Where()
:
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source,
Func<TSource, bool> predicate)
A l’opposé, certaines méthodes d’extension sur IQueryable
possèdent des arguments de type Expression<Func<TResult>>
, si on compare la signature avec la fonction équivalente System.Linq.Queryable.Where()
:
public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source,
Expression<Func<TSource, bool>> predicate)
Ainsi les méthodes d’extension de IEnumerable
utilisent des delegates alors que les méthodes d’extension de IQueryable
utilisent des arbres d’expressions.
Dans le cadre de Entity Framework et LinQ-to-SQL, cette différence peut avoir des conséquences non négligeables puisque la conversion de code C# vers des requêtes SQL ne peut se faire qu’à partir d’objets de type Expression
. Ce sont ces objets Expression
qui seront analysés pour déterminer la requête à exécuter dans la base SQL.
Si on utilise des delegates pour effectuer ces requêtes, il ne peut pas y avoir d’évaluation de ces delegates en base de données. Donc pour effectuer le traitement, il faut récupérer tout le contenu de la table sur laquelle la requête doit être exécutée. Ensuite, le delegate fournit en argument de la fonction Where()
sera executé sur toutes les lignes pour obtenir le résultat. La méthode d’extension utilisant de delegate est donc moins efficace que son équivalent utilisant un arbre d’expression puisqu’on devra récupérer tout le contenu de la table.
Sachant que IQueryable
dérive de IEnumerable
, suivant le type de l’argument utilisé pour les fonctions, des surchages différentes seront exécutées:
- Avec
Expression<Func<TResult>>
, les surchargesIQueryable
seront exécutées, Func<TResult>
, ça sera plutôt les surchargesIEnumeable
qui seront exécutées.
Entity Framework
Comme indiqué plus haut, suivant le type d’argument utilisé entre Expression<Func<TResult>>
et Func<TResult>
, Entity Framework aura un comportement différent:
- Avec
Expression<Func<TResult>>
: Entity Framework va analyser l’expression pour en déduire la requête SQL à exécuté en base. - Avec
Func<TResult>
: Entity Framework récupère tout le contenu de la table ou des tables sur lesquelles porte la requête, le place dans le contexte et ensuite, exécute le delegate pour effectuer le tri.
Ainsi si on considère une table PERSON avec 3 colonnes:
- FirstName
- LastName
- Age
En exécutant:
public IEnumerable<Person> Where(DbEntities dbContext, Func<Person, bool> whereClause)
{
return dbContext.Persons.Where(where);
}
La requête suivante sera exécutée en base:
SELECT
[Extent1].[LastName] AS [LastName],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[Age] AS [Age]
FROM [dbo].[Persons] AS [Extent1]
De même, on exécutant:
public IEnumerable<Person> Where(DbEntities dbContext, Expression<Func<Person, bool>> whereClause)
{
return dbContext.Persons.Where(where);
}
var results = Where(dbContext, p => p.Age > 25);
La requête comportera une clause WHERE:
SELECT
[Extent1].[LastName] AS [LastName],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[Age] AS [Age]
FROM [dbo].[Persons] AS [Extent1]
WHERE [Extent1].[Age] > 25
Sérialisation
Les delegates ne sont pas sérialisables, ainsi les Func<T, TResult>
et Action<T>
ne sont pas sérialisables. Ceci s’explique par le fait que les delegates sont compilés en instructions IL pour être exécuté, au runtime il ne s’agit pas d’une structure mais d’une suite d’instructions IL.
A l’opposé, un objet de type Expression<Func<T, Result>>
n’est pas compilé en instructions IL au runtime. Cet objet reste une structure qui, par construction, a été implémentée pour être sérialisable. Comme indiqué plus haut, pour qu’un objet de type Expression soit exécutée, il faut exécuter l’instruction Expression.Compile()
. Cette instruction transformera la structure en fonction anonyme exécutable.
- Falling in Love with LINQ – Part 7: Expressions and Funcs:http://fascinatedwithsoftware.com/blog/post/2011/12/02/Falling-in-Love-with-LINQ-Part-7-Expressions-and-Funcs.aspx
- Lambda Expressions:http://blog.nuclex-games.com/tutorials/lambda-expressions/
- More on Expression vs Func with Entity Framework:http://fascinatedwithsoftware.com/blog/post/2012/01/10/More-on-Expression-vs-Func-with-Entity-Framework.aspx
- LINQ: Func<T> vs. Expression<Func<T>> :http://ivanitskyi.blogspot.fr/2013/06/linq-func-vs-expression.html
- Why would you use Expression<Func<T>> rather than Func<T>?:http://stackoverflow.com/questions/793571/why-would-you-use-expressionfunct-rather-than-funct
- Arborescences d’expression sur MSDN:https://msdn.microsoft.com/fr-fr/library/bb397951.aspx