Что 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() является избыточным. Компилятор С# мог бы быть создан для компиляции вложенных лямбда-выражений в дерево выражений с использованием Expression.Constant() вместо Expression.Quote(), и любой поставщик запросов LINQ, который хочет обработать деревья выражений на каком-то другом языке запросов (например, SQL), может выглядеть для ConstantExpression с типом Expression<TDelegate> вместо UnaryExpression со специальным типом Quote node, а все остальное будет одинаковым.

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

Ответ 1

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

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

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

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

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

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

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

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

Пусть начнется, отклонив неинтересный случай. Если мы хотим его вернуть делегат, то вопрос о том, следует ли использовать 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", ...

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

Простым способом является:

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

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

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

Вопрос заключается в следующем: почему бы не исключить "Котировку" и сделать это так же?

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

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

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

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

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

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

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

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

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

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

Ответ 2

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

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

Например, в нем говорится о Expression.Quote:

4.4.42 Цитата

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

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

Ответ 4

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