Exécuter des arborescences d’expressions

Une arborescence d’expressions est une structure de données qui représente du code. Il ne s’agit pas de code compilé et exécutable. Si vous souhaitez exécuter le code .NET représenté par une arborescence d’expressions, vous devez le convertir en instructions de langage intermédiaire exécutables. L’exécution d’une arborescence d’expressions peut retourner une valeur, ou elle peut simplement effectuer une action telle que l’appel d’une méthode.

Seules les arborescences d’expressions qui représentent des expressions lambda peuvent être exécutées. Les arborescences d’expressions qui représentent des expressions lambda peuvent être de type LambdaExpression ou Expression<TDelegate>. Pour exécuter ces arborescences d’expressions, appelez la méthode Compile pour créer un délégué exécutable, puis appelez le délégué.

Notes

Si le type du délégué n’est pas connu, autrement dit si l’expression lambda est de type LambdaExpression et non Expression<TDelegate>, appelez la méthode DynamicInvoke sur le délégué au lieu de l’appeler directement.

Si une arborescence d’expressions ne représente pas une expression lambda, vous pouvez créer une expression lambda ayant l’arborescence d’expressions d’origine comme corps, en appelant la méthode Lambda<TDelegate>(Expression, IEnumerable<ParameterExpression>). Ensuite, vous pouvez exécuter l’expression lambda comme décrit plus haut dans cette section.

Des expressions lambda aux fonctions

Vous pouvez convertir n’importe quelle LambdaExpression ou n’importe quel type dérivé de LambdaExpression en langage intermédiaire exécutable. Les autres types d’expressions ne peuvent pas être convertis directement en code. Cette restriction a peu d’effet dans la pratique. Les expressions lambda sont les seuls types d’expressions que vous pourriez souhaiter exécuter par l’intermédiaire d’une conversion en langage intermédiaire exécutable. (Réfléchissez à ce que signifie l’exécution directe d’une System.Linq.Expressions.ConstantExpression. Est-ce que cela veut dire quelque chose d’utile ?) Toute arborescence de l’expression qui est une System.Linq.Expressions.LambdaExpression ou un type dérivé de LambdaExpression peut être convertie en langage intermédiaire (IL). Le type d’expression System.Linq.Expressions.Expression<TDelegate> est le seul exemple concret dans les bibliothèques .NET Core. Il sert à représenter une expression mappée à un type délégué quelconque. Ce type étant mappé à un type délégué, .NET peut examiner l’expression et générer le langage intermédiaire pour un délégué approprié qui correspond à la signature de l’expression lambda. Le type délégué est basé sur le type d’expression. Si vous souhaitez utiliser l’objet délégué d’une manière fortement typée, vous devez connaître le type de retour et la liste d’arguments. La méthode LambdaExpression.Compile() retourne le type Delegate. Vous devez effectuer un cast vers le type délégué approprié pour que les outils de compilation puissent vérifier la liste d’arguments de type de retour.

Dans la plupart des cas, un mappage simple entre une expression et son délégué correspondant existe. Par exemple, une arborescence d’expressions représentée par Expression<Func<int>> serait convertie en un délégué du type Func<int>. Pour une expression lambda avec tout type de retour et liste d’arguments, il existe un type délégué qui est le type cible pour le code exécutable représenté par cette expression lambda.

Le type System.Linq.Expressions.LambdaExpression contient des membres LambdaExpression.Compile et LambdaExpression.CompileToMethod que vous utiliseriez pour convertir une arborescence d’expressions en code exécutable. La méthode Compile crée un délégué. La méthode CompileToMethod met à jour un objet System.Reflection.Emit.MethodBuilder avec le langage intermédiaire qui représente la sortie compilée de l’arborescence d’expressions.

Important

CompileToMethod est disponible uniquement dans .NET Framework, et non dans .NET Core ou .NET 5 et versions ultérieures.

Si vous le souhaitez, vous pouvez également fournir un System.Runtime.CompilerServices.DebugInfoGenerator qui reçoit les informations de débogage de symbole pour l’objet délégué généré. DebugInfoGenerator fournit des informations de débogage complètes sur le délégué généré.

Vous pouvez convertir une expression en délégué à l’aide du code suivant :

Expression<Func<int>> add = () => 1 + 2;
var func = add.Compile(); // Create Delegate
var answer = func(); // Invoke Delegate
Console.WriteLine(answer);

L’exemple de code suivant illustre les types concrets utilisés lorsque vous compilez et exécutez une arborescence d’expression.

Expression<Func<int, bool>> expr = num => num < 5;

// Compiling the expression tree into a delegate.
Func<int, bool> result = expr.Compile();

// Invoking the delegate and writing the result to the console.
Console.WriteLine(result(4));

// Prints True.

// You can also use simplified syntax
// to compile and run an expression tree.
// The following line can replace two previous statements.
Console.WriteLine(expr.Compile()(4));

// Also prints True.

L’exemple de code suivant montre comment exécuter une arborescence d’expressions qui représente l’élévation d’un nombre à une puissance en créant une expression lambda et en l’exécutant. Le résultat, qui représente le nombre élevé à la puissance, est affiché.

// The expression tree to execute.
BinaryExpression be = Expression.Power(Expression.Constant(2d), Expression.Constant(3d));

