Avatar ·

Зачем использовать Expression<Func<T>> вместо Func<T> в C#? Разница на примерах

Привет, коллеги. Я начал изучать C# и наткнулся на такую штуку как Expression>. Вроде бы и Func и Expression> выглядят одинаково, когда я пишу лямбды. Но зачем тогда нужно это "Expression"? Вот простой пример. У меня есть метод, который принимает Func: ```csharp public void Filter(Func predicate) { var numbers = new List {1, 2, 3, 4, 5}; var result = numbers.Where(predicate).ToList(); // делаю что-то с result } ``` Я вызываю его так: `Filter(x => x > 3);` — всё работает. Но я видел, что в Entity Framework часто используют Expression>. Например: ```csharp public IQueryable GetData(Expression> predicate) { return dbSet.Where(predicate); } ``` В чём принципиальная разница? Почему нельзя везде использовать просто Func? Я понимаю, что Expression как-то связан с деревьями выражений, но не могу уловить, зачем это нужно на практике. Когда я должен выбирать Expression, а когда Func? И что происходит "под капотом"? Буду благодарен за развёрнутое объяснение с примерами. <<>>

Ключевое отличие: код vs данные

Основная разница между Func<T> и Expression<Func<T>> в том, что Func<T> — это скомпилированный делегат (готовый к выполнению код на IL), а Expression<Func<T>> — это дерево выражений, то есть структура данных, описывающая код в виде объектов. Это как разница между готовым блюдом и рецептом его приготовления.

Как это работает на практике

Рассмотрим простую лямбду: x => x > 3.

  • Func<int, bool> — компилятор сразу превращает её в IL-код. Вы не можете "заглянуть внутрь" — только вызвать: predicate(5) вернёт true.
  • Expression<Func<int, bool>> — компилятор строит дерево объектов: ParameterExpression (x), GreaterThan, Constant (3). Вы можете анализировать, изменять и даже транслировать это дерево в другой язык (например, SQL).

Пример: разбор дерева выражений

Expression<Func<int, bool>> expr = x => x > 3;
// expr.Body — это BinaryExpression (GreaterThan)
// expr.Parameters[0] — параметр x

// Вы можете "разобрать" выражение:
var binary = (BinaryExpression)expr.Body;
var left = (ParameterExpression)binary.Left;    // x
var right = (ConstantExpression)binary.Right;  // 3
Console.WriteLine($"{left.Name} {binary.NodeType} {right.Value}");
// Вывод: x GreaterThan 3

Главный сценарий: LINQ to SQL / Entity Framework

Когда вы пишете db.Users.Where(u => u.Age > 18), EF Core принимает Expression<Func<User, bool>>. Он не выполняет эту лямбду в C# — он анализирует дерево выражений и преобразует его в SQL:

SELECT * FROM Users WHERE Age > 18

Если бы вы передали Func<User, bool>, EF пришлось бы скачать всех пользователей из БД, а затем фильтровать их в памяти — это катастрофически медленно.

Когда использовать что?

  • Func<T> — когда вам нужно выполнить код "здесь и сейчас" (фильтрация в памяти, обработка коллекций List<T>, IEnumerable).
  • Expression<Func<T>> — когда код будет интерпретироваться или транслироваться (ORM, построение динамических запросов, валидация правил, AOP).

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

// Создаём фильтр "Age > 18" программно
ParameterExpression param = Expression.Parameter(typeof(User), "u");
ConstantExpression const18 = Expression.Constant(18);
BinaryExpression body = Expression.GreaterThan(
    Expression.Property(param, "Age"), const18);
var lambda = Expression.Lambda<Func<User, bool>>(body, param);

// Используем в EF Core
var adults = db.Users.Where(lambda).ToList();

С Func такое сделать невозможно — вы не можете "собрать" делегат из частей во время выполнения.

Производительность

Expression имеет накладные расходы на компиляцию. Если вы вызываете .Compile() на выражении, получается обычный делегат, но это медленнее, чем прямая передача Func. Поэтому для простых операций в памяти используйте Func.

Итог: Expression — это мощный инструмент для метапрограммирования. Он позволяет обращаться с кодом как с данными, что необходимо для ORM, построителей запросов и динамической компиляции.

<<>>

Дополнение: стек-трейс и отладка

Есть ещё одно практическое отличие, которое может быть полезно при отладке. Когда вы передаёте Expression, вы можете получить читаемое строковое представление лямбды:

Expression<Func<int, bool>> expr = x => x * 2 + 1 > 10;
Console.WriteLine(expr.ToString()); // "x => ((x * 2) + 1) > 10"

С Func вы получите только "System.Func`2[System.Int32,System.Boolean]" — никакой информации о логике. Это особенно полезно в логировании или при построении сообщений об ошибках валидации.

Когда Expression может быть избыточным

Не пытайтесь везде использовать Expression. Если вы работаете с List<T> или IEnumerable (LINQ to Objects), передача Expression заставит вас вызывать .Compile() для каждого элемента, что убивает производительность. Пример плохого кода:

// ПЛОХО: Expression для in-memory коллекции
List<int> numbers = new List<int> { 1, 2, 3 };
var filtered = numbers.Where(expr.Compile()); // компиляция на каждый вызов

Правильно использовать Func для in-memory операций и Expression только когда нужна трансляция в другой контекст (SQL, удалённый вызов).

Ещё один пример: валидация правил

Вы можете хранить правила валидации как Expression и анализировать их:

var rules = new List<Expression<Func<User, bool>>> {
    u => u.Age >= 18,
    u => !string.IsNullOrEmpty(u.Name)
};
// Можно вывести все правила пользователю:
foreach (var rule in rules)
    Console.WriteLine(rule.ToString());

С Func вы бы не смогли красиво отобразить правила — только выполнить их.