Что такое дерево выражений (Expression Tree) в C#?

Ответ

Дерево выражений (Expression Tree) — это древовидная структура данных, которая представляет код на этапе выполнения в виде данных, а не исполняемых инструкций. Это позволяет анализировать, модифицировать или транслировать код (например, в SQL-запрос) перед его выполнением.

Основное отличие от делегата:

  • Делегат Func<int, int>: x => x + 1 — это скомпилированный код для немедленного выполнения.
  • Выражение Expression<Func<int, int>>: x => x + 1 — это данные, описывающие структуру этого лямбда-выражения (узел сложения с параметром x и константой 1).

Пример создания и компиляции дерева выражений "вручную":

using System.Linq.Expressions;

// Цель: создать выражение, эквивалентное (x) => x * x + 2

// 1. Параметр выражения.
ParameterExpression paramX = Expression.Parameter(typeof(int), "x");

// 2. Константа '2'.
ConstantExpression constantTwo = Expression.Constant(2, typeof(int));

// 3. Умножение: x * x.
BinaryExpression multiply = Expression.Multiply(paramX, paramX);

// 4. Сложение: (x * x) + 2.
BinaryExpression addBody = Expression.Add(multiply, constantTwo);

// 5. Построение лямбда-выражения.
Expression<Func<int, int>> lambdaExpression =
    Expression.Lambda<Func<int, int>>(addBody, paramX);

// 6. Дерево выражений можно проанализировать.
Console.WriteLine(lambdaExpression.Body); // Выведет: ((x * x) + 2)

// 7. Компиляция дерева в исполняемый делегат.
Func<int, int> compiledDelegate = lambdaExpression.Compile();

// 8. Вызов.
int result = compiledDelegate(5); // (5 * 5) + 2 = 27
Console.WriteLine(result);

Практическое применение:

  1. LINQ to SQL/Entity Framework: Когда вы пишете db.Users.Where(u => u.Age > 18), выражение u => u.Age > 18 передаётся как Expression<Func<User, bool>>. Поставщик EF анализирует это дерево и преобразует его в SQL WHERE Age > 18, а не выполняет фильтрацию в памяти.
  2. Динамическая генерация кода: Создание специализированных методов на лету для оптимизации.
  3. Валидация и анализ кода: Проверка структуры выражений перед выполнением.

Ответ 18+ 🔞

Давай разберёмся, что за зверь такой — дерево выражений. Представь, что ты пишешь лямбду, типа x => x + 1. Обычно это просто функция, которую можно вызвать — и всё, поезд ушёл. Но если обернуть её в Expression<Func<int, int>>, то вместо готового кода ты получаешь, по сути, схему этого кода. Как чертёж двигателя вместо самого двигателя. И с этим чертежом можно делать что угодно: разобрать по винтикам, пересобрать или, например, перевести на язык SQL. Это и есть Expression Tree.

Чем отличается от обычного делегата?

  • Func<int, int> — это уже скомпилированная инструкция «взять число и прибавить единицу». Выполняй — и не морочь голову.
  • Expression<Func<int, int>> — это данные, которые говорят: «слушай, тут у нас операция сложения, слева — параметр с именем 'x', справа — константа '1'». И всё это в виде дерева объектов, которое можно обойти.

Собираем дерево своими руками, как конструктор

Вот тебе пример, как собрать выражение x => x * x + 2 с нуля, из кусочков:

using System.Linq.Expressions;

// Хотим собрать (x) => x * x + 2

// 1. Сначала объявляем параметр — нашу 'x'.
ParameterExpression paramX = Expression.Parameter(typeof(int), "x");

// 2. Константа '2'. Просто число.
ConstantExpression constantTwo = Expression.Constant(2, typeof(int));

// 3. Умножаем x на x. Получаем узел умножения.
BinaryExpression multiply = Expression.Multiply(paramX, paramX);

// 4. К результату умножения прибавляем двойку. Получаем тело выражения.
BinaryExpression addBody = Expression.Add(multiply, constantTwo);

// 5. Упаковываем всё это в лямбда-выражение, указывая параметр.
Expression<Func<int, int>> lambdaExpression =
    Expression.Lambda<Func<int, int>>(addBody, paramX);

// 6. Можем посмотреть, что внутри. Это же данные!
Console.WriteLine(lambdaExpression.Body); // Напечатает: ((x * x) + 2)

// 7. Но если надо — компилируем это дерево обратно в быстрый делегат.
Func<int, int> compiledDelegate = lambdaExpression.Compile();

// 8. И выполняем.
int result = compiledDelegate(5); // (5 * 5) + 2 = 27
Console.WriteLine(result);

Зачем это всё, спрашивается, нужно?

А вот, блядь, где начинается магия:

  1. Вся мощь Entity Framework / LINQ to SQL построена на этом. Когда ты пишешь db.Users.Where(u => u.Age > 18), твоё условие u => u.Age > 18 приходит не как готовая функция, а именно как дерево выражений. EF, этот хитрожопый фреймворк, разбирает это дерево, видит там обращение к свойству Age и операцию «больше», и переводит это в чистый SQL: WHERE Age > 18. Если бы это была обычная функция, он бы тупо выгрузил всю таблицу и фильтровал в памяти — пиздец производительности.

  2. Динамическая генерация кода. Можно на лету собирать оптимальные методы под конкретную задачу, когда заранее не знаешь, что понадобится.

  3. Анализ и валидация. Можешь поковыряться в «чертеже» кода, прежде чем его запускать. Проверить, нет ли там подозрительных операций.

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