Avatar ·

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

Всем привет! Я начал изучать C# и наткнулся на такую конструкцию: `Expression>`. Понятно, что `Func` — это просто делегат, который можно вызвать. Но зачем оборачивать его в `Expression<>`? Вроде бы можно просто передать лямбду как `Func` и вызвать её, когда нужно. Например, в Entity Framework я видел, что в методах вроде `.Where()` часто используют `Expression>`. Но если я пишу `.Where(x => x.Age > 18)`, то компилятор сам подставляет нужный тип. Зачем мне явно указывать `Expression<>`? У меня есть такой код: ```csharp Func square = x => x * x; Console.WriteLine(square(5)); // 25 ``` И такой: ```csharp Expression> squareExpr = x => x * x; // Как это использовать? Просто вызвать не получится. ``` Я понимаю, что `Expression<>` — это какое-то "дерево выражений", но не могу уловить практическую разницу. Когда мне нужно использовать `Expression>`, а когда достаточно `Func`? И почему в некоторых библиотеках (например, ORM) требуют именно `Expression<>`? Объясните, пожалуйста, на пальцах, с примерами. Спасибо! <<>>

Основное различие: код 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>>.