Was macht Expression.Quote (), was Expression.Constant () noch nicht kann?


97

Hinweis: Mir ist die frühere Frage „ Was ist der Zweck der Expression.Quote-Methode von LINQ? , Aber wenn Sie weiterlesen, werden Sie sehen, dass es meine Frage nicht beantwortet.

Ich verstehe, was der erklärte Zweck Expression.Quote()ist. Allerdings Expression.Constant()kann für den gleichen Zweck verwendet werden (zusätzlich zu allen Zwecken , die Expression.Constant()bereits für verwendet wird ). Daher verstehe ich nicht, warum Expression.Quote()überhaupt erforderlich ist.

Um dies zu demonstrieren, habe ich ein kurzes Beispiel geschrieben, in dem man normalerweise verwendet Quote(siehe die mit Ausrufezeichen gekennzeichnete Zeile), aber ich habe Constantstattdessen verwendet und es hat genauso gut funktioniert:

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

Die Ausgabe von expr.ToString()ist auch für beide gleich (ob ich benutze Constantoder Quote).

Angesichts der obigen Beobachtungen scheint dies Expression.Quote()überflüssig zu sein. Der C # -Compiler könnte dazu gebracht worden sein, verschachtelte Lambda-Ausdrücke in einen Ausdrucksbaum zu kompilieren, an dem Expression.Constant()stattdessen beteiligt ist Expression.Quote(), und jeder LINQ-Abfrageanbieter, der Ausdrucksbäume in eine andere Abfragesprache (z. B. SQL) verarbeiten möchte, könnte nach einem ConstantExpressionTyp mit Expression<TDelegate>statt suchen a UnaryExpressionmit dem speziellen QuoteKnotentyp, und alles andere wäre gleich.

Was vermisse ich? Warum wurde Expression.Quote()und der spezielle QuoteKnotentyp für UnaryExpressionerfunden?

Antworten:


189

Kurze Antwort:

Der Anführungszeichenoperator ist ein Operator, der eine Schließsemantik für seinen Operanden induziert . Konstanten sind nur Werte.

Anführungszeichen und Konstanten haben unterschiedliche Bedeutungen und daher unterschiedliche Darstellungen in einem Ausdrucksbaum . Die gleiche Darstellung für zwei sehr unterschiedliche Dinge zu haben, ist äußerst verwirrend und fehleranfällig.

Lange Antwort:

Folgendes berücksichtigen:

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

Das äußere Lambda ist eine Fabrik für Addierer, die an den Parameter des äußeren Lambda gebunden sind.

Angenommen, wir möchten dies als Ausdrucksbaum darstellen, der später kompiliert und ausgeführt wird. Was soll der Körper des Ausdrucksbaums sein? Dies hängt davon ab, ob der kompilierte Status einen Delegaten oder einen Ausdrucksbaum zurückgeben soll.

Beginnen wir damit, den uninteressanten Fall zurückzuweisen. Wenn wir möchten, dass ein Delegierter zurückgegeben wird, ist die Frage, ob Quote oder Constant verwendet werden soll, ein strittiger Punkt:

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

Das Lambda hat ein verschachteltes Lambda; Der Compiler generiert das innere Lambda als Delegat für eine Funktion, die über dem Status der für das äußere Lambda generierten Funktion geschlossen ist. Wir brauchen diesen Fall nicht mehr zu betrachten.

Angenommen, wir möchten, dass der kompilierte Zustand einen Ausdrucksbaum des Inneren zurückgibt. Dafür gibt es zwei Möglichkeiten: den einfachen und den harten Weg.

Der schwierige Weg ist, das statt zu sagen

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

was wir wirklich meinen ist

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

Und dann erzeugen , um den Ausdrucksbaum für die , die Herstellung dieses Chaos :

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

bla bla bla, Dutzende Zeilen des Reflexionscodes, um das Lambda herzustellen. Der Anführungsoperator soll dem Ausdrucksbaum-Compiler mitteilen, dass das angegebene Lambda als Ausdrucksbaum und nicht als Funktion behandelt werden soll, ohne dass der Code zur Generierung des Ausdrucksbaums explizit generiert werden muss .

