Что Expression.Quote () делает, чего Expression.Constant () еще не может?

98

Примечание. Мне известно о предыдущем вопросе « Какова цель метода Expression.Quote в LINQ?» , Но если вы продолжите читать, то увидите, что это не отвечает на мой вопрос.

Я понимаю, в чем Expression.Quote()заключается заявленная цель . Однако Expression.Constant()может использоваться для той же цели (в дополнение ко всем целям, для Expression.Constant()которых уже используется). Поэтому я не понимаю, зачем Expression.Quote()вообще это нужно.

Чтобы продемонстрировать это, я написал быстрый пример, в котором обычно можно было бы использовать Quote(см. Строку, отмеченную восклицательными знаками), но я использовал Constantвместо этого, и он работал одинаково хорошо:

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

Вывод expr.ToString()одинаков для обоих (использую я Constantили Quote).

Учитывая приведенные выше наблюдения, кажется, что Expression.Quote()это избыточно. Компилятор C # можно было бы заставить компилировать вложенные лямбда-выражения в дерево выражений с участием Expression.Constant()вместо Expression.Quote(), и любой поставщик запросов LINQ, который хочет обрабатывать деревья выражений на каком-либо другом языке запросов (например, SQL), мог бы искать ConstantExpressionс типом Expression<TDelegate>вместо a UnaryExpressionсо специальным Quoteтипом узла, а все остальное будет таким же.

Что мне не хватает? Почему был изобретен Expression.Quote()и особый Quoteтип узла для UnaryExpression?

Тимви
источник

Ответы:

189

Короткий ответ:

Оператор кавычек - это оператор, который индуцирует семантику закрытия для своего операнда. . Константы - это просто значения.

Кавычки и константы имеют разное значение и поэтому по- разному представлены в дереве выражений . Одно и то же представление двух очень разных вещей чрезвычайно сбивает с толку и подвержено ошибкам.

Длинный ответ:

Учтите следующее:

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

Внешняя лямбда - это фабрика для сумматоров, привязанных к параметру внешней лямбды.

Теперь предположим, что мы хотим представить это как дерево выражений, которое позже будет скомпилировано и выполнено. Каким должно быть тело дерева выражений? Это зависит от того, хотите ли вы, чтобы скомпилированное состояние возвращало делегат или дерево выражения.

Начнем с неинтересного случая. Если мы хотим, чтобы он возвращал делегата, вопрос о том, использовать ли Quote или Constant, является спорным:

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

Лямбда имеет вложенную лямбду; компилятор генерирует внутреннюю лямбду как делегат функции, закрытой по состоянию функции, созданной для внешней лямбды. Нам больше не нужно рассматривать этот случай.

Предположим, мы хотим, чтобы скомпилированное состояние возвращало дерево выражений интерьера. Есть два способа сделать это: легкий и сложный.

Сложно сказать, что вместо

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

что мы на самом деле имеем в виду

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

А затем сгенерируйте для этого дерево выражений , создав такой беспорядок :

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

бла-бла-бла, десятки строк кода отражения, чтобы сделать лямбду. Цель оператора кавычки - сообщить компилятору дерева выражений, что мы хотим, чтобы данная лямбда обрабатывалась как дерево выражения, а не как функция, без необходимости явно генерировать код генерации дерева выражений. .

Самый простой способ:

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

И действительно, если вы скомпилируете и запустите этот код, вы получите правильный ответ.

Обратите внимание, что оператор кавычек - это оператор, который индуцирует семантику закрытия для внутренней лямбды, которая использует внешнюю переменную, формальный параметр внешней лямбды.

Возникает вопрос: почему бы не удалить Quote и сделать то же самое?

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

Константа не вызывает семантику закрытия. Зачем это нужно? Вы сказали, что это постоянно . Это просто ценность. Он должен быть идеальным, как передано компилятору; компилятор должен иметь возможность просто генерировать дамп этого значения в стек, где это необходимо.

Так как закрытие не вызвано, если вы сделаете это, вы получите исключение «переменная типа« System.Int32 »не определена» при вызове.

