Зачем использовать Expression<Func<T>> вместо Func<T> в C#? Разница на примерах
Ключевое отличие: код 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 вы бы не смогли красиво отобразить правила — только выполнить их.
Ответов пока нет.
Войдите чтобы оставить ответ