Der einfache Weg ist:

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

Wenn Sie diesen Code kompilieren und ausführen, erhalten Sie die richtige Antwort.

Beachten Sie, dass der Anführungszeichenoperator der Operator ist, der eine Schließsemantik für das innere Lambda induziert, das eine äußere Variable verwendet, einen formalen Parameter des äußeren Lambda.

Die Frage ist: Warum nicht Quote eliminieren und das Gleiche tun lassen?

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

Die Konstante induziert keine Abschlusssemantik. Warum sollte es? Sie sagten, dies sei eine Konstante . Es ist nur ein Wert. Es sollte perfekt sein, wenn es dem Compiler übergeben wird. Der Compiler sollte in der Lage sein, nur einen Speicherauszug dieses Werts auf dem Stapel zu generieren, auf dem er benötigt wird.

Da kein Abschluss induziert wird, erhalten Sie beim Aufruf eine Ausnahme vom Typ "System.Int32 'ist nicht definiert" vom Aufruf "Variable".

(Nebenbei: Ich habe gerade den Codegenerator für die Delegatenerstellung aus zitierten Ausdrucksbäumen überprüft, und leider ist noch ein Kommentar vorhanden, den ich 2006 in den Code eingefügt habe. Zu Ihrer Information, der angehobene äußere Parameter wird beim Zitieren in eine Konstante aufgenommen Der Ausdrucksbaum wird vom Laufzeit-Compiler als Delegat bestätigt. Es gab einen guten Grund, warum ich den Code so geschrieben habe, dass ich mich nicht genau an diesen Moment erinnere, aber er hat den bösen Nebeneffekt, dass Werte über äußere Parameter geschlossen werden anstatt über Variablen zu schließen. Anscheinend hat das Team, das diesen Code geerbt hat, beschlossen, diesen Fehler nicht zu beheben. Wenn Sie sich also darauf verlassen, dass die Mutation eines geschlossenen äußeren Parameters in einem kompilierten zitierten inneren Lambda beobachtet wird, werden Sie enttäuscht sein. Da es jedoch eine ziemlich schlechte Programmierpraxis ist, sowohl (1) einen formalen Parameter zu mutieren als auch (2) sich auf die Mutation einer äußeren Variablen zu verlassen, würde ich empfehlen, dass Sie Ihr Programm so ändern, dass diese beiden schlechten Programmierpraktiken nicht verwendet werden Warten auf eine Lösung, die nicht bevorsteht. Entschuldigung für den Fehler.)

Um die Frage zu wiederholen:

Der C # -Compiler könnte dazu gebracht worden sein, verschachtelte Lambda-Ausdrücke in einen Ausdrucksbaum zu kompilieren, der Expression.Constant () anstelle von Expression.Quote () und jeden LINQ-Abfrageanbieter umfasst, der Ausdrucksbäume in einer anderen Abfragesprache (z. B. SQL) verarbeiten möchte ) könnte nach einem ConstantExpression mit dem Typ Expression anstelle eines UnaryExpression mit dem speziellen Quote-Knotentyp Ausschau halten, und alles andere wäre gleich.

Du hast Recht. Wir könnten semantische Informationen codieren, die "Verschlusssemantik für diesen Wert induzieren" bedeuten, indem wir den Typ des konstanten Ausdrucks als Flag verwenden .

"Konstante" hätte dann die Bedeutung "diesen konstanten Wert verwenden, es sei denn, der Typ ist zufällig ein Ausdrucksbaumtyp und der Wert ist ein gültiger Ausdrucksbaum. In diesem Fall verwenden Sie stattdessen den Wert, der der Ausdrucksbaum ist, der sich aus dem Umschreiben des ergibt Innenraum des gegebenen Ausdrucksbaums, um eine Schließungssemantik im Kontext aller äußeren Lambdas zu induzieren, in denen wir uns gerade befinden könnten.