(Кроме того: я только что рассмотрел генератор кода для создания делегатов из цитируемых деревьев выражений, и, к сожалению, комментарий, который я вставил в код еще в 2006 году, все еще там. К вашему сведению, приподнятый внешний параметр снимается в константу, когда цитируемый дерево выражений реифицируется как делегат компилятором времени выполнения.Я написал код таким образом, по уважительной причине, которую я не помню в данный момент, но у нее есть неприятный побочный эффект, заключающийся в введении замыкания над значениями внешних параметров а не замыкание по переменным. Очевидно, команда, унаследовавшая этот код, решила не исправлять этот недостаток, поэтому, если вы полагаетесь на мутацию закрытого внешнего параметра, наблюдаемую в скомпилированной цитируемой внутренней лямбде, вы будете разочарованы. Однако, поскольку это довольно плохая практика программирования - и (1) изменять формальный параметр, и (2) полагаться на мутацию внешней переменной, я бы порекомендовал вам изменить свою программу, чтобы не использовать эти две плохие практики программирования, а не ожидает исправления, которое, похоже, не появится. Приносим извинения за ошибку.)

Итак, повторяя вопрос:

Компилятор C # можно было заставить компилировать вложенные лямбда-выражения в дерево выражений с использованием Expression.Constant () вместо Expression.Quote () и любого поставщика запросов LINQ, который хочет обрабатывать деревья выражений на каком-либо другом языке запросов (например, SQL ) может искать ConstantExpression с типом Expression вместо UnaryExpression со специальным типом узла Quote, а все остальное будет таким же.

Ты прав. Мы могли бы закодировать семантическую информацию, которая означает «вызвать семантику закрытия для этого значения», используя тип константного выражения в качестве флага .

Тогда "константа" будет иметь значение "использовать это постоянное значение, если только тип не является типом дерева выражения, а значение является допустимым деревом выражения, и в этом случае вместо этого используйте значение, которое является деревом выражения, полученным в результате перезаписи внутренняя часть данного дерева выражений, чтобы вызвать семантику закрытия в контексте любых внешних лямбда-выражений, в которых мы могли бы находиться прямо сейчас.

Но почему бы нам сделать эту сумасшедшую вещь? Оператор кавычек - безумно сложный оператор , и его следует использовать явно, если вы собираетесь его использовать. Вы предлагаете, чтобы не добавлять один дополнительный фабричный метод и тип узла среди нескольких десятков уже имеющихся, чтобы мы добавили к константам причудливый угловой регистр, так что константы иногда являются логическими константами, а иногда их переписывают. лямбды с семантикой замыкания.

Это также будет иметь несколько странный эффект, поскольку константа не означает «использовать это значение». Предположим, по какой-то причудливой причине вы хотите, чтобы в третьем случае выше было скомпилировано дерево выражений в делегат, который передает дерево выражений с непереписанной ссылкой на внешнюю переменную? Зачем? Возможно, потому, что вы тестируете свой компилятор и хотите просто передать константу, чтобы вы могли позже выполнить какой-то другой анализ. Ваше предложение сделало бы это невозможным; любая константа, которая относится к типу дерева выражений, будет перезаписана независимо. Есть разумные основания полагать, что «константа» означает «использовать это значение». «Константа» - это узел «делай то, что я говорю». Постоянный процессор '

И, конечно, обратите внимание, что теперь вы возлагаете бремя понимания (то есть понимание того, что константа имеет сложную семантику, которая означает «константа» в одном случае и «вызывает семантику закрытия» на основе флага, который есть в системе типов ) на каждый поставщик, который выполняет семантический анализ дерева выражений, а не только поставщиков Microsoft. Сколько из этих сторонних поставщиков ошиблись бы?

«Цитата» размахивает большим красным флагом, который говорит: «Эй, приятель, посмотри сюда, я - вложенное лямбда-выражение, и у меня дурацкая семантика, если я закрываюсь по внешней переменной!» тогда как «Константа» говорит: «Я не более чем ценность; используйте меня, как считаете нужным». Когда что-то является сложным и опасным, мы хотим заставить его размахивать красными флажками, а не скрывать этот факт, заставляя пользователя копаться в системе типов , чтобы выяснить, является ли это значение особенным или нет.

Более того, идея о том, что избегание избыточности является даже целью, неверна. Конечно, целью является предотвращение ненужной, сбивающей с толку избыточности, но большая часть избыточности - это хорошо; избыточность создает ясность. Новые фабричные методы и типы узлов дешевы . Мы можем сделать столько, сколько нам нужно, чтобы каждая из них четко представляла одну операцию. Нам не нужно прибегать к неприятным уловкам вроде «это означает одно, если в этом поле не установлено это значение, и в этом случае это означает что-то другое».

