Co robi Expression.Quote (), czego Expression.Constant () nie może już zrobić?


98

Uwaga: znam wcześniejsze pytanie „ Jaki jest cel metody Expression.Quote w LINQ? , Ale jeśli przeczytasz dalej, zobaczysz, że to nie odpowiada na moje pytanie.

Rozumiem, jaki jest podany cel Expression.Quote(). Jednak Expression.Constant()może być używany do tego samego celu (oprócz wszystkich celów, do których Expression.Constant()jest już używany). Dlatego nie rozumiem, dlaczego Expression.Quote()w ogóle jest to wymagane.

Aby to zademonstrować, napisałem szybki przykład, w którym zwyczajowo można by użyć Quote(patrz linia oznaczona wykrzyknikami), ale Constantzamiast tego użyłem i działało równie dobrze:

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

Wyjście expr.ToString()jest takie samo dla obu (niezależnie od tego, czy używam, Constantczy Quote).

Biorąc pod uwagę powyższe obserwacje, wydaje się, że Expression.Quote()jest to zbędne. Kompilator C # mógł skompilować zagnieżdżone wyrażenia lambda do drzewa wyrażeń obejmującego Expression.Constant()zamiast Expression.Quote(), a każdy dostawca zapytań LINQ, który chce przetwarzać drzewa wyrażeń na inny język zapytań (taki jak SQL), mógłby szukać ConstantExpressionz typem Expression<TDelegate>zamiast a UnaryExpressionze specjalnym Quotetypem węzła, a wszystko inne byłoby takie samo.

czego mi brakuje? Dlaczego wynaleziono Expression.Quote()specjalny Quotetyp węzła UnaryExpression?

Odpowiedzi:


191

Krótka odpowiedź:

Operator cudzysłowu jest operatorem, który wywołuje semantykę zamknięcia w swoim operandzie . Stałe to tylko wartości.

Cytaty i stałe mają różne znaczenia i dlatego mają różne reprezentacje w drzewie wyrażeń . Posiadanie tej samej reprezentacji dwóch bardzo różnych rzeczy jest niezwykle zagmatwane i podatne na błędy.

Długa odpowiedź:

Rozważ następujące:

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

Zewnętrzna lambda to fabryka sumatorów, które są powiązane z zewnętrznym parametrem lambda.

Teraz załóżmy, że chcemy przedstawić to jako drzewo wyrażeń, które zostanie później skompilowane i wykonane. Jaka powinna być treść drzewa wyrażeń? Zależy to od tego, czy chcesz, aby stan skompilowany zwracał delegata, czy drzewo wyrażeń.

Zacznijmy od odrzucenia nieciekawej sprawy. Jeśli chcielibyśmy, aby zwrócił delegata, to kwestia, czy użyć cudzysłowu, czy stałej, jest kwestią sporną:

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

Lambda ma zagnieżdżoną lambdę; kompilator generuje wewnętrzną lambdę jako delegata funkcji zamkniętej na stan funkcji wygenerowanej dla zewnętrznej lambda. Nie musimy więcej rozważać tej sprawy.

Załóżmy, że chcielibyśmy, aby stan kompilacji zwracał drzewo wyrażeń wnętrza. Są na to dwa sposoby: łatwy i trudny.

Trudno jest powiedzieć, że zamiast

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

tak naprawdę mamy na myśli

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

A następnie wygenerować drzewo ekspresyjną że , produkujących ten bałagan :

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

bla bla bla, dziesiątki wierszy kodu odbicia tworzącego lambdę. Celem operatora cytatu jest poinformowanie kompilatora drzewa wyrażeń, że chcemy, aby dana lambda była traktowana jako drzewo wyrażeń, a nie jako funkcja, bez konieczności jawnego generowania kodu generującego drzewo wyrażeń .

Prosty sposób to:

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

I rzeczywiście, jeśli skompilujesz i uruchomisz ten kod, otrzymasz właściwą odpowiedź.

