web-dev-qa-db-fra.com

Qu'est-ce que Expression.Quote () fait qu'Expression.Constant () ne peut pas déjà faire?

Remarque: 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, vous verrez que cela ne répond pas à ma question.

Je comprends quel est l'objectif déclaré de Expression.Quote(). Cependant, Expression.Constant() peut être utilisé aux mêmes fins (en plus de tous les objectifs pour lesquels Expression.Constant() est déjà utilisé). Par conséquent, je ne comprends pas pourquoi Expression.Quote() est requis du tout. 

Pour illustrer cela, j’ai écrit un exemple rapide où l’on utilisait habituellement Quote (voir la ligne marquée avec des points d’exclamation), mais j’ai plutôt utilisé Constant et cela a fonctionné 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 aussi (que j'utilise Constant ou Quote).

Compte tenu des observations ci-dessus, il apparaît que Expression.Quote() est redondant. Le compilateur C # aurait pu compiler des expressions lambda imbriquées dans un arbre d’expression impliquant 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 SQL) pourrait rechercher un ConstantExpression avec tapez Expression<TDelegate> au lieu d'une UnaryExpression avec le type de noeud spécial Quote, et tout le reste serait identique.

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

92
Timwi

Réponse courte:

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

Les citations et les constantes ont des significations différentes 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 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.

Supposons maintenant que nous souhaitons représenter ceci sous la forme d’un arbre d’expression qui sera ultérieurement compilé et exécuté. Que devrait être le corps de l'arbre d'expression? Cela dépend si vous voulez que l'état compilé retourne un délégué ou un arbre d'expression.

Commençons par rejeter le cas sans intérêt. Si nous souhaitons qu’il retourne un délégué, la question de savoir si Quote ou Constant doit être utilisé 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 en tant que délégué d'une fonction fermée sur l'état de la fonction générée pour le lambda extérieur. Nous n'avons plus besoin d'examiner ce cas.

Supposons que nous souhaitions que l'état compilé retourne un arbre d'expression de l'intérieur. Il y a deux façons de le faire: le moyen facile et le moyen 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 générez ensuite 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'arbres 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 la génération d'arbre d'expression. code .

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 de guillemet 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 ce soit 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 devrait-il? Vous avez dit qu'il s'agissait d'une constante . C'est juste une valeur. Il devrait être parfait tel que remis au compilateur; le compilateur devrait pouvoir générer un dump de cette valeur dans la pile où cela est nécessaire.

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

(A part: je viens de passer en revue le générateur de code pour la création de délégué à partir d'arbres d'expression cités, et malheureusement, un commentaire que j'ai mis dans le code en 2006 est toujours présent. FYI, le paramètre externe hissé est: snapshotted dans une constante lorsque l’arbre d’expression cité est réifié en tant que délégué par le compilateur d’exécution. Il y avait une bonne raison pour avoir écrit le code de cette manière, ce dont je ne me souviens pas exactement. moment, mais il a le désagréable effet d’introduire des valeurs de fermeture de paramètres externes plutôt que des variables de fermeture sur . Apparemment, l'équipe qui a hérité de ce code a décidé de ne pas corriger ce problème, donc si vous vous appuyez sur la mutation d'un paramètre externe fermé observé dans un lambda intérieur compilé, vous êtes Cependant, étant donné que c’est une très mauvaise pratique de programmation de (1) muter un paramètre formel et (2) de s’appuyer sur la mutation d’une variable externe, je Si vous changez votre programme pour ne pas utiliser ces deux mauvaises pratiques de programmation, plutôt que d’attendre un correctif qui ne semble pas être imminent. Toutes mes excuses pour l'erreur.)

Donc, pour répéter la question:

Le compilateur C # aurait pu compiler des expressions lambda imbriquées dans un arbre d’expression impliquant Expression.Constant () au lieu d’Expression.Quote (), ainsi que tout fournisseur de requêtes LINQ souhaitant traiter les arbres d’expression dans un autre langage de requête (tel que SQL). ) pourrait rechercher une ConstantExpression avec le type Expression au lieu d’une expression UnaryExpression avec le type de noeud Quote spécial, et tout le reste serait identique.

Vous avez raison. Nous pourrions coder des informations sémantiques signifiant "induire une sémantique de fermeture sur cette valeur" par en utilisant le type de l'expression constante comme un drapeau .

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

Mais pourquoi ferions-nous cette chose folle? 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 que, pour ne pas ajouter une méthode d'usine et un type de nœud parmi les dizaines déjà existantes, nous ajoutons un casse-tête bizarre aux constantes, de sorte que les constantes soient parfois logiquement constantes et parfois réécrites. Lambdas avec sémantique de fermeture.

Cela aurait aussi l’effet un peu étrange que Constante ne signifie pas "utiliser cette valeur". Supposons que, pour une raison quelconque, vous vouliez le troisième cas ci-dessus pour compiler un arbre d'expressions en un délégué qui distribue un arbre d'expressions contenant une référence non réécrite. à une variable externe? Pourquoi? Peut-être parce que vous testez votre compilateur et souhaitez simplement transmettre la constante afin que vous puissiez effectuer une autre analyse ultérieurement. Votre proposition rendrait cela impossible. toute constante qui se trouve être du type arbre d'expression serait réécrite indépendamment. On peut raisonnablement s'attendre à ce que "constant" signifie "utilise cette valeur". "Constant" est un noeud "fais ce que je dis". Le travail du processeur constant n’est pas de deviner ce que vous vouliez dire en fonction du type.

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

"Quote" brandit un grand drapeau rouge qui dit "hé mon pote, regarde ici, je suis une expression lambda imbriquée et j'ai une sémantique délirante 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." Lorsque quelque chose est compliqué et dangereux, nous voulons faire en sorte que nous levions les drapeaux rouges, sans le cacher, en obligeant l'utilisateur à creuser avec le système de types afin de: savoir si cette valeur est spéciale ou non.

En outre, l'idée d'éviter la redondance est même un objectif est incorrecte. Éviter les redondances inutiles et source de confusion est certes un objectif, mais la plupart des redondances sont une bonne chose. la redondance crée la clarté. Les nouvelles méthodes d'usine et les types de nœuds sont économiques . Nous pouvons en faire autant que nécessaire pour que chacune représente une opération proprement. Nous n'avons pas besoin de recourir à des astuces désagréables du type "cela signifie une chose sauf si ce champ est défini sur cette chose, auquel cas cela signifie autre chose".

183
Eric Lippert

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

Il existe un projet CodePlex de Microsoft appelé Dynamic Language Runtime . Sa documentation comprend le document intitulé "Spécification des arbres d'expression v2" , qui correspond exactement à ceci: Spécification des arborescences d'expression LINQ dans .NET 4.

Par exemple, il est écrit 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 contenus. Si un nœud ParameterExpression contenu déclare un local qui serait fermé l'expression résultante, puis Quote remplace ParameterExpression dans ses emplacements de référence.Au moment de l'exécution du noeud Quote, il substitue les références de variable de fermeture aux nœuds de référence ParameterExpression, puis renvoie l'expression citée. […] ( p. 63–64)

18
stakx

Après cette excellente réponse, les sémantiques sont claires. Ce 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 interne est un ajout. 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 renvoyer une référence de méthode compilée lambda interne lorsque l'appelant lambda externe est invoqué (car nous disons que lambda est compilé en 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 en clair f pour le second . À mon avis, cela crée une grande confusion, dans la mesure où les langages de programmation renvoyant une valeur sont directs (pas besoin de Quote ou de toute autre opération), mais l'invocation nécessite une écriture supplémentaire (parenthèses + arguments), qui se traduit en une sorte de invoke au niveau MSIL. Les concepteurs .Net ont fait le contraire pour les arbres d’expression. Serait intéressant de connaître la raison.

0
Konstantin Triger