Partager via


Créer des arborescences d’expression

Le compilateur C# a créé toutes les arborescences d’expressions que vous avez vues jusqu’à présent. Vous avez créé une expression lambda affectée à une variable typée en tant que Expression<Func<T>> ou un type similaire. Dans de nombreux scénarios, vous générez une expression en mémoire au moment de l’exécution.

Les arborescences d’expression sont immuables. Cela signifie que vous devez générer l’arborescence à partir des feuilles jusqu’à la racine. Les API que vous utilisez pour générer des arborescences d’expressions reflètent cet état de fait : les méthodes que vous utilisez pour générer un nœud prennent tous ses enfants en tant qu’arguments. Examinons quelques exemples illustrant les techniques à appliquer.

Créer des nœuds

Vous commencez par l’expression d’addition avec laquelle vous avez travaillé tout au long de ces sections :

Expression<Func<int>> sum = () => 1 + 2;

Pour construire cette arborescence d’expressions, nous construisons d’abord les nœuds terminaux. Les nœuds feuilles sont des constantes. Utilisez la méthode Constant pour créer les nœuds :

var one = Expression.Constant(1, typeof(int));
var two = Expression.Constant(2, typeof(int));

Ensuite, générez l’expression d’addition :

var addition = Expression.Add(one, two);

Après cela, nous créons l’expression lambda :

var lambda = Expression.Lambda(addition);

Cette expression lambda ne contient aucun argument. Plus loin dans cette section, nous voyons comment mapper des arguments à des paramètres, et comment générer des expressions plus complexes.

Pour les expressions comme celle-ci, nous pouvons combiner tous les appels en une seule instruction :

var lambda2 = Expression.Lambda(
    Expression.Add(
        Expression.Constant(1, typeof(int)),
        Expression.Constant(2, typeof(int))
    )
);

Créer une arborescence

La section précédente a montré les principes de base de la création d’une arborescence d’expression en mémoire. Les arborescences plus complexes signifient généralement qu’il y a davantage de types de nœuds et davantage de nœuds dans l’arborescence. Examinons un autre exemple et voyons deux autres types de nœuds que vous générez souvent lors de la création d’arborescences d’expressions : les nœuds d’argument et les nœuds d’appel de méthode. Commençons par générer une arborescence d’expressions pour créer cette expression :

Expression<Func<double, double, double>> distanceCalc =
    (x, y) => Math.Sqrt(x * x + y * y);

Nous commençons par créer des expressions de paramètre pour x et y :

var xParameter = Expression.Parameter(typeof(double), "x");
var yParameter = Expression.Parameter(typeof(double), "y");

La création des expressions de multiplication et d’addition suit le modèle que nous avons déjà vu :

var xSquared = Expression.Multiply(xParameter, xParameter);
var ySquared = Expression.Multiply(yParameter, yParameter);
var sum = Expression.Add(xSquared, ySquared);

Ensuite, nous devons créer une expression d’appel de méthode pour l’appel à Math.Sqrt.

var sqrtMethod = typeof(Math).GetMethod("Sqrt", new[] { typeof(double) }) ?? throw new InvalidOperationException("Math.Sqrt not found!");
var distance = Expression.Call(sqrtMethod, sum);

L’appel GetMethod peut retourner null si la méthode est introuvable. Probablement parce que vous avez mal orthographié le nom de la méthode. Sinon, cela peut signifier que l’assembly requis n’est pas chargé. Enfin, placez l’appel de méthode dans une expression lambda et veillez à définir les arguments de l’expression lambda :

var distanceLambda = Expression.Lambda(
    distance,
    xParameter,
    yParameter);

Dans cet exemple plus complexe, vous pouvez observer quelques techniques supplémentaires dont vous avez souvent besoin pour créer des arborescences d’expressions.

Tout d’abord, vous devez créer les objets qui représentent des variables locales ou des paramètres avant de les utiliser. Une fois que vous avez créé ces objets, vous pouvez les utiliser dans votre arborescence d’expressions partout où vous en avez besoin.

Ensuite, vous devez utiliser un sous-ensemble des API de réflexion pour créer un objet System.Reflection.MethodInfo afin de pouvoir créer une arborescence d’expressions pour accéder à cette méthode. Vous devez vous limiter au sous-ensemble des API de réflexion disponibles sur la plateforme .NET Core. Là encore, ces techniques s’étendent à d’autres arborescences d’expressions.

