Expression
Представьте: вы пишете C# код. Нужно передать условие фильтрации в метод. Что выбрать: Func<User, bool> или Expression<Func<User, bool>>? Вроде бы разница минимальна — оба принимают пользователя и возвращают bool. Но за кулисами скрывается целая вселенная различий.
На Stack Overflow этот вопрос набрал тысячи просмотров — и не зря. Сегодня мы разберемся, почему Expression<Func<T>> может быть мощнее, быстрее и безопаснее, чем обычный Func<T>. И самое главное — когда применять каждый из них, чтобы не стрелять себе в ногу.
Краткий ликбез: что такое Func и Expression?
Func<T> — это просто делегат. Готовая к выполнению функция, скомпилированный машинный код. Вы передаете ей данные — она их обрабатывает. Быстро, эффективно, но… непрозрачно.
Func<User, bool> isAdult = user => user.Age >= 18;
bool result = isAdult(someUser); // выполняется мгновенно
Expression<Func<T>> — это дерево выражений. Не код, а его описание. Вы не можете просто вызвать его — нужно либо скомпилировать в делегат (через .Compile()), либо проанализировать его структуру.
Expression<Func<User, bool>> expr = user => user.Age >= 18;
// Нельзя просто вызвать: expr(someUser) — ошибка компиляции
var compiled = expr.Compile();
bool result = compiled(someUser); // теперь можно
Казалось бы, зачем лишние телодвижения? Но именно эта «описательность» открывает дверь к магии.
Сценарий 1: LINQ to Objects (работа в памяти)
Когда вы работаете с коллекциями в памяти (например, List<T>), LINQ to Objects использует Func<T>. Деревья выражений здесь не нужны — все выполняется в том же процессе.
var users = new List<User>();
var adults = users.Where(u => u.Age >= 18).ToList(); // Func<User, bool>
Производительность отличная: никаких накладных расходов на разбор дерева. Если вы работаете только с локальными данными — смело используйте Func. Он быстрее и проще.
Сценарий 2: LINQ to SQL / Entity Framework (работа с БД)
А вот здесь начинается магия. Когда вы пишете:
var adults = dbContext.Users.Where(u => u.Age >= 18).ToList();
Entity Framework не выполняет ваш код на C#. Вместо этого он анализирует переданное дерево выражений и преобразует его в SQL-запрос:
SELECT * FROM Users WHERE Age >= 18
Если бы вы передали Func, EF бы не смог «заглянуть внутрь» — он бы просто выгрузил ВСЕХ пользователей из таблицы и выполнил фильтрацию в памяти. Представьте: таблица на 10 миллионов записей — и все они летят по сети в ваше приложение, чтобы отфильтроваться на клиенте.
Expression<Func<T>> позволяет EF (или любому другому IQueryable-провайдеру) прочитать ваше условие, перевести его на язык базы данных (SQL, MongoDB query, OData и т.д.) и выполнить фильтрацию на сервере.
Производительность: бенчмарки и реальные цифры
Давайте посмотрим на цифры. Возьмем простой пример: фильтрация 100 000 пользователей по возрасту.
// Вариант 1: Func — загружаем всех, фильтруем в памяти
var allUsers = dbContext.Users.ToList(); // SELECT * FROM Users
var adults1 = allUsers.Where(u => u.Age >= 18).ToList();
// Вариант 2: Expression — фильтруем на сервере
var adults2 = dbContext.Users.Where(u => u.Age >= 18).ToList(); // SELECT * FROM Users WHERE Age >= 18
Результаты (примерные, но показательные):
- Func: передано 100 000 записей (≈ 10-20 МБ данных) → время: ~500 мс
- Expression: передано 5 000 записей (≈ 1 МБ) → время: ~50 мс
Разница в 10 раз только на передаче данных. А если у вас сложные JOIN, агрегации или сортировки — преимущество деревьев выражений становится еще более очевидным.
Когда Expression может быть медленнее?
Честность превыше всего. Expression<Func<T>> имеет накладные расходы:
- Построение дерева выражений (если вы создаете его динамически через
Expression.AndAlso() и т.д.)
- Компиляция через
.Compile() — это дорогая операция (но выполняется один раз)
- Разбор дерева провайдером (например, EF анализирует Expression в SQL)
Если вы работаете только с локальными данными и не используете IQueryable — Func всегда будет быстрее. Не усложняйте там, где это не нужно.
Продвинутый сценарий: динамические запросы
Деревья выражений позволяют строить запросы на лету. Например, вы хотите дать пользователю возможность фильтровать по любым полям:
public IQueryable<User> FilterUsers(string propertyName, object value)
{
var param = Expression.Parameter(typeof(User), "u");
var property = Expression.Property(param, propertyName);
var constant = Expression.Constant(value);
var equals = Expression.Equal(property, constant);
var lambda = Expression.Lambda<Func<User, bool>>(equals, param);
return dbContext.Users.Where(lambda);
}
// Использование:
var result = FilterUsers("Email", "test@example.com").ToList();
С Func такое сделать невозможно — вы не можете «разобрать» делегат на составные части. А с Expression — легко.
А как же производительность компиляции?
Многие разработчики боятся .Compile() на деревьях выражений. Да, это не бесплатно. Но:
- Компиляция происходит один раз — результат кешируется
- После компиляции Expression работает с той же скоростью, что и обычный Func
- В реальных приложениях (EF, LINQ to SQL) компиляцию выполняет провайдер, а не вы
Если вы пишете свою библиотеку для работы с данными — используйте кеширование скомпилированных делегатов:
Блоги
Рекомендаций по книгам
Читайте также
Представьте: вы пишете C# код. Нужно передать условие фильтрации в метод. Что выбрать: Func<User, bool> или Expression<Func<User, bool>>? Вроде бы разница минимальна — оба принимают пользователя и возвращают bool. Но за кулисами скрывается целая вселенная различий.
На Stack Overflow этот вопрос набрал тысячи просмотров — и не зря. Сегодня мы разберемся, почему Expression<Func<T>> может быть мощнее, быстрее и безопаснее, чем обычный Func<T>. И самое главное — когда применять каждый из них, чтобы не стрелять себе в ногу.
Краткий ликбез: что такое Func и Expression?
Func<T> — это просто делегат. Готовая к выполнению функция, скомпилированный машинный код. Вы передаете ей данные — она их обрабатывает. Быстро, эффективно, но… непрозрачно.
Func<User, bool> isAdult = user => user.Age >= 18;
bool result = isAdult(someUser); // выполняется мгновенно
Expression<Func<T>> — это дерево выражений. Не код, а его описание. Вы не можете просто вызвать его — нужно либо скомпилировать в делегат (через .Compile()), либо проанализировать его структуру.
Expression<Func<User, bool>> expr = user => user.Age >= 18;
// Нельзя просто вызвать: expr(someUser) — ошибка компиляции
var compiled = expr.Compile();
bool result = compiled(someUser); // теперь можно
Казалось бы, зачем лишние телодвижения? Но именно эта «описательность» открывает дверь к магии.
Сценарий 1: LINQ to Objects (работа в памяти)
Когда вы работаете с коллекциями в памяти (например, List<T>), LINQ to Objects использует Func<T>. Деревья выражений здесь не нужны — все выполняется в том же процессе.
var users = new List<User>();
var adults = users.Where(u => u.Age >= 18).ToList(); // Func<User, bool>
Производительность отличная: никаких накладных расходов на разбор дерева. Если вы работаете только с локальными данными — смело используйте Func. Он быстрее и проще.
Сценарий 2: LINQ to SQL / Entity Framework (работа с БД)
А вот здесь начинается магия. Когда вы пишете:
var adults = dbContext.Users.Where(u => u.Age >= 18).ToList();
Entity Framework не выполняет ваш код на C#. Вместо этого он анализирует переданное дерево выражений и преобразует его в SQL-запрос:
SELECT * FROM Users WHERE Age >= 18
Если бы вы передали Func, EF бы не смог «заглянуть внутрь» — он бы просто выгрузил ВСЕХ пользователей из таблицы и выполнил фильтрацию в памяти. Представьте: таблица на 10 миллионов записей — и все они летят по сети в ваше приложение, чтобы отфильтроваться на клиенте.
Expression<Func<T>> позволяет EF (или любому другому IQueryable-провайдеру) прочитать ваше условие, перевести его на язык базы данных (SQL, MongoDB query, OData и т.д.) и выполнить фильтрацию на сервере.
Производительность: бенчмарки и реальные цифры
Давайте посмотрим на цифры. Возьмем простой пример: фильтрация 100 000 пользователей по возрасту.
// Вариант 1: Func — загружаем всех, фильтруем в памяти
var allUsers = dbContext.Users.ToList(); // SELECT * FROM Users
var adults1 = allUsers.Where(u => u.Age >= 18).ToList();
// Вариант 2: Expression — фильтруем на сервере
var adults2 = dbContext.Users.Where(u => u.Age >= 18).ToList(); // SELECT * FROM Users WHERE Age >= 18
Результаты (примерные, но показательные):
- Func: передано 100 000 записей (≈ 10-20 МБ данных) → время: ~500 мс
- Expression: передано 5 000 записей (≈ 1 МБ) → время: ~50 мс
Разница в 10 раз только на передаче данных. А если у вас сложные JOIN, агрегации или сортировки — преимущество деревьев выражений становится еще более очевидным.
Когда Expression может быть медленнее?
Честность превыше всего. Expression<Func<T>> имеет накладные расходы:
- Построение дерева выражений (если вы создаете его динамически через
Expression.AndAlso()и т.д.) - Компиляция через
.Compile()— это дорогая операция (но выполняется один раз) - Разбор дерева провайдером (например, EF анализирует Expression в SQL)
Если вы работаете только с локальными данными и не используете IQueryable — Func всегда будет быстрее. Не усложняйте там, где это не нужно.
Продвинутый сценарий: динамические запросы
Деревья выражений позволяют строить запросы на лету. Например, вы хотите дать пользователю возможность фильтровать по любым полям:
public IQueryable<User> FilterUsers(string propertyName, object value)
{
var param = Expression.Parameter(typeof(User), "u");
var property = Expression.Property(param, propertyName);
var constant = Expression.Constant(value);
var equals = Expression.Equal(property, constant);
var lambda = Expression.Lambda<Func<User, bool>>(equals, param);
return dbContext.Users.Where(lambda);
}
// Использование:
var result = FilterUsers("Email", "test@example.com").ToList();
С Func такое сделать невозможно — вы не можете «разобрать» делегат на составные части. А с Expression — легко.
А как же производительность компиляции?
Многие разработчики боятся .Compile() на деревьях выражений. Да, это не бесплатно. Но:
- Компиляция происходит один раз — результат кешируется
- После компиляции Expression работает с той же скоростью, что и обычный Func
- В реальных приложениях (EF, LINQ to SQL) компиляцию выполняет провайдер, а не вы
Если вы пишете свою библиотеку для работы с данными — используйте кеширование скомпилированных делегатов: