Дерева виразів для чайників? [зачинено]


83

Я манекен у цьому сценарії.

Я намагався прочитати в Google, що це, але я просто не розумію. Хтось може дати мені просте пояснення того, що вони є і чому вони корисні?

редагувати: Я говорю про функцію LINQ у .Net.


1
Я знаю, що ця публікація досить стара, але останнім часом я вивчаю дерева виразів. Я зацікавився після того, як почав користуватися плавним NHibernate. Джеймс Грегорі широко використовує те, що відоме як статичне відбиття, і у нього є вступ: jagregory.com/writings/introduction-to-static-reflection Щоб побачити статичні дерева відбиття та вираження в дії, перегляньте вихідний код Fluent NHibernate ( fluentnhibernate.org ). Це дуже чисто, і дуже класна концепція.
Jim Schubert

Відповіді:


89

Найкраще пояснення про дерева виразів, яке я коли-небудь читав, - це стаття Чарлі Калверта.

Підсумовуючи це;

Дерево виразів представляє те, що ви хочете зробити, а не те, як ви хочете це зробити.

Розглянемо такий простий лямбда-вираз:
Func<int, int, int> function = (a, b) => a + b;

Це твердження складається з трьох розділів:

  • Декларація: Func<int, int, int> function
  • Оператор рівних: =
  • Лямбда-вираз: (a, b) => a + b;

Змінна functionвказує на вихідний виконуваний код, який знає, як скласти два числа .

Це найважливіша різниця між делегатами та виразами. Ви телефонуєте functionFunc<int, int, int>), ніколи не знаючи, що це буде робити з двома цілими числами, які ви передали. Потрібно два, а один повертає, це найбільше, що може знати ваш код.

У попередньому розділі ви бачили, як оголосити змінну, яка вказує на необроблений виконуваний код. Дерева виразів не є виконуваним кодом , вони є формою структури даних.

Тепер, на відміну від делегатів, ваш код може знати, для чого призначено дерево виразів.

LINQ забезпечує простий синтаксис для перекладу коду в структуру даних, яка називається деревом виразів. Першим кроком є ​​додавання оператора using для введення Linq.Expressionsпростору імен:

using System.Linq.Expressions;

Тепер ми можемо створити дерево виразів:
Expression<Func<int, int, int>> expression = (a, b) => a + b;

Ідентичний лямбда-вираз, показаний у попередньому прикладі, перетворюється у дерево виразів, оголошене типом Expression<T>. Ідентифікатор expression не є виконуваним кодом; це структура даних, яка називається деревом виразів.

Це означає, що ви не можете просто викликати дерево виразів, як виклик делегата, але ви можете його проаналізувати. Отже, що може зрозуміти ваш код, аналізуючи змінну expression?

// `expression.NodeType` returns NodeType.Lambda.
// `expression.Type` returns Func<int, int, int>.
// `expression.ReturnType` returns Int32.

var body = expression.Body;
// `body.NodeType` returns ExpressionType.Add.
// `body.Type` returns System.Int32.

var parameters = expression.Parameters;
// `parameters.Count` returns 2.

var firstParam = parameters[0];
// `firstParam.Name` returns "a".
// `firstParam.Type` returns System.Int32.

var secondParam = parameters[1].
// `secondParam.Name` returns "b".
// `secondParam.Type` returns System.Int32.

Тут ми бачимо, що є багато інформації, яку ми можемо отримати із виразу.

Але навіщо нам це потрібно?

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

Запит LINQ to SQL не виконується всередині вашої програми C #. Натомість він перекладається на SQL, надсилається по дроту і виконується на сервері баз даних. Іншими словами, такий код насправді ніколи не виконується всередині вашої програми:
var query = from c in db.Customers where c.City == "Nantes" select new { c.City, c.CompanyName };

Спочатку він перекладається в наступний оператор SQL, а потім виконується на сервері:
SELECT [t0].[City], [t0].[CompanyName] FROM [dbo].[Customers] AS [t0] WHERE [t0].[City] = @p0

Код, знайдений у виразі запиту, повинен бути переведений у запит SQL, який може бути надісланий іншому процесу як рядок. У цьому випадку цей процес виявляється базою даних SQL-сервера. Очевидно, що перекласти структуру даних, таку як дерево виразів, у SQL буде набагато простіше, ніж перекласти необроблений ІЛ чи виконуваний код у SQL. Щоб дещо перебільшити складність проблеми, просто уявіть спробу перевести ряд нулів і одиниць в SQL!

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

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