Générer du code en profondeur

Vous n’êtes pas limité dans ce que vous pouvez générer à l’aide de ces API. Toutefois, plus l’arborescence d’expressions que vous souhaitez générer est compliquée, plus le code est difficile à gérer et à lire.

Nous allons générer une arborescence d’expressions qui est l’équivalent de ce code :

Func<int, int> factorialFunc = (n) =>
{
    var res = 1;
    while (n > 1)
    {
        res = res * n;
        n--;
    }
    return res;
};

Le code précédent n’a pas généré l’arborescence d’expression, mais simplement le délégué. À l’aide de la classe Expression, vous ne pouvez pas créer de lambda-instructions. Voici le code nécessaire pour générer la même fonctionnalité. Il n’existe pas d’API pour générer une boucle while. Au lieu de cela, vous devez générer une boucle qui contient un test conditionnel, et une cible d’étiquette pour quitter la boucle.

var nArgument = Expression.Parameter(typeof(int), "n");
var result = Expression.Variable(typeof(int), "result");

// Creating a label that represents the return value
LabelTarget label = Expression.Label(typeof(int));

var initializeResult = Expression.Assign(result, Expression.Constant(1));

// This is the inner block that performs the multiplication,
// and decrements the value of 'n'
var block = Expression.Block(
    Expression.Assign(result,
        Expression.Multiply(result, nArgument)),
    Expression.PostDecrementAssign(nArgument)
);

// Creating a method body.
BlockExpression body = Expression.Block(
    new[] { result },
    initializeResult,
    Expression.Loop(
        Expression.IfThenElse(
            Expression.GreaterThan(nArgument, Expression.Constant(1)),
            block,
            Expression.Break(label, result)
        ),
        label
    )
);

Le code de génération de l’arborescence d’expressions pour la fonction factorielle est un peu plus long, plus compliqué et truffé d’étiquettes, d’instructions break et d’autres éléments que vous souhaitez éviter lors de vos tâches quotidiennes de codage.

Dans cette section, vous avez écrit du code pour visiter tous les nœuds de cette arborescence d’expressions et écrire des informations sur les nœuds créés dans cet exemple. Vous pouvez afficher ou télécharger l’exemple de code à partir du dépôt GitHub dotnet/docs. Essayez par vous-même en créant et en exécutant les exemples.

Mapper des constructions de code à des expressions

L’exemple de code suivant montre comment créer une arborescence d’expressions qui représente l’expression lambda num => num < 5 à l’aide de l’API.

// 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 });

L’API de l’arborescence d’expressions prend également en charge les affectations et les expressions de flux de contrôle, comme les boucles, les blocs conditionnels et les blocs try-catch. À l’aide de l’API, vous pouvez créer des arborescences d’expressions qui sont plus complexes que celles qui peuvent être créées à partir d’expressions lambda par le compilateur C#. L’exemple suivant montre comment créer une arborescence d’expressions qui calcule la factorielle d’un nombre.

// Creating a parameter expression.
ParameterExpression value = Expression.Parameter(typeof(int), "value");

// Creating an expression to hold a local variable.
ParameterExpression result = Expression.Parameter(typeof(int), "result");

// Creating a label to jump to from a loop.
LabelTarget label = Expression.Label(typeof(int));

// Creating a method body.
BlockExpression block = Expression.Block(
    // Adding a local variable.
    new[] { result },
    // Assigning a constant to a local variable: result = 1
    Expression.Assign(result, Expression.Constant(1)),
        // Adding a loop.
        Expression.Loop(
           // Adding a conditional block into the loop.
           Expression.IfThenElse(
               // Condition: value > 1
               Expression.GreaterThan(value, Expression.Constant(1)),
               // If true: result *= value --
               Expression.MultiplyAssign(result,
                   Expression.PostDecrementAssign(value)),
               // If false, exit the loop and go to the label.
               Expression.Break(label, result)
           ),
       // Label to jump to.
       label
    )
);

// Compile and execute an expression tree.
int factorial = Expression.Lambda<Func<int, int>>(block, value).Compile()(5);

Console.WriteLine(factorial);
// Prints 120.

Pour plus d’informations, consultez Génération de méthodes dynamiques avec des arborescences d’expressions dans Visual Studio 2010 (ou version ultérieure).