Зачем использовать Expression<Func<T>> вместо Func<T> в C#? Объясните на примерах
Основное различие: код vs данные
Func<T> — это скомпилированный делегат. Вы передаёте лямбду, компилятор превращает её в IL-код, и вы можете только вызвать этот метод. Вы не можете проанализировать, что внутри: какие поля сравниваются, какие операции выполняются.
Expression<Func<T>> — это дерево выражений. Лямбда не компилируется в IL, а представляется в виде структуры данных, которую можно разобрать, изменить и только потом скомпилировать в делегат.
Практический пример: своя мини-ORM
Представьте, что вы пишете библиотеку для работы с базой данных. Вам нужно превратить C#-лямбду в SQL-запрос. С Func<T> это невозможно:
// ❌ Нельзя узнать, что за условие
public List<User> GetUsers(Func<User, bool> predicate) {
// Мы можем только вызвать predicate(user) в памяти
// Но не можем сгенерировать SQL "WHERE Age > 18"
}
С Expression<Func<T>> вы можете разобрать дерево:
// ✅ Можно проанализировать
public IQueryable<User> GetUsers(Expression<Func<User, bool>> predicate) {
// Разбираем Expression: находим MemberAccess (Age), Constant (18), GreaterThan
// Генерируем: SELECT * FROM Users WHERE Age > 18
return db.Users.Where(predicate); // EF сам это делает
}
Когда использовать Expression, а когда Func?
- Используйте
Func<T>, когда вам нужно просто выполнить код на стороне клиента (в памяти). Пример: сортировка, фильтрация LINQ-to-Objects, колбэки. - Используйте
Expression<Func<T>>, когда код нужно проанализировать, преобразовать или выполнить удалённо (БД, веб-сервис, удалённый сервер).
Как работать с Expression?
Дерево выражений можно скомпилировать в делегат в любой момент:
Expression<Func<int, int>> squareExpr = x => x * x;
Func<int, int> squareFunc = squareExpr.Compile(); // Теперь можно вызвать
Console.WriteLine(squareFunc(5)); // 25
Но главная сила — в анализе. Пример разбора простого выражения:
Expression<Func<int, bool>> isPositive = x => x > 0;
var binaryExpr = (BinaryExpression)isPositive.Body;
Console.WriteLine(binaryExpr.NodeType); // GreaterThan
Console.WriteLine(binaryExpr.Left); // x (ParameterExpression)
Console.WriteLine(binaryExpr.Right); // 0 (ConstantExpression)
Реальный сценарий из Entity Framework
Когда вы пишете:
var adults = context.Users.Where(u => u.Age > 18).ToList();
Параметр u => u.Age > 18 — это Expression<Func<User, bool>>. EF Core разбирает это дерево и генерирует SQL:
SELECT * FROM Users WHERE Age > 18
Если бы это был просто Func<User, bool>, EF пришлось бы загрузить всех пользователей в память и фильтровать их там — катастрофа для производительности.
Итог
Func<T>— чёрный ящик: вы видите только вход и выход.Expression<Func<T>>— прозрачная конструкция: вы видите все детали и можете их изменять.- Используйте
Expressionвезде, где нужна мета-программирование: ORM, построители запросов, кодогенерация, валидация на основе правил.
Дополнение: когда Expression — это единственный выход
Есть ситуации, где без Expression<Func<T>> не обойтись. Например, динамическое создание фильтров:
// Хотим фильтровать по любому свойству
public Expression<Func<T, bool>> BuildFilter<T>(string propertyName, object value) {
var param = Expression.Parameter(typeof(T), "x");
var property = Expression.Property(param, propertyName);
var constant = Expression.Constant(value);
var body = Expression.Equal(property, constant);
return Expression.Lambda<Func<T, bool>>(body, param);
}
// Использование:
var filter = BuildFilter<User>("Name", "Alice");
var users = context.Users.Where(filter).ToList();
С Func<T> вы бы не смогли динамически выбирать свойство — пришлось бы писать кучу switch-case или использовать рефлексию с потерей производительности.
Ещё один нюанс: Expression позволяет оптимизировать запросы. Например, можно объединить несколько предикатов через AndAlso или OrElse, построив сложное дерево, которое будет выполнено на стороне БД за один запрос.
Короткое правило: если ваш код должен "понимать" лямбду, а не просто вызывать её — используйте Expression<Func<T>>.
Ответов пока нет.
Войдите чтобы оставить ответ