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

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

Я понимаю, какова заявленная цель 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> вместо UnaryExpression со специальным типом узла Quote, и все остальное будет таким же.

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


person Timwi    schedule 15.09.2010    source источник


Ответы (5)


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

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

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

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

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

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

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

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

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

Итак, повторим вопрос:

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

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

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

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

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

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

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

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

person Eric Lippert    schedule 20.09.2010
comment
Мне сейчас неловко, потому что я не подумал о семантике закрытия и не смог проверить случай, когда вложенная лямбда захватывает параметр из внешней лямбды. Если бы я сделал это, я бы заметил разницу. Еще раз большое спасибо за ваш ответ. - person Timwi; 21.09.2010

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

is - это проект CodePlex от Microsoft под названием Dynamic Language Runtime < / del>. Его документация включает документ под названием "Expression Trees v2 Spec" , что и есть: спецификация деревьев выражений LINQ в .NET 4.

Обновление. CodePlex не работает. Спецификация деревьев выражений v2 (PDF) < / strong> переехал на GitHub.

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

4.4.42 Цитата

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

person Community    schedule 18.02.2012
comment
Отличный ответ типа «научи человека ловить рыбу». Я просто хотел бы добавить, что документация перемещена и теперь доступна по адресу docs.microsoft.com/en-us/dotnet/framework/. Цитируемый документ, в частности, находится на GitHub: github.com/IronLanguages/dlr/tree/ master / Docs - person relatively_random; 19.11.2019

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

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

person Konstantin Triger    schedule 08.12.2018

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

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.

person NetMage    schedule 01.09.2020

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

person Joe Wood    schedule 20.09.2010
comment
Это? Какой именно выразительности это добавляет? Что вы можете «выразить» с помощью этого UnaryExpression (это тоже странный вид выражения), чего вы еще не могли выразить с помощью ConstantExpression? - person Timwi; 20.09.2010