Zauważ, że operator cudzysłowu jest operatorem, który wywołuje semantykę zamykania wewnętrznej lambdy, która używa zmiennej zewnętrznej, formalnego parametru zewnętrznej lambdy.

Pytanie brzmi: dlaczego nie wyeliminować Quote i sprawić, by robił to samo?

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

Stała nie wywołuje semantyki domknięcia. Dlaczego miałoby to robić? Powiedziałeś, że to była stała . To tylko wartość. Powinien być doskonały, jak przekazano kompilatorowi; kompilator powinien być w stanie po prostu wygenerować zrzut tej wartości na stos, tam gdzie jest to potrzebne.

Ponieważ nie ma wywołania zamknięcia, jeśli to zrobisz, otrzymasz wyjątek „zmienna 's' typu„ System.Int32 ”nie jest zdefiniowana”.

(Na marginesie: właśnie przejrzałem generator kodu do tworzenia delegatów z cytowanych drzew wyrażeń i niestety komentarz, który umieściłem w kodzie z powrotem w 2006 roku, wciąż tam jest. FYI, podniesiony parametr zewnętrzny jest migawkowany do stałej, gdy cytowany drzewo wyrażeń jest reifikowane jako delegat przez kompilator środowiska uruchomieniowego. Był dobry powód, dla którego napisałem kod w ten sposób, którego nie pamiętam w tym momencie, ale ma to nieprzyjemny efekt uboczny wprowadzenia domknięcia wartości parametrów zewnętrznych zamiast zamknięcia zmiennych. Najwyraźniej zespół, który odziedziczył ten kod, postanowił nie naprawiać tej usterki, więc jeśli polegasz na mutacji zamkniętego parametru zewnętrznego obserwowanego w skompilowanej, cytowanej wewnętrznej lambdzie, będziesz rozczarowany. Jednakże, ponieważ dość złą praktyką programistyczną jest zarówno (1) mutacja parametru formalnego, jak i (2) poleganie na mutacji zmiennej zewnętrznej, zalecałbym zmianę programu tak, aby nie używał tych dwóch złych praktyk programowania, zamiast czekając na poprawkę, która wydaje się nie nadejść. Przepraszamy za błąd.)

Tak więc, aby powtórzyć pytanie:

Kompilator C # mógł skompilować zagnieżdżone wyrażenia lambda do drzewa wyrażeń obejmującego Expression.Constant () zamiast Expression.Quote () i dowolnego dostawcy zapytań LINQ, który chce przetwarzać drzewa wyrażeń w innym języku zapytań (takim jak SQL ) może szukać ConstantExpression z typem Expression zamiast UnaryExpression ze specjalnym typem węzła Quote, a wszystko inne byłoby takie samo.

Masz rację. My mogli kodować informacje semantyczne, które oznacza „wywoływania semantykę zamknięcia na tej wartości” za pomocą typ stałej ekspresji jako flaga .

„Stała” miałby wtedy znaczenie „użyj tej stałej wartości, chyba że typ jest typem drzewa wyrażenia, a wartość jest prawidłowym drzewem wyrażenia, w takim przypadku zamiast tego użyj wartości będącej drzewem wyrażenia wynikającym z przepisania wnętrza danego drzewa wyrażeń, aby wywołać semantykę domknięcia w kontekście wszelkich zewnętrznych lambd, w których możemy się teraz znajdować.

Ale dlaczego mielibyśmy robić to szalone rzeczy? Operator cudzysłowu jest niesamowicie skomplikowanym operatorem i powinien być używany jawnie, jeśli zamierzasz go używać. Sugerujesz, aby nie dodawać jednej dodatkowej metody fabrycznej i typu węzła spośród kilkudziesięciu już istniejących, aby dodać dziwaczny przypadek narożny do stałych, tak aby stałe były czasami logicznymi stałymi, a czasami są przepisywane lambdy z semantyką zamknięcia.