// Create a lambda expression.
Expression<Func<double>> le = Expression.Lambda<Func<double>>(be);

// Compile the lambda expression.
Func<double> compiledExpression = le.Compile();

// Execute the lambda expression.
double result = compiledExpression();

// Display the result.
Console.WriteLine(result);

// This code produces the following output:
// 8

Exécution et durées de vie

Vous exécutez le code en appelant le délégué créé quand vous avez appelé LambdaExpression.Compile(). Le code précédent, add.Compile(), retourne un délégué. Vous appelez ce délégué en appelant func(), qui exécute le code.

Ce délégué représente le code dans l’arborescence d’expressions. Vous pouvez conserver le handle de ce délégué et l’appeler ultérieurement. Vous n’avez pas besoin de compiler l’arborescence d’expressions chaque fois que vous souhaitez exécuter le code qu’elle représente. (N’oubliez pas que les arborescences d’expressions sont immuables ; compiler la même arborescence d’expressions ultérieurement crée un délégué qui exécute le même code.)

Attention

Évitez de créer des mécanismes de mise en cache plus complexes pour améliorer les performances en évitant les appels de compilation inutiles. Comparer deux arborescences d’expressions arbitraires pour déterminer si elles représentent le même algorithme est une opération qui prend du temps. Le temps de calcul économisé en évitant tout appel supplémentaire à LambdaExpression.Compile() sera probablement largement contrebalancé par le temps passé à exécuter le code qui détermine si deux arborescences d’expressions génèrent le même code exécutable.

Mises en garde

Compiler une expression lambda en un délégué et appeler ce délégué sont l’une des opérations les plus simples que vous puissiez effectuer avec une arborescence d’expressions. Toutefois, même avec cette simple opération, il existe des pièges dont vous devez être conscient.

Les expressions lambda créent des fermetures sur toutes les variables locales qui sont référencées dans l’expression. Vous devez vérifier que toutes les variables qui font partie du délégué sont utilisables à l’emplacement où vous appelez Compile et quand vous exécutez le délégué résultant. Le compilateur garantit que les variables sont dans l’étendue. Toutefois, si votre expression accède à une variable qui implémente IDisposable, il est possible que votre code supprime l’objet alors qu’il est toujours détenu par l’arborescence d’expressions.

Par exemple, ce code fonctionne bien, car int n’implémente pas IDisposable :

private static Func<int, int> CreateBoundFunc()
{
    var constant = 5; // constant is captured by the expression tree
    Expression<Func<int, int>> expression = (b) => constant + b;
    var rVal = expression.Compile();
    return rVal;
}

Le délégué a capturé une référence à la variable locale constant. Cette variable est accessible à tout moment ultérieurement, quand la fonction retournée par CreateBoundFunc s’exécute.

Maintenant, examinez la classe qui suit, qui implémente System.IDisposable :

public class Resource : IDisposable
{
    private bool _isDisposed = false;
    public int Argument
    {
        get
        {
            if (!_isDisposed)
                return 5;
            else throw new ObjectDisposedException("Resource");
        }
    }

    public void Dispose()
    {
        _isDisposed = true;
    }
}

Si vous l’utilisez dans une expression comme illustré dans le code suivant, vous obtenez une System.ObjectDisposedException quand vous exécutez le code référencé par la propriété Resource.Argument :

private static Func<int, int> CreateBoundResource()
{
    using (var constant = new Resource()) // constant is captured by the expression tree
    {
        Expression<Func<int, int>> expression = (b) => constant.Argument + b;
        var rVal = expression.Compile();
        return rVal;
    }
}

Le délégué retourné par cette méthode a fermé l’objet constant, qui a été supprimé. (Il a été supprimé car il a été déclaré dans une instruction using.)

Désormais, quand vous exécuterez le délégué retourné par cette méthode, une ObjectDisposedException est levée au moment de l’exécution.

Cela semble étrange d’avoir une erreur d’exécution qui représente une construction de compilation, mais cela fait partie des éventualités quand vous travaillez avec des arborescences d’expressions.

Comme il existe de nombreuses permutations de ce problème, il est difficile de proposer des conseils généraux pour l’éviter. Faites attention si vous accédez à des variables locales quand vous définissez des expressions et faites attention si vous accédez à l’état de l’objet actif (représenté par this) quand vous créez une arborescence d’expressions qui peut être retournée par une API publique.

Le code dans votre expression peut référencer des méthodes ou des propriétés dans d’autres assemblys. Ces assemblys doivent être accessibles quand l’expression est définie, quand elle est compilée et quand le délégué résultant est appelé. S’il n’est pas présent, vous recevrez une ReferencedAssemblyNotFoundException.

Résumé

Les arborescences d’expressions qui représentent des expressions lambda peuvent être compilées pour créer un délégué exécutable. Les arborescences d’expression fournissent un mécanisme pour exécuter le code représenté par une arborescence d’expressions.

L’arborescence d’expressions représente le code qui s’exécuterait pour toute construction donnée que vous créez. Tant que l’environnement dans lequel vous compilez et exécutez le code correspond à celui dans lequel vous créez une expression, tout fonctionne comme prévu. Si ce n’est pas le cas, les erreurs sont prévisibles et sont interceptées lors de vos premiers tests de n’importe quel code utilisant les arborescences d’expressions.