Cosa fa Expression.Quote () che Expression.Constant () non può già fare?


97

Nota: Sono consapevole del problema precedente “ Qual è lo scopo del metodo di Expression.Quote di LINQ? , Ma se continui a leggere vedrai che non risponde alla mia domanda.

Capisco qual è lo scopo dichiarato Expression.Quote(). Tuttavia, Expression.Constant()può essere utilizzato per lo stesso scopo (oltre a tutti gli scopi per cui Expression.Constant()è già utilizzato). Pertanto, non capisco perché Expression.Quote()è affatto richiesto.

Per dimostrarlo, ho scritto un breve esempio dove si userebbe abitualmente Quote(vedi la riga contrassegnata con punti esclamativi), ma l'ho usato Constantinvece e ha funzionato altrettanto bene:

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);

L'output di expr.ToString()è lo stesso anche per entrambi (sia che io lo usi Constanto Quote).

Alla luce delle osservazioni di cui sopra, sembra che Expression.Quote()sia ridondante. Il compilatore C # potrebbe essere stato creato per compilare espressioni lambda annidate in un albero delle espressioni che coinvolge Expression.Constant()invece di Expression.Quote(), e qualsiasi provider di query LINQ che desidera elaborare alberi delle espressioni in un altro linguaggio di query (come SQL) potrebbe cercare un ConstantExpressiontipo con Expression<TDelegate>invece di a UnaryExpressioncon il Quotetipo di nodo speciale e tutto il resto sarebbe lo stesso.

Cosa mi sto perdendo? Perché è stato inventato il tipo di nodo Expression.Quote()speciale Quoteper UnaryExpression?

Risposte:


189

Risposta breve:

L'operatore di quotazione è un operatore che induce la semantica di chiusura sul proprio operando . Le costanti sono solo valori.

Virgolette e costanti hanno significati diversi e quindi hanno rappresentazioni diverse in un albero delle espressioni . Avere la stessa rappresentazione per due cose molto diverse è estremamente confuso e soggetto a bug.

Risposta lunga:

Considera quanto segue:

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

Il lambda esterno è una factory per i sommatori associati al parametro lambda esterno.

Supponiamo ora di voler rappresentare questo come un albero delle espressioni che verrà successivamente compilato ed eseguito. Quale dovrebbe essere il corpo dell'albero delle espressioni? Dipende se si desidera che lo stato compilato restituisca un delegato o un albero delle espressioni.

Cominciamo ignorando il caso poco interessante. Se desideriamo che restituisca un delegato, la questione se utilizzare Quote o Constant è un punto controverso:

        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));

Il lambda ha un lambda annidato; il compilatore genera il lambda interno come delegato a una funzione chiusa sullo stato della funzione generata per il lambda esterno. Non dobbiamo più considerare questo caso.

Supponiamo di desiderare che lo stato compilato restituisca un albero delle espressioni dell'interno. Ci sono due modi per farlo: il modo facile e il modo difficile.

Il modo più difficile è dirlo invece di

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

quello che intendiamo veramente è

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

E quindi genera l'albero delle espressioni per quello , producendo questo pasticcio :

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

blah blah blah, dozzine di righe di codice di riflessione per rendere lambda. Lo scopo dell'operatore quote è di dire al compilatore dell'albero delle espressioni che vogliamo che il dato lambda sia trattato come un albero delle espressioni, non come una funzione, senza dover generare esplicitamente il codice di generazione dell'albero delle espressioni .

Il modo più semplice è:

        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));

E infatti, se compili ed esegui questo codice ottieni la risposta giusta.

Si noti che l'operatore di citazione è l'operatore che induce la semantica di chiusura sul lambda interno che utilizza una variabile esterna, un parametro formale del lambda esterno.

La domanda è: perché non eliminare Quote e fare in modo che faccia la stessa cosa?

        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 costante non induce la semantica di chiusura. Perché dovrebbe? Hai detto che questa era una costante . È solo un valore. Dovrebbe essere perfetto come consegnato al compilatore; il compilatore dovrebbe essere in grado di generare solo un dump di quel valore nello stack dove è necessario.

Dato che non viene indotta alcuna chiusura, se si esegue questa operazione si otterrà un'eccezione "variabile 's' di tipo 'System.Int32' non definita" sull'invocazione.