Aber warum sollten wir dieses verrückte Ding machen? Der Anführungszeichenoperator ist ein wahnsinnig komplizierter Operator und sollte explizit verwendet werden, wenn Sie ihn verwenden möchten. Sie schlagen vor, dass wir Konstanten einen bizarren Eckfall hinzufügen, um sparsam zu sein, keine zusätzliche Factory-Methode und keinen Knotentyp unter den bereits vorhandenen mehreren Dutzend hinzuzufügen, sodass Konstanten manchmal logische Konstanten sind und manchmal neu geschrieben werden Lambdas mit Abschlusssemantik.

Es hätte auch den etwas merkwürdigen Effekt, dass Konstante nicht "diesen Wert verwenden" bedeutet. Angenommen, Sie wollten aus irgendeinem bizarren Grund, dass der dritte Fall oben einen Ausdrucksbaum in einen Delegaten kompiliert, der einen Ausdrucksbaum austeilt, der einen nicht umgeschriebenen Verweis auf eine äußere Variable enthält? Warum? Vielleicht, weil Sie Ihren Compiler testen und die Konstante einfach weitergeben möchten, damit Sie später eine andere Analyse durchführen können. Ihr Vorschlag würde dies unmöglich machen; Jede Konstante, die zufällig vom Typ Ausdrucksbaum ist, wird unabhängig davon neu geschrieben. Man hat eine vernünftige Erwartung, dass "konstant" "diesen Wert verwenden" bedeutet. "Konstante" ist ein Knoten "Mach was ich sage". Der konstante Prozessor ' basierend auf dem Typ zu sagen.

Und beachten Sie natürlich, dass Sie jetzt die Last des Verstehens (dh das Verstehen, dass Konstante eine komplizierte Semantik hat, die in einem Fall "Konstante" bedeutet und "Schließsemantik induzieren" basierend auf einem Flag, das sich im Typsystem befindet ) auf jeden legen Anbieter, der eine semantische Analyse eines Ausdrucksbaums durchführt, nicht nur für Microsoft-Anbieter. Wie viele dieser Drittanbieter würden das falsch verstehen?

"Quote" schwenkt eine große rote Fahne mit der Aufschrift "Hey Kumpel, schau her, ich bin ein verschachtelter Lambda-Ausdruck und ich habe eine verrückte Semantik, wenn ich über einer äußeren Variablen geschlossen bin!" während "Konstante" sagt "Ich bin nichts weiter als ein Wert; benutze mich, wie du es für richtig hältst." Wenn etwas kompliziert und gefährlich ist, möchten wir, dass es rote Fahnen schwenkt, ohne diese Tatsache zu verbergen, indem der Benutzer das Typsystem durchforstet , um herauszufinden, ob dieser Wert ein besonderer Wert ist oder nicht.

Darüber hinaus ist die Idee, Redundanz zu vermeiden, sogar ein Ziel, falsch. Sicher, unnötige, verwirrende Redundanz zu vermeiden, ist ein Ziel, aber die meisten Redundanzen sind eine gute Sache. Redundanz schafft Klarheit. Neue Fabrikmethoden und Knotenarten sind billig . Wir können so viele machen, wie wir brauchen, damit jeder eine Operation sauber darstellt. Wir müssen nicht auf böse Tricks zurückgreifen wie "Dies bedeutet eine Sache, es sei denn, dieses Feld ist auf diese Sache eingestellt. In diesem Fall bedeutet es etwas anderes."


11
Es ist mir jetzt peinlich, weil ich nicht an die Abschlusssemantik gedacht habe und keinen Fall testen konnte, in dem das verschachtelte Lambda einen Parameter von einem äußeren Lambda erfasst. Wenn ich das getan hätte, hätte ich den Unterschied bemerkt. Nochmals vielen Dank für Ihre Antwort.
Timwi

19

Diese Frage hat bereits eine hervorragende Antwort erhalten. Ich möchte außerdem auf eine Ressource verweisen, die sich bei Fragen zu Ausdrucksbäumen als hilfreich erweisen kann:

Dort ist war ein CodePlex-Projekt von Microsoft namens Dynamische Sprachlaufzeit. Die Dokumentation enthält das Dokument mit dem Titel:"Expression Trees v2 Spec"Das ist genau das: Die Spezifikation für LINQ-Ausdrucksbäume in .NET 4.

Update: CodePlex ist nicht mehr verfügbar. Die Expression Trees v2 Spec (PDF) wurde nach GitHub verschoben .

Zum Beispiel heißt es Folgendes über Expression.Quote:

4.4.42 Zitat

Verwenden Sie Quote in UnaryExpressions, um einen Ausdruck darzustellen, der einen "konstanten" Wert vom Typ Ausdruck hat. Im Gegensatz zu einem konstanten Knoten behandelt der Quote-Knoten speziell enthaltene ParameterExpression-Knoten. Wenn ein enthaltener ParameterExpression-Knoten ein lokales Element deklariert, das im resultierenden Ausdruck geschlossen werden würde, ersetzt Quote den ParameterExpression an seinen Referenzpositionen. Zur Laufzeit, wenn der Quote-Knoten ausgewertet wird, ersetzt er die Referenzvariablenreferenzen der ParameterExpression durch die Verschlussvariablenreferenzen und gibt dann den Ausdruck in Anführungszeichen zurück. […] (S. 63–64)


1
Hervorragende Antwort von der Art, wie man einen Mann zum Fischen lehrt. Ich möchte nur hinzufügen, dass die Dokumentation verschoben wurde und jetzt unter docs.microsoft.com/en-us/dotnet/framework/… verfügbar ist . Das zitierte Dokument befindet sich speziell auf GitHub: github.com/IronLanguages/dlr/tree/master/Docs
relativ

3

Nach dieser wirklich hervorragenden Antwort ist klar, wie die Semantik aussieht. Es ist nicht so klar, warum sie so gestaltet sind. Bedenken Sie:

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

Wenn dieses Lambda kompiliert und aufgerufen wird, wertet es den inneren Ausdruck aus und gibt das Ergebnis zurück. Der innere Ausdruck ist hier eine Addition, daher wird ps + pt ausgewertet und das Ergebnis zurückgegeben. Nach dieser Logik folgender Ausdruck:

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

sollte die Lambda-kompilierte Methodenreferenz eines inneren zurückgeben, wenn das äußere Lambda aufgerufen wird (weil wir sagen, dass Lambda zu einer Methodenreferenz kompiliert wird). Warum brauchen wir ein Angebot?! Unterscheiden des Falls, in dem die Methodenreferenz zurückgegeben wird, vom Ergebnis dieses Referenzaufrufs.

Speziell:

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

Aus irgendeinem Grund haben .NET-Designer Expression.Quote (f) für den ersten Fall und plain f für den zweiten gewählt. Meiner Ansicht nach ist dies sehr verwirrend, da in den meisten Programmiersprachen die Rückgabe eines Werts direkt erfolgt (kein Anführungszeichen oder eine andere Operation erforderlich), der Aufruf jedoch zusätzliches Schreiben (Klammern + Argumente) erfordert, was in eine Art übersetzt wird auf MSIL-Ebene aufrufen . .Net-Designer haben es für die Ausdrucksbäume umgekehrt gemacht. Wäre interessant den Grund zu kennen.


-2

Ich denke, hier geht es um die Ausdruckskraft des Baumes. Der konstante Ausdruck, der den Delegaten enthält, enthält eigentlich nur ein Objekt, das zufällig ein Delegat ist. Dies ist weniger aussagekräftig als eine direkte Aufteilung in einen unären und binären Ausdruck.


Ist es? Welche Ausdruckskraft fügt es genau hinzu? Was können Sie mit dieser UnaryExpression „ausdrücken“ (was auch eine seltsame Art von Ausdruck ist), die Sie mit ConstantExpression noch nicht ausdrücken konnten?
Timwi
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.