Оскільки запит надходить до компілятора, інкапсульованого в таку абстрактну структуру даних, компілятор може вільно інтерпретувати його практично будь-яким способом, який він хоче. Він не змушений виконувати запит у певному порядку або певним чином. Натомість він може проаналізувати дерево виразів, виявити, що ви хочете зробити, а потім вирішити, як це зробити. Принаймні теоретично, він має свободу враховувати будь-яку кількість факторів, таких як поточний мережевий трафік, навантаження на базу даних, поточні набори результатів, які вона має в наявності, тощо. На практиці LINQ to SQL не враховує всі ці фактори , але теоретично безкоштовно робити майже те, що він хоче. Крім того, можна передати це дерево виразів якомусь користувацькому коду, який ви пишете від руки, який може проаналізувати його та перевести в щось дуже різне від того, що створюється LINQ to SQL.

Ще раз бачимо, що дерева виразів дозволяють представляти (висловлювати?) Те, що ми хочемо робити. І ми використовуємо перекладачів, які вирішують, як вживаються наші вирази.


2
Одна з кращих відповідей.
Джонні,

4
відмінна відповідь. один маленький аспект, який слід додати до цього блискучого пояснення, - інше використання дерев виразів полягає в тому, що ви можете змінювати дерево виразів на льоту під час виконання, як вам може здатися потрібним, перш ніж подавати його на виконання, що часом надзвичайно корисно.
Ян Д,

41

Дерево виразів - це механізм перетворення виконуваного коду в дані. Використовуючи дерево виразів, ви можете створити структуру даних, яка представляє вашу програму.

У C # ви можете працювати з деревом виразів, створеним лямбда-виразами, використовуючи Expression<T>клас.


У традиційній програмі ви пишете такий код:

double hypotenuse = Math.Sqrt(a*a + b*b);

Цей код змушує компілятор генерувати призначення, і все. У більшості випадків це все, що вас турбує.

За допомогою звичайного коду ваш додаток не може повернутися назад і переглянути, hypotenuseщоб визначити, що його було створено за допомогою Math.Sqrt()виклику; ця інформація просто не є частиною того, що включено.

Тепер розглянемо такий лямбда-вираз, як такий:

Func<int, int, double> hypotenuse = (a, b) => Math.Sqrt(a*a + b*b);

Це трохи інше, ніж раніше. Зараз hypotenuseнасправді є посилання на блок виконуваного коду . Якщо зателефонувати

hypotenuse(3, 4);

ви отримаєте 5повернене значення .

Ми можемо використовувати дерева виразів, щоб дослідити створений блок виконуваного коду. Спробуйте замість цього:

Expression<Func<int, int, int>> addTwoNumbersExpression = (x, y) => x + y;
BinaryExpression body = (BinaryExpression) addTwoNumbersExpression.Body;
Console.WriteLine(body);

Це дає:

(x + y)

З деревами виразів можливі більш досконалі методики та маніпуляції.


7
Добре, я був з вами до кінця, але я все ще не розумію, чому це велика справа. Я важко думаю про програми.

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

2
Так, ця відповідь була б кращою, якби він / вона пояснив, чому (х + у) насправді було корисно для нас. Чому ми хочемо дослідити (x + y) і як ми це робимо?
Paul Matthews

Вам не потрібно його досліджувати, ви робите це лише для того, щоб побачити, що таке ваш запит і що буде перекладено на якусь іншу мову в такому випадку в SQL
stanimirsp

15

Дерева виразів - це представлення виразу в пам’яті, наприклад, арифметичний або булевий вираз. Наприклад, розглянемо арифметичний вираз

a + b*2

Оскільки * має вищий операторський пріоритет, ніж +, дерево виразів будується так:

    [+]
  /    \
 a     [*]
      /   \
     b     2

Маючи це дерево, його можна оцінити за будь-якими значеннями a та b. Крім того, ви можете перетворити його в інші дерева виразів, наприклад, щоб отримати вираз.

Коли ви реалізуєте дерево виразів, я б запропонував створити базовий клас Expression . Похідний від цього, клас BinaryExpression буде використовуватися для всіх двійкових виразів, таких як + та *. Тоді ви можете ввести VariableReferenceExpression до посилальних змінних (таких як a та b), та іншого класу ConstantExpression (для 2 із прикладу).

Дерево виразів у багатьох випадках будується в результаті аналізу вхідних даних (безпосередньо від користувача або з файлу). Для оцінки дерева виразів я б запропонував використовувати шаблон Visitor .


15

Коротка відповідь: Приємно мати можливість писати однаковий запит LINQ і спрямовувати його на будь-яке джерело даних. Ви не можете мати запит "Інтегрована мова" без нього.