(A parte: ho appena esaminato il generatore di codice per la creazione di delegati da alberi di espressioni citati, e sfortunatamente un commento che ho inserito nel codice nel 2006 è ancora lì. Cordiali saluti, il parametro esterno sollevato viene snapshot in una costante quando viene citato l'albero delle espressioni viene reificato come delegato dal compilatore runtime. C'era una buona ragione per cui ho scritto il codice in quel modo che non ricordo in questo preciso momento, ma ha il brutto effetto collaterale di introdurre la chiusura sui valori dei parametri esterni piuttosto che chiusura sulle variabili. Apparentemente il team che ha ereditato quel codice ha deciso di non correggere quel difetto, quindi se ti affidi alla mutazione di un parametro esterno chiuso osservato in un lambda interno citato compilato, rimarrai deluso. Tuttavia, poiché è una pratica di programmazione piuttosto pessima sia (1) mutare un parametro formale e (2) fare affidamento sulla mutazione di una variabile esterna, ti consiglio di cambiare il tuo programma per non usare queste due cattive pratiche di programmazione, piuttosto che in attesa di una correzione che non sembra essere imminente. Mi scuso per l'errore.)

Quindi, per ripetere la domanda:

Il compilatore C # potrebbe essere stato creato per compilare espressioni lambda annidate in un albero delle espressioni che coinvolge Expression.Constant () invece di Expression.Quote () e qualsiasi provider di query LINQ che desidera elaborare alberi delle espressioni in un altro linguaggio di query (come SQL ) potrebbe cercare una ConstantExpression con il tipo Expression invece di un UnaryExpression con lo speciale tipo di nodo Quote, e tutto il resto sarebbe lo stesso.

Hai ragione. Abbiamo potuto codificare informazione semantica che significa "inducono semantica chiusura di questo valore" dal usando il tipo dell'espressione costante come bandiera .

"Costante" avrebbe quindi il significato "usa questo valore costante, a meno che il tipo non sia un tipo di albero delle espressioni e il valore non sia un albero delle espressioni valido, nel qual caso, utilizza invece il valore che è l'albero delle espressioni risultante dalla riscrittura del all'interno dell'albero delle espressioni dato per indurre la semantica di chiusura nel contesto di qualsiasi lambda esterno in cui potremmo trovarci in questo momento.

Ma perché avremmo facciamo che cosa folle? L'operatore di citazione è un operatore follemente complicato e dovrebbe essere usato esplicitamente se lo userai. Stai suggerendo che per essere parsimoniosi nel non aggiungere un metodo factory e un tipo di nodo in più tra le diverse dozzine già presenti, aggiungiamo un bizzarro caso d'angolo alle costanti, in modo che le costanti siano a volte logicamente costanti, e talvolta vengono riscritte lambda con semantica di chiusura.

Avrebbe anche l'effetto un po 'strano che costante non significhi "usa questo valore". Supponi per qualche bizzarro motivo di volere che il terzo caso sopra compili un albero delle espressioni in un delegato che distribuisce un albero delle espressioni che ha un riferimento non riscritto a una variabile esterna? Perché? Forse perché stai testando il tuo compilatore e vuoi semplicemente passare la costante in modo da poter eseguire qualche altra analisi su di essa in seguito. La tua proposta lo renderebbe impossibile; qualsiasi costante che sia del tipo albero delle espressioni verrebbe riscritta a prescindere. Si ha una ragionevole aspettativa che "costante" significhi "usa questo valore". "Constant" è un nodo "fai quello che dico". Il processore costante '

E nota, naturalmente, che ora stai imponendo il peso della comprensione (cioè, capire che la costante ha una semantica complicata che significa "costante" in un caso e "induce una semantica di chiusura" basata su un flag che è nel sistema dei tipi ) su ogni provider che esegue l'analisi semantica di un albero delle espressioni, non solo sui provider Microsoft. Quanti di questi fornitori di terze parti avrebbero sbagliato?

"Quote" sta sventolando una grande bandiera rossa che dice "hey amico, guarda qui, sono un'espressione lambda annidata e ho una semantica stravagante se sono chiuso su una variabile esterna!" mentre "costante" sta dicendo "non sono altro che un valore; usami come meglio credi". Quando qualcosa è complicato e pericoloso, vogliamo fargli sventolare bandiere rosse, non nascondere questo fatto facendo scavare all'utente il sistema dei tipi per scoprire se questo valore è speciale o meno.

Inoltre, l'idea che evitare la ridondanza sia persino un obiettivo non è corretta. Certo, evitare ridondanze inutili e confuse è un obiettivo, ma la maggior parte delle ridondanze è una buona cosa; la ridondanza crea chiarezza. I nuovi metodi di fabbrica e i tipi di nodi sono economici . Possiamo farne quante ne abbiamo bisogno in modo che ognuna rappresenti un'operazione in modo pulito. Non abbiamo bisogno di ricorrere a brutti trucchi come "questo significa una cosa a meno che questo campo non sia impostato su questa cosa, nel qual caso significa qualcos'altro".


11
Sono imbarazzato ora perché non pensavo alla semantica di chiusura e non sono riuscito a testare un caso in cui il lambda annidato acquisisce un parametro da un lambda esterno. Se lo avessi fatto, avrei notato la differenza. Molte grazie ancora per la tua risposta.
Timwi

19

Questa domanda ha già ricevuto un'ottima risposta. Vorrei inoltre indicare una risorsa che può rivelarsi utile con domande sugli alberi delle espressioni:

è era un progetto CodePlex di Microsoft chiamato Dynamic Language Runtime. La sua documentazione include il documento intitolato,"Specifiche Expression Trees v2", che è esattamente questo: la specifica per gli alberi delle espressioni LINQ in .NET 4.

Aggiornamento: CodePlex è defunto. La specifica Expression Trees v2 (PDF) è stata spostata su GitHub .

Ad esempio, dice quanto segue su Expression.Quote:

4.4.42 Citazione

Utilizzare Quote in UnaryExpressions per rappresentare un'espressione con un valore "costante" di tipo Expression. A differenza di un nodo Constant, il nodo Quote gestisce in modo speciale i nodi ParameterExpression contenuti. Se un nodo ParameterExpression contenuto dichiara un locale che verrebbe chiuso nell'espressione risultante, Quote sostituisce ParameterExpression nelle posizioni di riferimento. In fase di esecuzione, quando il nodo Quote viene valutato, sostituisce i riferimenti alla variabile di chiusura per i nodi di riferimento ParameterExpression e quindi restituisce l'espressione tra virgolette. […] (Pagg. 63-64)


1
Ottima risposta del tipo "Teach-a-man-to-fish". Vorrei solo aggiungere che la documentazione è stata spostata ed è ora disponibile su docs.microsoft.com/en-us/dotnet/framework/… . Il documento citato, nello specifico, è su GitHub: github.com/IronLanguages/dlr/tree/master/Docs
Relativamente_random

3

Dopo questa una risposta davvero eccellente, è chiaro quali sono le semantiche. Non è così chiaro il motivo per cui sono progettati in questo modo, considera:

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

Quando questo lambda viene compilato e richiamato, valuta l'espressione interna e restituisce il risultato. L'espressione interna qui è un'aggiunta, quindi ps + pt viene valutato e viene restituito il risultato. Seguendo questa logica, la seguente espressione:

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

dovrebbe restituire un riferimento al metodo compilato lambda di un interno quando viene richiamato il lambda esterno (perché diciamo che lambda viene compilato in un riferimento al metodo). Allora perché abbiamo bisogno di un preventivo ?! Per differenziare il caso in cui viene restituito il riferimento al metodo rispetto al risultato di tale chiamata di riferimento.

Nello specifico:

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

Per qualche ragione i progettisti di .Net hanno scelto Expression.Quote (f) per il primo caso e la semplice f per il secondo. A mio avviso, questo causa molta confusione, poiché nella maggior parte dei linguaggi di programmazione la restituzione di un valore è diretta (non è necessario Quote o altre operazioni), ma l'invocazione richiede una scrittura extra (parentesi + argomenti), che si traduce in una sorta di invocare a livello MSIL. I designer di .Net hanno fatto l'opposto per gli alberi delle espressioni. Sarebbe interessante conoscere il motivo.


-2

Penso che il punto qui sia l'espressività dell'albero. L'espressione costante che contiene il delegato in realtà contiene solo un oggetto che sembra essere un delegato. Questo è meno espressivo di una scomposizione diretta in un'espressione unaria e binaria.


È? Quale espressività aggiunge, esattamente? Cosa puoi "esprimere" con quell'UnaryExpression (che è anche un tipo strano di espressione da usare) che non potresti già esprimere con ConstantExpression?
Timwi
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.