103 votes

Que fait Expression.Quote() que Expression.Constant() ne peut pas déjà faire ?

Note : Je suis conscient de la question précédente " Quel est le but de la méthode Expression.Quote de LINQ ? " mais si vous lisez la suite, vous verrez que cela ne répond pas à ma question.

Je comprends ce que l'objectif déclaré de Expression.Quote() est. Cependant, Expression.Constant() peuvent être utilisées dans le même but (en plus de toutes les fins que Expression.Constant() est déjà utilisé pour). Par conséquent, je ne comprends pas pourquoi Expression.Quote() n'est pas du tout nécessaire.

Pour le démontrer, j'ai écrit un exemple rapide où l'on utiliserait habituellement Quote (voir la ligne marquée par des points d'exclamation), mais j'ai utilisé Constant à la place et ça a aussi bien fonctionné :

string[] array = { "one", "two", "three" };

// This example constructs an expression tree equivalent to the lambda:
// str => str.AsQueryable().Any(ch => ch == 'e')

Expression<Func<char, bool>> innerLambda = ch => ch == 'e';

var str = Expression.Parameter(typeof(string), "str");
var expr =
    Expression.Lambda<Func<string, bool>>(
        Expression.Call(typeof(Queryable), "Any", new Type[] { typeof(char) },
            Expression.Call(typeof(Queryable), "AsQueryable",
                            new Type[] { typeof(char) }, str),
            // !!!
            Expression.Constant(innerLambda)    // <--- !!!
        ),
        str
    );

// Works like a charm (prints one and three)
foreach (var str in array.AsQueryable().Where(expr))
    Console.WriteLine(str);

La sortie de expr.ToString() est la même pour les deux, aussi (que j'utilise Constant o Quote ).

Compte tenu des observations ci-dessus, il apparaît que Expression.Quote() est redondant. Il aurait été possible de faire en sorte que le compilateur C# compile les expressions lambda imbriquées dans un arbre d'expression comprenant Expression.Constant() au lieu de Expression.Quote() et tout fournisseur de requêtes LINQ souhaitant traiter les arbres d'expression dans un autre langage de requête (tel que le SQL) pourrait rechercher l'option ConstantExpression avec le type Expression<TDelegate> au lieu d'un UnaryExpression avec le spécial Quote type de nœud, et tout le reste serait identique.

Qu'est-ce que je rate ? Pourquoi est-ce que Expression.Quote() et le spécial Quote type de nœud pour UnaryExpression inventé ?

205voto

Eric Lippert Points 300275

Réponse courte :

L'opérateur de citation est un opérateur dont induit une sémantique de fermeture sur son opérande . Les constantes ne sont que des valeurs.

Les citations et les constantes ont des Signification et ont donc différentes représentations dans un arbre d'expression . Avoir la même représentation pour deux choses très différentes est extrêmement confuse et sujette aux bogues.

Longue réponse :

Considérez ce qui suit :

(int s)=>(int t)=>s+t

Le lambda extérieur est une usine pour les additionneurs qui sont liés au paramètre du lambda extérieur.

Supposons maintenant que nous souhaitions représenter cela sous la forme d'un arbre d'expression qui sera ensuite compilé et exécuté. Quel devrait être le corps de l'arbre d'expression ? Cela dépend si vous voulez que l'état compilé renvoie un délégué ou un arbre d'expression.

Commençons par écarter le cas inintéressant. Si nous souhaitons qu'il renvoie un délégué, la question de savoir s'il faut utiliser Quote ou Constant est sans objet :

        var ps = Expression.Parameter(typeof(int), "s");
        var pt = Expression.Parameter(typeof(int), "t");
        var ex1 = Expression.Lambda(
                Expression.Lambda(
                    Expression.Add(ps, pt),
                pt),
            ps);

        var f1a = (Func<int, Func<int, int>>) ex1.Compile();
        var f1b = f1a(100);
        Console.WriteLine(f1b(123));

Le lambda a un lambda imbriqué ; le compilateur génère le lambda intérieur comme un délégué à une fonction fermée sur l'état de la fonction générée pour le lambda extérieur. Nous n'avons plus besoin de considérer ce cas.

Supposons que nous souhaitions que l'état compilé renvoie un fichier arbre d'expression de l'intérieur. Il y a deux façons de le faire : la méthode facile et la méthode difficile.

La manière forte est de dire qu'au lieu de

(int s)=>(int t)=>s+t

ce que nous voulons vraiment dire est

(int s)=>Expression.Lambda(Expression.Add(...

Et ensuite, générer l'arbre d'expression pour que , produisant ce désordre :

        Expression.Lambda(
            Expression.Call(typeof(Expression).GetMethod("Lambda", ...

bla bla bla, des dizaines de lignes de code de réflexion pour faire le lambda. Le but de l'opérateur quote est d'indiquer au compilateur d'arbre d'expression que nous voulons que le lambda donné soit traité comme un arbre d'expression, et non comme une fonction, sans avoir à générer explicitement le code de génération d'arbre d'expression .

Le moyen le plus simple est :

        var ex2 = Expression.Lambda(
            Expression.Quote(
                Expression.Lambda(
                    Expression.Add(ps, pt),
                pt)),
            ps);

        var f2a = (Func<int, Expression<Func<int, int>>>)ex2.Compile();
        var f2b = f2a(200).Compile();
        Console.WriteLine(f2b(123));

Et en effet, si vous compilez et exécutez ce code, vous obtenez la bonne réponse.

Remarquez que l'opérateur quote est l'opérateur qui induit une sémantique de fermeture sur le lambda intérieur qui utilise une variable extérieure, un paramètre formel du lambda extérieur.

La question est la suivante : pourquoi ne pas éliminer Quote et faire en sorte que cela fasse la même chose ?

        var ex3 = Expression.Lambda(
            Expression.Constant(
                Expression.Lambda(
                    Expression.Add(ps, pt),
                pt)),
            ps);

        var f3a = (Func<int, Expression<Func<int, int>>>)ex3.Compile();
        var f3b = f3a(300).Compile();
        Console.WriteLine(f3b(123));

La constante n'induit pas de sémantique de fermeture. Pourquoi le ferait-elle ? Vous avez dit que c'était un constant . C'est juste une valeur. Elle devrait être parfaite telle qu'elle est transmise au compilateur ; ce dernier devrait être capable de générer un dump de cette valeur sur la pile où elle est nécessaire.

Comme il n'y a pas de fermeture induite, si vous faites cela, vous obtiendrez une exception "variable 's' de type 'System.Int32' non définie" lors de l'invocation.

(A propos : je viens de revoir le générateur de code pour la création de délégués à partir d'arbres d'expression cités, et malheureusement un commentaire que j'ai mis dans le code en 2006 est toujours là. Pour info, le paramètre extérieur soulevé est snapshoté en une constante lorsque l'arbre d'expression cité est réifié comme un délégué par le compilateur d'exécution. Il y avait une bonne raison pour laquelle j'ai écrit le code de cette manière, dont je ne me souviens pas à ce moment précis, mais cela a le désagréable effet secondaire d'introduire la fermeture sur valeurs des paramètres extérieurs plutôt que la fermeture sur variables . Apparemment, l'équipe qui a hérité de ce code a décidé de ne pas corriger cette faille, donc si vous comptez sur la mutation d'un paramètre extérieur fermé observé dans un lambda intérieur cité compilé, vous allez être déçu. Cependant, étant donné que c'est une assez mauvaise pratique de programmation que de (1) muter un paramètre formel et (2) de compter sur la mutation d'une variable externe, je vous recommanderais de modifier votre programme pour ne pas utiliser ces deux mauvaises pratiques de programmation, plutôt que d'attendre un correctif qui ne semble pas venir. Toutes mes excuses pour cette erreur).

Donc, pour répéter la question :

On aurait pu faire en sorte que le compilateur C# compile les expressions lambda imbriquées dans un arbre d'expression impliquant Expression.Constant() au lieu de Expression.Quote(), et tout fournisseur de requêtes LINQ qui souhaite traiter les arbres d'expression dans un autre langage de requête (tel que SQL) pourrait rechercher une ConstantExpression de type Expression au lieu d'une UnaryExpression avec le type de nœud spécial Quote, et tout le reste serait identique.

Vous avez raison. Nous pourrait encoder des informations sémantiques qui signifient "induire une sémantique de fermeture sur cette valeur" en en utilisant le type de l'expression constante comme un drapeau .

"Constante" aurait alors le sens de "utiliser cette valeur constante", sauf si le type se trouve être un type d'arbre d'expression y la valeur est un arbre d'expression valide, dans ce cas, utilisez plutôt la valeur qui est l'arbre d'expression résultant de la réécriture de l'intérieur de l'arbre d'expression donné pour induire la sémantique de fermeture dans le contexte de toutes les lambdas externes dans lesquelles nous pourrions être en ce moment.

Mais pourquoi serait on fait cette chose folle ? L'opérateur de citation est un opérateur incroyablement compliqué. et il devrait être utilisé explicitement si vous comptez l'utiliser. Vous suggérez qu'afin d'être parcimonieux et de ne pas ajouter une méthode d'usine et un type de nœud supplémentaires parmi les dizaines qui existent déjà, nous ajoutons un cas de figure bizarre aux constantes, de sorte que les constantes soient parfois logiquement des constantes, et parfois des lambdas réécrits avec une sémantique de fermeture.

Cela aurait également l'effet un peu étrange que constant ne signifie pas "utilisez cette valeur". Supposons que, pour une raison bizarre, vous recherché le troisième cas ci-dessus pour compiler un arbre d'expression dans un délégué qui distribue un arbre d'expression qui a une référence non réécrite à une variable externe ? Pourquoi ? Peut-être parce que vous testez votre compilateur et que tu veux juste faire passer la constante pour que tu puisses l'analyser plus tard. Votre proposition rendrait cela impossible ; toute constante qui se trouve être de type expression tree serait réécrite de toute façon. On peut raisonnablement s'attendre à ce que "constant" signifie "utiliser cette valeur". "Constant" est un nœud "faites ce que je dis". Le travail du processeur de constantes n'est pas de deviner ce que vous voulez faire. signifiait à dire en fonction du type.

Et notez bien sûr que vous mettez maintenant la charge de la compréhension (c'est-à-dire comprendre que constant a une sémantique compliquée qui signifie "constant" dans un cas et "induire une sémantique de fermeture" basée sur un drapeau qui est dans le système de type ) sur chaque qui effectue une analyse sémantique d'un arbre d'expression, et pas seulement sur les fournisseurs de Microsoft. Combien de ces fournisseurs tiers se tromperaient ?

"Citer", c'est agiter un grand drapeau rouge qui dit "hé mon pote, regarde par ici, je suis une expression lambda imbriquée et j'ai une sémantique bizarre si je suis fermé sur une variable externe !" alors que "Constant" dit "je ne suis rien de plus qu'une valeur ; utilise-moi comme bon te semble". Lorsqu'une chose est compliquée et dangereuse, nous voulons qu'elle émette des signaux d'alarme, et non pas qu'elle cache ce fait en obligeant l'utilisateur à fouiller dans la base de données de l système de type afin de savoir si cette valeur est spéciale ou non.

En outre, l'idée qu'éviter la redondance soit même un objectif est incorrecte. Bien sûr, éviter la redondance inutile et déroutante est un objectif, mais la plupart des redondances sont une bonne chose ; la redondance crée de la clarté. Les nouvelles méthodes d'usine et les nouveaux types de nœuds sont bon marché . Nous pouvons en faire autant que nécessaire pour que chacun représente proprement une opération. Nous n'avons pas besoin d'avoir recours à des trucs méchants comme "ceci signifie une chose à moins que ce champ ne prenne cette valeur, auquel cas cela signifie autre chose".

13 votes

Je suis embarrassé maintenant parce que je n'ai pas pensé à la sémantique de fermeture et que je n'ai pas testé un cas où le lambda imbriqué capture un paramètre d'un lambda extérieur. Si j'avais fait cela, j'aurais remarqué la différence. Merci encore pour votre réponse.

19voto

stakx Points 29832

Cette question a déjà reçu une excellente réponse. J'aimerais également vous indiquer une ressource qui peut s'avérer utile pour les questions relatives aux arbres d'expression :

Il y a est était un projet CodePlex de Microsoft appelé Exécution du langage dynamique . Sa documentation comprend le document intitulé , "Expression Trees v2 Spec" qui est exactement cela : La spécification des arbres d'expression LINQ dans .NET 4.

Mise à jour : CodePlex est défunt. Le site Arbres d'expression v2 Spec (PDF) a déménagé sur GitHub .

Par exemple, on peut lire ce qui suit Expression.Quote :

4.4.42 Citation

Utilisez Quote dans UnaryExpressions pour représenter une expression qui a une valeur "constante" de type Expression. Contrairement au nœud Constant, le nœud Quote gère spécialement les nœuds ParameterExpression contenus. Si un nœud ParameterExpression contenu déclare un local qui serait fermé dans l'expression résultante, alors Quote remplace le ParameterExpression dans ses emplacements de référence. Au moment de l'exécution, lorsque le nœud Quote est évalué, il substitue les références de la variable de fermeture aux nœuds de référence ParameterExpression, puis renvoie l'expression citée. [ ] (p. 63-64)

1 votes

Excellente réponse du type "apprendre à un homme à pêcher". Je voudrais juste ajouter que la documentation a été déplacée et est maintenant disponible à l'adresse suivante docs.microsoft.com/fr/us/dotnet/framework/ . Le document cité, en particulier, se trouve sur GitHub : github.com/IronLanguages/dlr/tree/master/Docs

3voto

Konstantin Triger Points 111

Après cette réponse vraiment excellente, la sémantique est claire. Ce n'est pas si clair por qué ils sont conçus de cette façon, réfléchissez :

Expression.Lambda(Expression.Add(ps, pt));

Lorsque ce lambda est compilé et invoqué, il évalue l'expression interne et renvoie le résultat. L'expression interne est ici une addition, donc l'élément ps+pt est évalué et le résultat est renvoyé. En suivant cette logique, l'expression suivante :

Expression.Lambda(
    Expression.Lambda(
              Expression.Add(ps, pt),
            pt), ps);

devrait retourner la référence de la méthode compilée par le lambda interne lorsque le lambda externe est invoqué (parce que nous disons que le lambda se compile en une référence de méthode). Alors pourquoi avons-nous besoin d'une Citation ? Pour différencier le cas où la référence de la méthode est renvoyée du résultat de l'invocation de cette référence.

Plus précisément :

let f = Func<...>
return f; vs. return f(...);

Pour une raison quelconque, les concepteurs de .Net ont choisi Expression.Quote(f) pour le premier cas et simple f pour le second. À mon avis, cela entraîne une grande confusion, car dans la plupart des langages de programmation, retourner une valeur est direct (pas besoin de Citation ou toute autre opération), mais l'invocation nécessite une écriture supplémentaire (parenthèses + arguments), ce qui se traduit par une sorte de invoquez au niveau du MSIL. Les concepteurs de .Net ont fait le contraire pour les arbres d'expression. Il serait intéressant d'en connaître la raison.

1voto

NetMage Points 163

Je crois que c'est plutôt donné :

Expression<Func<Func<int>>> f = () => () => 2;

Votre arbre est Expression.Lambda(Expression.Lambda) y f représente l'Arbre d'Expression pour une lambda qui retourne un Func<int> qui renvoie 2 .

Mais si ce que vous vouliez était une lambda qui renvoie le Arbre d'expression pour un lambda qui renvoie 2 alors vous en avez besoin :

Expression<Func<Expression<Func<int>>>> f = () => () => 2;

Et maintenant votre arbre est Expression.Lambda(Expression.Quote(Expression.Lambda)) y f représente l'Arbre d'Expression pour une lambda qui retourne un Expression<Func<int>> qui est l'arbre d'expression d'un Func<int> qui renvoie 2 .

-3voto

Joe Wood Points 473

Je pense que le point essentiel ici est l'expressivité de l'arbre. L'expression constante contenant le délégué ne fait que contenir un objet qui se trouve être un délégué. C'est moins expressif qu'une décomposition directe en une expression unaire et binaire.

0 votes

C'est le cas ? Quelle expressivité ajoute-t-elle, exactement ? Que pouvez-vous "exprimer" avec cette UnaryExpression (qui est un type d'expression bizarre à utiliser, aussi) que vous ne pouviez pas déjà exprimer avec ConstantExpression ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X