Что 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
изобрел?
4 ответа
Короткий ответ:
Оператор цитаты - это оператор, который вызывает семантику замыкания в своем операнде. Константы - это просто значения.
Кавычки и константы имеют разные значения и поэтому имеют разные представления в дереве выражений. Наличие одного и того же представления для двух очень разных вещей чрезвычайно запутанно и подвержено ошибкам.
Длинный ответ:
Учтите следующее:
(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", ...
бла-бла-бла, десятки строк кода отражения, чтобы сделать лямбду. Цель оператора quote заключается в том, чтобы сообщить компилятору дерева выражений, что мы хотим, чтобы данная лямбда-код рассматривалась как дерево выражений, а не как функция, без необходимости явно генерировать код генерации дерева выражений.
Самый простой способ это:
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 снимается в константу, когда в кавычках Дерево выражений преобразуется в качестве делегата компилятором времени выполнения. Была веская причина, почему я написал код таким способом, который я не помню в данный момент, но у него действительно есть неприятный побочный эффект введения замыкания над значениями внешних параметров. вместо того, чтобы закрывать переменные. Очевидно, команда, которая унаследовала этот код, решила не исправлять этот недостаток, поэтому, если вы полагаетесь на мутацию закрытого внешнего параметра, наблюдаемого в скомпилированной цитируемой внутренней лямбде, вы будете разочарованы Однако, поскольку это довольно плохая практика программирования, чтобы (1) изменять формальный параметр и (2) полагаться на изменение внешней переменной, я бы порекомендовал вам изменить свою программу, чтобы не использовать эти два неправильных программирование, а не ожидание исправления, которое, похоже, не ожидается. Извиняюсь за ошибку.)
Итак, повторим вопрос:
Компилятор C# мог быть создан для компиляции вложенных лямбда-выражений в дерево выражений, включающее Expression.Constant() вместо Expression.Quote(), и любого поставщика запросов LINQ, который хочет обрабатывать деревья выражений в каком-либо другом языке запросов (например, SQL).) может искать ConstantExpression с типом Expression вместо UnaryExpression со специальным типом узла Quote, и все остальное будет таким же.
Ты прав. Мы могли бы закодировать семантическую информацию, которая означает "вызывать семантику замыкания для этого значения", используя тип константного выражения в качестве флага.
Тогда значение "Константа" будет иметь значение "использовать это постоянное значение, если только тип не является типом дерева выражений, а значение не является допустимым деревом выражений, и в этом случае вместо этого используйте значение, которое является деревом выражений, полученным в результате перезаписи внутренняя часть данного дерева выражений, чтобы вызвать семантику замыкания в контексте любых внешних лямбд, в которых мы могли бы быть сейчас.
Но зачем нам делать эту сумасшедшую вещь? Оператор цитаты - безумно сложный оператор, и его следует использовать явно, если вы собираетесь его использовать. Вы предлагаете, чтобы не экономить на добавлении одного дополнительного метода фабрики и типа узла из нескольких десятков уже существующих, чтобы мы добавили странный угловой регистр к константам, так что константы иногда являются логически константами, а иногда они переписываются лямбды с семантикой замыкания.
Также было бы несколько странно, что константа не означает "использовать это значение". Предположим, по какой-то странной причине, что вы хотели, чтобы в третьем случае выше было скомпилировано дерево выражений в делегате, который раздает дерево выражений, которое имеет не переписанную ссылку на внешнюю переменную? Зачем? Возможно, потому что вы тестируете свой компилятор и хотите просто передать константу, чтобы потом можно было провести другой анализ. Ваше предложение сделает это невозможным; любая константа, имеющая тип дерева выражений, будет переписана независимо. Можно предположить, что "постоянный" означает "использовать это значение". "Константа" - это узел "делай, что я говорю". Работа постоянного процессора - не угадывать, что вы хотели сказать, основываясь на типе.
И обратите внимание, конечно, что вы теперь возлагаете бремя понимания (то есть понимания того, что константа имеет сложную семантику, которая означает "константа" в одном случае и "семантику индукции замыкания", основанную на флаге, который находится в системе типов) на каждом провайдер, который выполняет семантический анализ дерева выражений, а не только провайдеров Microsoft. Сколько из этих сторонних провайдеров ошиблись?
"Цитата" размахивает большим красным флажком, на котором написано "Эй, приятель, посмотри сюда, я - вложенное лямбда-выражение, и у меня есть дурацкая семантика, если я замкнут по внешней переменной!" в то время как "Константа" говорит: "Я не более чем ценность; используйте меня, как считаете нужным". Когда что-то сложно и опасно, мы хотим, чтобы оно показывало красные флаги, не скрывая этого факта, заставляя пользователя копаться в системе типов, чтобы выяснить, является ли это значение специальным или нет.
Кроме того, идея о том, что избегание избыточности является даже целью, неверна. Конечно, избегать ненужного, сбивающего с толку избыточности - это цель, но большая часть избыточности - это хорошо; избыточность создает ясность. Новые фабричные методы и виды узлов дешевы. Мы можем сделать столько, сколько нам нужно, чтобы каждый из них четко представлял одну операцию. Нам не нужно прибегать к неприятным трюкам типа "это означает одно, если в этом поле не установлено это значение, в этом случае это означает что-то другое".
На этот вопрос уже получен отличный ответ. Я также хотел бы указать на ресурс, который может оказаться полезным с вопросами о деревьях выражений:
Существует проект CodePlex от Microsoft, который называется Dynamic Language Runtime. Его документация включает документ под названием "Expression Trees v2 Spec", который именно таков: Спецификация для деревьев выражений LINQ в.NET 4.
Например, это говорит о Expression.Quote
:
4.4.42 Цитата
Используйте Quote в UnaryExpressions для представления выражения, которое имеет "постоянное" значение типа Expression. В отличие от узла Constant, узел Quote специально обрабатывает содержащиеся узлы ParameterExpression. Если содержащийся узел ParameterExpression объявляет local, который будет закрыт в результирующем выражении, тогда Quote заменяет ParameterExpression в его ссылочных местоположениях. Во время выполнения, когда оценивается узел Quote, он заменяет ссылки на переменные замыкания на ссылочные узлы ParameterExpression, а затем возвращает выражение в кавычках. […] (Стр. 63–64)
После этого действительно превосходного ответа становится ясно, какова семантика. Не очень понятно, почему они разработаны таким образом, рассмотрим:
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 сделали противоположность для деревьев выражений. Было бы интересно узнать причину.
Я считаю, что это больше похоже на данное:
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
.
Я думаю, что дело здесь в выразительности дерева. Константное выражение, содержащее делегат, на самом деле просто содержит объект, который оказывается делегатом. Это менее выразительно, чем прямое разбиение на унарное и двоичное выражение.