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


97

Remarque: j'ai connaissance de la question précédente « Quel est le but de la méthode Expression.Quote de LINQ? » , Mais si vous continuez à lire, vous verrez que cela ne répond pas à ma question.

Je comprends quel est l'objectif déclaré Expression.Quote(). Cependant, Expression.Constant()peut être utilisé dans le même but (en plus de tous les buts Expression.Constant()déjà utilisés). Par conséquent, je ne comprends pas du tout pourquoi Expression.Quote()est nécessaire.

Pour démontrer cela, j'ai écrit un exemple rapide où l'on utiliserait habituellement Quote(voir la ligne marquée de points d'exclamation), mais j'ai utilisé à la Constantplace et cela a fonctionné tout aussi bien:

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 (que j'utilise Constantou Quote).

Compte tenu des observations ci-dessus, il apparaît que Expression.Quote()c'est redondant. Le compilateur C # aurait pu être conçu pour compiler des expressions lambda imbriquées dans une arborescence d'expressions impliquant Expression.Constant()au lieu de Expression.Quote(), et tout fournisseur de requêtes LINQ souhaitant traiter les arborescences d'expressions dans un autre langage de requête (tel que SQL) pourrait rechercher un ConstantExpressiontype with Expression<TDelegate>au lieu de a UnaryExpressionavec le Quotetype de nœud spécial , et tout le reste serait identique.

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

Réponses:


189

Réponse courte:

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

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

Longue réponse:

Considérer ce qui suit:

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

Le lambda externe est une fabrique pour les additionneurs liés au paramètre du lambda externe.

Maintenant, supposons que nous souhaitons le représenter comme 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 souhaitez que l'état compilé renvoie un délégué ou une arborescence d'expression.

Commençons par rejeter l'affaire sans intérêt. Si nous souhaitons qu'il renvoie un délégué, la question de savoir s'il faut utiliser Quote ou Constant est un point discutable:

        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 en tant que délégué à une fonction fermée sur l'état de la fonction générée pour le lambda externe. Nous n'avons plus besoin de considérer ce cas.

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

Le plus dur est de dire qu'au lieu de

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

ce que nous voulons vraiment dire, c'est

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

Et puis générez l'arbre d'expression pour cela , 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 du 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, pas 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.

Notez 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 externe, un paramètre formel du lambda externe.

La question est: 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 devrait-il? Vous avez dit que c'était une constante . C'est juste une valeur. Il devrait être parfait tel que remis au compilateur; le compilateur devrait être capable de simplement générer un vidage de cette valeur dans la pile où il est nécessaire.

Puisqu'il n'y a pas de fermeture induite, si vous faites cela, vous obtiendrez une exception "variable 's' de type 'System.Int32' is not defined" lors de l'invocation.

(A part: je viens de passer en revue le générateur de code pour la création de délégués à partir d'arbres d'expressions cités, et malheureusement, un commentaire que j'ai mis dans le code en 2006 est toujours là. Pour info, le paramètre externe hissé est instantané dans une constante lorsque le L'arbre d'expression est réifié en tant que délégué par le compilateur d'exécution. Il y avait une bonne raison pour laquelle j'ai écrit le code de cette façon dont je ne me souviens pas à ce moment précis, mais cela a le désagréable effet secondaire d'introduire une fermeture sur les valeurs des paramètres externes plutôt que de fermer sur des variables. Apparemment, l'équipe qui a hérité de ce code a décidé de ne pas corriger cette faille, donc si vous vous fiez à la mutation d'un paramètre externe fermé observé dans un lambda intérieur cité compilé, vous allez être déçu. Cependant, comme c'est une très mauvaise pratique de programmation à la fois (1) muter un paramètre formel et (2) 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 en attente d'un correctif qui ne semble pas être à venir. Toutes mes excuses pour l'erreur.)

Donc, pour répéter la question:

Le compilateur C # aurait pu être conçu pour compiler des expressions lambda imbriquées dans une arborescence d'expressions impliquant Expression.Constant () au lieu d'Expression.Quote (), et tout fournisseur de requêtes LINQ souhaitant traiter les arborescences d'expression dans un autre langage de requête (tel que SQL ) pourrait rechercher une ConstantExpression avec le type Expression au lieu d'une UnaryExpression avec le type de nœud Quote spécial, et tout le reste serait le même.

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

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

Mais pourquoi ferions- nous ce truc fou? L'opérateur de devis est un opérateur incroyablement compliqué , et il devrait être utilisé explicitement si vous allez l'utiliser. Vous suggérez qu'afin d'être parcimonieux sur le fait de ne pas ajouter une méthode d'usine et un type de nœud supplémentaires parmi les plusieurs dizaines déjà présents, nous ajoutions un cas d'angle bizarre aux constantes, de sorte que les constantes soient parfois logiquement des constantes, et parfois elles sont réécrites lambdas avec sémantique de fermeture.

Cela aurait également l'effet quelque peu étrange que constante ne signifie pas "utiliser cette valeur". Supposons que, pour une raison étrange, vous vouliez que le troisième cas ci-dessus compile un arbre d'expression en 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 vous souhaitez simplement transmettre la constante afin de pouvoir effectuer une autre analyse plus tard. Votre proposition rendrait cela impossible; toute constante qui se trouve être de type arbre d'expression serait réécrite malgré tout. On peut raisonnablement s'attendre à ce que «constant» signifie «utiliser cette valeur». «Constant» est un nœud «fais ce que je dis». Le processeur constant ' à dire en fonction du type.

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

"Quote" agite un gros drapeau rouge qui dit "Hey mon pote, regarde par ici, je suis une expression lambda imbriquée et j'ai une sémantique farfelue si je suis fermé sur une variable externe!" alors que "Constant" dit "Je ne suis rien de plus qu'une valeur; utilisez-moi comme bon vous semble." Quand quelque chose est compliqué et dangereux, nous voulons le faire agiter des drapeaux rouges, ne pas cacher ce fait en obligeant l'utilisateur à fouiller dans le système de types afin de savoir si cette valeur est spéciale ou non.

De plus, l'idée selon laquelle éviter la redondance est même un objectif est incorrecte. Bien sûr, éviter les redondances inutiles et déroutantes 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 types de nœuds sont bon marché . Nous pouvons en fabriquer autant que nécessaire pour que chacun représente une opération proprement. Nous n'avons pas besoin de recourir à de vilaines astuces comme "cela signifie une chose à moins que ce champ ne soit défini sur cette chose, auquel cas cela signifie autre chose".


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

19

Cette question a déjà reçu une excellente réponse. J'aimerais également signaler une ressource qui peut s'avérer utile pour les questions sur les arbres d'expressions:

est était un projet CodePlex de Microsoft appelé Runtime de langage dynamique. Sa documentation comprend le document intitulé,"Expression Trees v2 Spec", qui est exactement cela: la spécification des arborescences d'expressions LINQ dans .NET 4.

Mise à jour: CodePlex est obsolète. La spécification Expression Trees v2 (PDF) a été déplacée vers GitHub .

Par exemple, il dit ce qui suit à propos de Expression.Quote:

4.4.42 Citation

Utilisez Quote dans UnaryExpressions pour représenter une expression qui a une valeur "constante" de type Expression. Contrairement à un nœud Constant, le nœud Quote gère spécialement les nœuds ParameterExpression. Si un nœud ParameterExpression contenu déclare un local qui serait fermé dans l'expression résultante, 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 remplace les références de variable de fermeture par les nœuds de référence ParameterExpression, puis renvoie l'expression entre guillemets. […] (P. 63–64)


1
Excellente réponse du genre Teach-a-Man-to-Fish. Je voudrais juste ajouter que la documentation a été déplacée et est maintenant disponible sur docs.microsoft.com/en-us/dotnet/framework/… . Le document cité, en particulier, est sur GitHub: github.com/IronLanguages/dlr/tree/master/Docs
relative_random

3

Après cela, une excellente réponse, il est clair quelle est la sémantique. Il n'est pas si clair pourquoi ils sont conçus de cette façon, considérez:

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

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

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

devrait retourner une référence de méthode compilée lambda interne lorsque le lambda externe est appelé (car nous disons que lambda compile vers une référence de méthode). Alors pourquoi avons-nous besoin d'un devis?! Pour différencier le cas où la référence de méthode est renvoyée par rapport au résultat de cet appel de référence.

Plus précisément:

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

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


-2

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


C'est ça? Quelle expressivité ajoute-t-il exactement? Que pouvez-vous «exprimer» avec cette UnaryExpression (qui est également un type d'expression étrange à utiliser) que vous ne pouviez pas déjà exprimer avec ConstantExpression?
Timwi
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.