Довга відповідь: Як ви, мабуть, знаєте, під час компіляції вихідного коду ви перетворюєте його з однієї мови на іншу. Зазвичай від мови високого рівня (C #) до нижнього важеля (IL).

В основному це можна зробити двома способами:

  1. Ви можете перекласти код за допомогою функції пошуку та заміни
  2. Ви аналізуєте код і отримуєте дерево синтаксичного аналізу.

Останнє і роблять усі програми, які ми знаємо як «компілятори».

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

Тепер у LINQ to SQL дерева виразів перетворюються на команду SQL, а потім надсилаються по дроту на сервер баз даних. Наскільки я знаю, вони не роблять нічого по-справжньому химерного при перекладі коду, але могли б . Наприклад, постачальник запитів може створювати різний код SQL залежно від мережевих умов.


6

IIUC, дерево виразів схоже на абстрактне дерево синтаксису, але вираз зазвичай має одне значення, тоді як AST може представляти цілу програму (з класами, пакетами, функцією, операторами тощо)

У будь-якому випадку, для виразу (2 + 3) * 5 дерево має вигляд:

    *
   / \ 
  +   5
 / \
2   3

Оцініть кожен вузол рекурсивно (знизу вгору), щоб отримати значення в кореневому вузлі, тобто значення виразу.

Звичайно, ви можете мати унарні (заперечення) або тринарні (if-then-else) оператори, а також функції (n-ary, тобто будь-яку кількість операцій), якщо ваша мова виразів це дозволяє.

Оцінка типів і контроль типу здійснюється на схожих деревах.


5


Дерева виразів DLR є доповненням до C # для підтримки динамічного виконання мови (DLR). DLR також є тим, що відповідає за надання нам методу "var" оголошення змінних. ( var objA = new Tree();)

Детальніше про DLR .

По суті, Microsoft хотіла відкрити CLR для динамічних мов, таких як LISP, SmallTalk, Javascript тощо. Для цього їм була необхідна можливість аналізу та оцінки виразів на льоту. Це було неможливо до появи DLR.

Повертаючись до мого першого речення, дерева виразів є доповненням до C #, що відкриває можливість використання DLR. До цього C # був набагато статичнішою мовою - усі типи змінних повинні були оголошуватися як певний тип, а весь код повинен був бути написаний під час компіляції.

Використовуючи його з
деревами виразу даних, відкриваються шлюзи для динамічного коду.

Скажімо, наприклад, що ви створюєте сайт нерухомості. На етапі проектування ви знаєте всі фільтри, які можна застосувати. Для реалізації цього коду у вас є два варіанти: ви можете написати цикл, який порівнює кожну точку даних із низкою перевірок If-Then; або ви можете спробувати створити запит динамічною мовою (SQL) і передати його програмі, яка може виконати пошук для вас (база даних).

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

(Докладніше: MSDN: Як: використовувати дерева виразів для побудови динамічних запитів ).

Крім даних
Основне використання дерев виразів - для управління даними. Однак їх також можна використовувати для динамічно генерованого коду. Отже, якщо вам потрібна функція, яка визначається динамічно (ala Javascript), ви можете створити дерево виразів, скомпілювати його та оцінити результати.

Я хотів би піти трохи глибше, але цей сайт робить набагато кращу роботу:

Експресійні дерева як укладач

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

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


Так, я знаю, що запізнився в грі, але я хотів написати цю відповідь як спосіб зрозуміти це сам. (Це запитання постало високо під час мого пошуку в Інтернеті.)
Річард

Хороша робота. Це хороша відповідь.
Річ Брайант

5
Ключове слово "var" не має нічого спільного з DLR. Ви плутаєте це з "динамікою".
Ярік,

Це хороша, невелика відповідь на var тут, яка показує, що Ярік правильний. Однак вдячний за решту відповідей. quora.com/…
Джонні

1
Це все неправильно. varє синтаксичним цукром під час компіляції - він не має нічого спільного з деревами виразів, DLR або часом роботи. var i = 0отримує компіляцію, як якщо б ви писали int i = 0, тому ви не можете використовувати varдля представлення типу, який невідомий під час компіляції. Дерева виразів не є "доповненням до підтримки DLR", вони введені в .NET 3.5, щоб дозволити LINQ. З іншого боку, DLR введено в .NET 4.0, щоб дозволити динамічні мови (наприклад, IronRuby) та dynamicключове слово. Дерева виразів насправді використовуються DLR для забезпечення взаємодії, це не навпаки.
faafak Gür

-3

Дерево виразів, яке ви посилаєтесь, є деревом оцінки виразів?

Якщо так, то це дерево, побудоване парсером. Парсер використовував Lexer / Tokenizer для ідентифікації Токенів із програми. Синтаксичний аналізатор будує бінарне дерево з лексем.

Ось детальне пояснення


Ну, хоча це правда, що Дерево виразів, на яке посилається OP, працює аналогічно і з тією ж базовою концепцією, що і дерево синтаксичного аналізу, це робиться динамічно під час виконання з кодом, однак зауважте при введенні компілятора Roslyn рядка розділення між ними стало справді розмитим, якщо не повністю видалити.
yoel halb
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.