Miałoby to również nieco dziwny efekt, że stała nie oznacza „użyj tej wartości”. Załóżmy, że z jakiegoś dziwnego powodu chciałbyś, aby trzeci powyższy przypadek skompilował drzewo wyrażeń w delegata, który przekazuje drzewo wyrażenia, które ma nieprzepisane odniesienie do zmiennej zewnętrznej? Czemu? Być może dlatego, że testujesz kompilator i chcesz po prostu przekazać stałą, abyś mógł później przeprowadzić inną analizę. Twoja propozycja uniemożliwiłaby to; każda stała, która jest typu drzewa wyrażenia, zostanie przepisana niezależnie od tego. Można się spodziewać, że „stała” oznacza „użyj tej wartości”. „Stała” to węzeł „rób to, co mówię”. Stały procesor ” powiedzieć na podstawie typu.

Zauważ, oczywiście, że teraz kładziesz ciężar zrozumienia (to znaczy rozumienia, że ​​stała ma skomplikowaną semantykę, która oznacza „stałą” w jednym przypadku i „indukuje semantykę zamknięcia” w oparciu o flagę znajdującą się w systemie typów ) na każdym dostawca, który przeprowadza analizę semantyczną drzewa wyrażeń, a nie tylko dostawców firmy Microsoft. Ilu z tych zewnętrznych dostawców zrobiłoby to źle?

„Cytuj” macha wielką czerwoną flagą, która mówi: „Hej kolego, spójrz tutaj, jestem zagnieżdżonym wyrażeniem lambda i mam zwariowaną semantykę, jeśli jestem zamknięty na zmiennej zewnętrznej!” podczas gdy „Constant” mówi: „Jestem niczym więcej niż wartością; używaj mnie tak, jak uważasz za stosowne”. Kiedy coś jest skomplikowane i niebezpieczne, chcemy sprawić, by machało czerwonymi flagami, nie ukrywając tego faktu, zmuszając użytkownika do przekopywania się przez system typów , aby dowiedzieć się, czy ta wartość jest specjalna, czy nie.

Co więcej, pomysł, że uniknięcie nadmiarowości jest nawet celem, jest błędny. Oczywiście, unikanie niepotrzebnych, mylących redundancji jest celem, ale większość nadmiarowości to dobra rzecz; nadmiarowość zapewnia przejrzystość. Nowe metody fabryczne i rodzaje węzłów są tanie . Możemy zrobić tyle, ile potrzebujemy, aby każda z nich w przejrzysty sposób reprezentowała jedną operację. Nie musimy uciekać się do nieprzyjemnych sztuczek, takich jak „to oznacza jedną rzecz, chyba że to pole jest ustawione na tę rzecz, w którym to przypadku oznacza coś innego”.


11
Jestem teraz zażenowany, ponieważ nie pomyślałem o semantyce zamknięcia i nie udało mi się przetestować przypadku, w którym zagnieżdżona lambda przechwytuje parametr z zewnętrznej lambdy. Gdybym to zrobił, zauważyłbym różnicę. Jeszcze raz wielkie dzięki za odpowiedź.
Timwi

19

To pytanie otrzymało już doskonałą odpowiedź. Dodatkowo chciałbym wskazać zasób, który może okazać się pomocny w przypadku pytań dotyczących drzew wyrażeń:

Tam jest był projektem CodePlex firmy Microsoft o nazwie Dynamiczne środowisko wykonawcze języka. Jego dokumentacja zawiera dokument pt.„Specyfikacja drzew wyrażeń v2”, czyli dokładnie to: specyfikacja drzew wyrażeń LINQ w .NET 4.

Aktualizacja: CodePlex nie istnieje. Specyfikacja Drzewa wyrażeń v2 (PDF) została przeniesiona do GitHub .

Na przykład mówi o tym Expression.Quote:

4.4.42 Cytat

