Что Expression.Quote () делает то, что Expression.Constant () не может уже сделать? - PullRequest
92 голосов
/ 15 сентября 2010

Примечание. Мне известен более ранний вопрос « Какова цель метода 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?

Ответы [ 4 ]

179 голосов
/ 20 сентября 2010

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

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

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

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

Рассмотрим следующее:

(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(...

И затем сгенерировать дерево выражений для , которое , создавая this mess * :

        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 году, все еще там. К вашему сведению, внешний параметр hoisted равен snapshotted в константу, когда дерево выражений в кавычках преобразуется в качестве делегата компилятором времени выполнения. Была веская причина, почему я написал код таким способом, который я не помню в данный момент, но у него есть неприятная сторонаэффект введения замыкания по значениям внешних параметров, а не замыкания по переменным . Очевидно, команда, унаследовавшая этот код, решила не исправлять этот недостаток, поэтому, если вы полагаетесь на мутациюЗамкнутый внешний параметр, наблюдаемый в скомпилированной цитируемой внутренней лямбде, вы будете разочарованы, однако, поскольку это довольно плохая практика программирования - и (1) изменять формальный параметр, и (2) полагаться на мутациювнешняя переменная, я бы порекомендовал вам чанПусть ваша программа не использует эти две плохие практики программирования, вместо того, чтобы ждать исправления, которое, похоже, не ожидается.Извиняюсь за ошибку.)

Итак, чтобы повторить вопрос:

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

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

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

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

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

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

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

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

18 голосов
/ 18 февраля 2012

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

Существует проект CodePlex от Microsoft, который называется Dynamic Language Runtime . Его документация включает в себя документ под названием «Деревья выражений v2 Spec» , который именно так: Спецификация для деревьев выражений LINQ в .NET 4.

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

4.4.42 Цитата

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

2 голосов
/ 09 декабря 2018

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

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

0 голосов
/ 20 сентября 2010

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

...