Эрик Липперт
источник
11
Мне сейчас неловко, потому что я не подумал о семантике закрытия и не смог проверить случай, когда вложенная лямбда захватывает параметр из внешней лямбды. Если бы я сделал это, я бы заметил разницу. Еще раз большое спасибо за ваш ответ.
Timwi
19

На этот вопрос уже есть отличный ответ. Я также хотел бы указать на ресурс, который может оказаться полезным с вопросами о деревьях выражений:

Там является был проект CodePlex от Microsoft под названием Среда выполнения динамического языка. Его документация включает документ под названием,"Деревья выражений, версия 2", что именно так: Спецификация деревьев выражений LINQ в .NET 4.

Обновление: CodePlex не функционирует. Спецификация деревьев выражений v2 (PDF) перемещена на GitHub .

Например, в нем говорится следующее Expression.Quote:

4.4.42 Цитата

Используйте Quote в UnaryExpressions для представления выражения, имеющего «постоянное» значение типа Expression. В отличие от узла Constant, узел Quote специально обрабатывает содержащиеся узлы ParameterExpression. Если содержащийся узел ParameterExpression объявляет локальное значение, которое будет закрыто в результирующем выражении, то Quote заменяет ParameterExpression в его ссылочных местоположениях. Во время выполнения, когда оценивается узел Quote, он заменяет ссылки закрывающих переменных на ссылочные узлы ParameterExpression, а затем возвращает выражение в кавычках. […] (Стр. 63–64)

stakx поддерживает GoFundMonica
источник
1
Отличный ответ типа «научи человека ловить рыбу». Я просто хотел бы добавить, что документация перемещена и теперь доступна по адресу docs.microsoft.com/en-us/dotnet/framework/… . Цитируемый документ, в частности, находится на GitHub: github.com/IronLanguages/dlr/tree/master/Docs
relative_random
3

После этого действительно отличный ответ, понятно, какова семантика. Не очень понятно, почему они так устроены, рассмотрим:

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

Когда эта лямбда компилируется и вызывается, она оценивает внутреннее выражение и возвращает результат. Внутреннее выражение здесь - дополнение, поэтому вычисляется ps + pt и возвращается результат. Следуя этой логике, следующее выражение:

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

должен возвращать ссылку на скомпилированный метод внутренней лямбда-функции при вызове внешней лямбда-функции (потому что мы говорим, что лямбда-функция компилируется в ссылку на метод). Так зачем нам цитата ?! Чтобы отличить случай, когда возвращается ссылка на метод, от результата вызова этой ссылки.

В частности:

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

По какой-то причине дизайнеры .Net выбрали Expression.Quote (f) для первого случая и plain f для второго. На мой взгляд, это вызывает большую путаницу, поскольку в большинстве языков программирования значение возвращается напрямую (нет необходимости в Quote или какой-либо другой операции), но для вызова требуется дополнительная запись (круглые скобки + аргументы), что переводится в какой-то вид вызывать на уровне MSIL. Дизайнеры .Net сделали противоположное для деревьев выражений. Было бы интересно узнать причину.

Константин Тригер
источник
0

Я считаю, что это больше похоже на данное:

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

Ваше дерево Expression.Lambda(Expression.Lambda)и fпредставляет собой выражение дерева для лямбда , который возвращает Func<int>что возвращается 2.

Но если вам нужна лямбда, которая возвращает дерево выражений для возвращаемой лямбды 2, вам нужно:

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

И теперь ваше дерево Expression.Lambda(Expression.Quote(Expression.Lambda))и fпредставляет собой выражение дерева для лямбда , который возвращает Expression<Func<int>>это дерево выражения для того, Func<int>что возвращается 2.

NetMage
источник
-2

Думаю, дело здесь в выразительности дерева. Постоянное выражение, содержащее делегат, на самом деле просто содержит объект, который оказывается делегатом. Это менее выразительно, чем прямая разбивка на унарные и бинарные выражения.

Джо Вуд
источник
Это? Какой именно выразительности это добавляет? Что вы можете «выразить» с помощью этого UnaryExpression (которое тоже является странным видом выражения), чего вы еще не могли выразить с помощью ConstantExpression?
Timwi