Użyj cudzysłowu w UnaryExpressions, aby reprezentować wyrażenie, które ma „stałą” wartość typu Expression. W przeciwieństwie do węzła Constant, węzeł Quote specjalnie obsługuje zawarte węzły ParameterExpression. Jeśli zawarty węzeł ParameterExpression deklaruje lokalną, która zostanie zamknięta w wynikowym wyrażeniu, wówczas Quote zastępuje ParameterExpression w jego lokalizacjach referencyjnych. W czasie wykonywania, gdy węzeł Quote jest oceniany, zastępuje odwołania do zmiennych zamknięcia dla węzłów odwołań ParameterExpression, a następnie zwraca cytowane wyrażenie. […] (S. 63–64)


1
Doskonała odpowiedź typu „naucz człowieka łowić ryby”. Chciałbym tylko dodać, że dokumentacja została przeniesiona i jest teraz dostępna pod adresem docs.microsoft.com/en-us/dotnet/framework/… . Cytowany dokument znajduje się w szczególności na GitHub: github.com/IronLanguages/dlr/tree/master/Docs
stosunkowo_random

3

Po tej naprawdę doskonałej odpowiedzi, jasne jest, jaka jest semantyka. Nie jest jasne, dlaczego są zaprojektowane w ten sposób, rozważ:

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

Gdy ta lambda jest kompilowana i wywoływana, oblicza wewnętrzne wyrażenie i zwraca wynik. Wyrażenie wewnętrzne jest tutaj dodaniem, więc ps + pt jest obliczane i zwracany jest wynik. Zgodnie z tą logiką następujące wyrażenie:

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

powinna zwracać odwołanie do metody skompilowanej lambda wewnętrznej, gdy wywoływana jest zewnętrzna lambda (ponieważ mówimy, że lambda kompiluje się do odwołania do metody). Dlaczego więc potrzebujemy wyceny ?! Aby rozróżnić przypadek, w którym zwracane jest odwołanie do metody, od wyniku tego wywołania odwołania.

Konkretnie:

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

Z jakiegoś powodu projektanci .Net wybrali Expression.Quote (f) w pierwszym przypadku i zwykły f w drugim. Moim zdaniem powoduje to duże zamieszanie, ponieważ w większości języków programowania zwracanie wartości jest bezpośrednie (nie ma potrzeby stosowania cudzysłowu ani żadnej innej operacji), ale wywołanie wymaga dodatkowego zapisu (nawiasy + argumenty), co przekłada się na wywołać na poziomie MSIL. Projektanci .Net uczynili to przeciwieństwem dla drzew wyrażeń. Ciekawie byłoby poznać przyczynę.


0

Wydaje mi się, że bardziej przypomina dane:

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

Twoje drzewo jest Expression.Lambda(Expression.Lambda)i freprezentuje drzewo wyrażeń dla wyrażenia lambda, które zwraca Func<int>zwracaną wartość 2.

Ale jeśli to, czego chciałeś, to lambda, która zwraca drzewo wyrażeń dla lambdy, która zwraca 2, to potrzebujesz:

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

A teraz twoje drzewo jest Expression.Lambda(Expression.Quote(Expression.Lambda))i freprezentuje drzewo wyrażeń dla wyrażenia lambda, które zwraca wartość będącą Expression<Func<int>>drzewem wyrażeń dla a, Func<int>która zwraca 2.


-2

Myślę, że chodzi o ekspresję drzewa. Wyrażenie stałe zawierające delegata w rzeczywistości zawiera tylko obiekt, który jest delegatem. Jest to mniej wyraziste niż bezpośrednie rozbicie na jednoargumentowe i binarne wyrażenie.


Czy to jest? Jaką wyrazistość to dokładnie dodaje? Co możesz „wyrazić” za pomocą tego UnaryExpression (co jest również dziwnym rodzajem wyrażenia), którego nie mogłeś już wyrazić za pomocą ConstantExpression?
Timwi,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.