Partager via


Générer des arborescences d’expressions

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 type Expression<Func<T>> ou similaire. Pour de nombreux scénarios, vous générez une expression en mémoire au moment de l’exécution.

Les arborescences d’expressions sont immuables. Être immuable signifie que vous devez construire l’arbre à partir des feuilles jusqu’à la racine. Les API que vous utilisez pour générer des arborescences d’expressions reflètent ce 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 pour vous montrer les techniques.

Créer des nœuds

Vous commencez par l’expression d’ajout avec laquelle vous avez travaillé dans toutes les sections suivantes :

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

Pour construire cette arborescence d’expressions, vous commencez par construire les nœuds de feuilles. Les nœuds feuilles sont des constantes. Utilisez la Constant méthode 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’ajout :

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

Une fois que vous avez créé l’expression d’ajout, vous créez l’expression lambda :

var lambda = Expression.Lambda(addition);

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

Pour les expressions comme celle-ci, vous pouvez combiner tous les appels dans 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 montre les principes de base de la création d’une arborescence d’expressions en mémoire. Les arborescences plus complexes signifient généralement plus de types de nœuds et plus de nœuds dans l’arborescence. Exécutons un autre exemple et affichons deux types de nœuds supplémentaires que vous générez généralement lorsque vous créez des arborescences d’expressions : les nœuds d’argument et les nœuds d’appel de méthode. Créons une arborescence d’expressions pour créer cette expression :

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

Vous commencez 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’ajout suit le modèle que vous avez déjà vu :

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

Ensuite, vous devez 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, vous placez l’appel de méthode dans une expression lambda et veillez à définir les arguments à l’expression lambda :

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

Dans cet exemple plus compliqué, vous voyez quelques techniques supplémentaires dont vous aurez souvent besoin pour créer des arborescences d’expressions.

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

Ensuite, vous devez utiliser un sous-ensemble des API Reflection pour créer un System.Reflection.MethodInfo objet 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.

Construire du code en profondeur

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

Créons 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’expressions, mais simplement le délégué. À l’aide de la Expression classe, vous ne pouvez pas générer d’instructions lambdas. Voici le code requis pour générer la même fonctionnalité. Il n’existe pas d’API pour générer une while boucle, au lieu de cela, vous devez générer une boucle qui contient un test conditionnel et une cible d’étiquette pour sortir de 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 pour construire l’arborescence d’expressions de la fonction factorielle est significativement plus long, plus compliqué, et il est truffé d’étiquettes, de déclarations de rupture et d'autres éléments que vous souhaitez éviter dans vos tâches de codage quotidiennes.

Pour cette section, vous avez écrit du code pour visiter chaque nœud 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 dans le dépôt GitHub dotnet/docs. Expérimentez vous-même en créant et en exécutant les exemples.

Mapper des constructions de code à des expressions

L’exemple de code suivant illustre une arborescence d’expressions qui représente l’expression num => num < 5 lambda à 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 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, qui s’applique également aux versions ultérieures de Visual Studio.