Що Expression.Quote () робить те, що Expression.Constant () ще не може зробити?


97

Примітка: Мені відомо про попереднє запитання “ Яка мета методу 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>замість 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", ...

бла-бла-бла, десятки рядків коду відбиття, щоб зробити лямбду. Призначення оператора 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));

І справді, якщо ви скомпілюєте та запустите цей код, ви отримаєте правильну відповідь.

Зверніть увагу, що оператор котирувань - це оператор, який індукує семантику закриття внутрішньої лямбди, яка використовує зовнішню змінну, формальний параметр зовнішньої лямбди.

Питання: чому б не усунути Цитату і не змусити це робити те саме?

        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) покладатися на мутацію зовнішньої змінної, я б рекомендував вам змінити програму, щоб не використовувати ці дві погані практики програмування, а не чекаючи виправлення, яке, здається, не відбудеться. Вибачення за помилку.)

Отже, щоб повторити запитання:

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

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

Тоді "Постійна" мала б значення "використовувати це значення константи, якщо тільки тип не є типом дерева виразів і значення не є дійсним деревом виразів, і в цьому випадку замість цього використовуйте значення, яке є деревом виразів, отриманим в результаті переписування інтер'єр даного дерева виразів, щоб викликати семантику закриття в контексті будь-яких зовнішніх лямбд, які ми можемо знаходити зараз.

Але чому б нам зробити цю божевільну річ? Оператор котирувань - це шалено складний оператор , і його слід використовувати явно, якщо ви збираєтеся ним користуватися. Ви пропонуєте, щоб зважаючи на те, щоб не додавати один зайвий заводський метод і тип вузла серед кількох десятків уже наявних, ми додали химерний кутовий регістр до констант, щоб константи іноді були логічно константами, а іноді їх переписували лямбди з семантикою закриття.

Це також мало б якийсь дивний ефект, що константа не означає "використовувати це значення". Припустимо, з якоїсь химерної причини ви хотіли, щоб третій випадок зкомпілював дерево виразів у делегат, який передає дерево виразів, яке має не переписане посилання на зовнішню змінну? Чому? Можливо, тому, що ви тестуєте свій компілятор і хочете просто передати константу, щоб потім можна було виконати якийсь інший аналіз. Ваша пропозиція зробить це неможливим; будь-яка константа, яка має тип дерева виразів, буде переписана незалежно. Можна обгрунтовано сподіватися, що «константа» означає «використовувати це значення». "Постійний" - це вузол "роби те, що я кажу". Постійний процесор ' сказати на основі типу.

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

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

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


11
Зараз мене бентежить, тому що я не думав про семантику закриття і не зміг перевірити випадок, коли вкладена лямбда захоплює параметр із зовнішньої лямбди. Якби я це зробив, я б помітив різницю. Ще раз велике спасибі за вашу відповідь.
Timwi

19

Це питання вже отримало чудову відповідь. Крім того, я хотів би вказати на ресурс, який може бути корисним із запитаннями про дерева виразів:

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

Оновлення: CodePlex не працює. У Expression Trees v2 Spec (PDF) переїхав в GitHub .

Наприклад, там сказано наступне про Expression.Quote:

4.4.42 Цитата

Використовуйте Quote у UnaryExpressions для представлення виразу, який має "константне" значення типу Expression. На відміну від вузла Constant, вузол Quote спеціально обробляє містять вузли ParameterExpression. Якщо вміщений вузол ParameterExpression оголошує локальний елемент, який буде закритий у результуючому виразі, тоді Quote замінює ParameterExpression у його опорних місцях. Під час виконання, коли обчислюється вузол Quote, він замінює посилання на змінну закриття для посилальних вузлів ParameterExpression, а потім повертає цитований вираз. […] (Стор. 63–64)


1
Відмінна відповідь типу «навчити людину ловити рибу». Я хотів би лише додати, що документація переміщена і тепер доступна за адресою docs.microsoft.com/en-us/dotnet/framework/… . Цитований документ, в зокрема, на GitHub: github.com/IronLanguages/dlr/tree/master/Docs
relatively_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. .Чисті дизайнери зробили протилежне для дерев виразів. Було б цікаво дізнатись причину.


-2

Думаю, справа тут у виразності дерева. Вираз константи, що містить делегат, насправді просто містить об’єкт, який є делегатом. Це менш виразно, ніж пряме розбиття на одинарний та двійковий вираз.


Є це? Яку виразність він додає, власне? Що ви можете «висловити» за допомогою цього UnaryExpression (що теж є дивним видом виразу), якого ви вже не могли висловити за допомогою ConstantExpression?
Timwi
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.