IEnumerable<T> vs IQueryable<T>: что выбрать в C#? Полный разбор

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

IEnumerable<T> vs IQueryable<T>: дуэль, которая решит судьбу вашего приложения

Вы когда-нибудь задумывались, почему ваш запрос к базе данных тормозит, хотя данных — кот наплакал? Или почему после добавления одной строчки кода приложение превращается в черепаху? Скорее всего, вы столкнулись с классической дилеммой: IEnumerable<T> или IQueryable<T>? Это не просто два интерфейса из мира C# и LINQ. Это два разных мира, два подхода к работе с данными. Один — для локальных коллекций, другой — для удаленных источников, таких как базы данных. В этой статье мы не просто перечислим различия, а разберем их на реальных примерах, чтобы вы больше никогда не попадали в ловушку производительности.

Кто есть кто: базовые понятия

Давайте начнем с основ. IEnumerable<T> — это интерфейс, который представляет собой последовательность элементов в памяти. Он идеально подходит для работы с коллекциями, такими как List<T>, Array или Dictionary. Когда вы используете IEnumerable, все операции (фильтрация, сортировка) выполняются на стороне клиента, то есть в вашем приложении.

IQueryable<T> — это более продвинутый интерфейс, предназначенный для работы с внешними источниками данных (базы данных, веб-сервисы). Он не хранит данные, а строит дерево выражений (Expression Tree). Это позволяет провайдеру LINQ (например, Entity Framework) преобразовать ваш C#-код в эффективный SQL-запрос и выполнить его на стороне сервера.

Ключевая фраза, которую нужно запомнить: IEnumerable работает в памяти, IQueryable — на стороне источника данных.

Главное отличие: где и когда выполняется логика

Представьте, что вам нужно отфильтровать список из 10 000 товаров, которые дороже 1000 рублей, и отсортировать их по цене.

Пример 1: IEnumerable (фильтрация в памяти)

// Предположим, products — это List<Product> (IEnumerable)
var expensiveProducts = products
    .Where(p => p.Price > 1000)
    .OrderBy(p => p.Price)
    .ToList();

Что здесь происходит? Сначала в память загружаются ВСЕ 10 000 товаров. Затем C# применяет фильтр Where и сортировку OrderBy уже в вашем приложении. Если у вас 10 000 записей, это не критично. Но если их 10 миллионов — приложение упадет или зависнет.

Пример 2: IQueryable (фильтрация на сервере БД)

// Предположим, dbContext.Products — это DbSet<Product> (IQueryable)
var expensiveProducts = dbContext.Products
    .Where(p => p.Price > 1000)
    .OrderBy(p => p.Price)
    .ToList();

Здесь магия: IQueryable не загружает данные сразу. Вместо этого он строит SQL-запрос: SELECT * FROM Products WHERE Price > 1000 ORDER BY Price. Этот запрос отправляется в базу данных, и она возвращает только отфильтрованные и отсортированные данные. В памяти вы получаете только то, что вам нужно. Экономия ресурсов колоссальная.

Практический пример: ловушка с ToList()

Одна из самых частых ошибок начинающих разработчиков — это преждевременный вызов ToList() или ToArray(). Это превращает IQueryable в IEnumerable.

Плохой код (убивает производительность):

var allProducts = dbContext.Products.ToList(); // Загружаем ВСЕ данные в память!
var filteredProducts = allProducts.Where(p => p.Price > 1000).ToList();

В этом примере мы сначала загружаем всю таблицу Products в память, а потом фильтруем ее. Это эквивалентно тому, чтобы выгрузить весь склад на парковку, а потом искать в куче нужную коробку.

Хороший код (оптимальный):

var filteredProducts = dbContext.Products
    .Where(p => p.Price > 1000)
    .ToList(); // Загружаем только то, что нужно

Запомните правило: всегда старайтесь отложить материализацию данных (ToList, ToArray, First, Single) до последнего момента. Это дает LINQ-провайдеру максимум информации для построения оптимального SQL-запроса.

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

  • Работа с коллекциями в памяти: списки, массивы, словари. Если данные уже находятся в оперативной памяти, IEnumerable — ваш выбор.
  • Пост-обработка данных: после того как вы получили данные из БД через IQueryable и сохранили их в список, вы можете использовать IEnumerable для дальнейших сложных преобразований, которые невозможно выразить в SQL.
  • Небольшие объемы данных: если вы уверены, что данных всегда будет мало (например, список стран), то разница в производительности будет незаметна.

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

  • Работа с базами данных через Entity Framework, LINQ to SQL или другие ORM. Это основной сценарий.
  • Динамические запросы: когда вам нужно построить сложный запрос в зависимости от условий (фильтры, сортировка, пагинация). IQueryable позволяет добавлять условия "на лету", не загружая лишних данных.
  • Большие объемы данных: если вы работаете с таблицами, содержащими миллионы записей, использование IQueryable — это не просто рекомендация, а необходимость для выживания приложения.

Глубокое погружение: Expression Trees и Delegates

Чтобы понять разницу на уровне архитектуры, нужно взглянуть на сигнатуры методов Where.

IEnumerable.Where принимает Func<T, bool> — это просто делегат, указывающий на метод. Компилятор C# превращает его в инструкции IL (промежуточный язык). Когда код выполняется, он просто вызывает этот метод для каждого элемента коллекции.

IQueryable.Where принимает Expression<Func<T, bool>> — это дерево выражений. Это не скомпилированный код, а его абстрактное синтаксическое дерево. LINQ-провайдер (например, для SQL) "ходит" по этому дереву и переводит его в SQL-запрос. Именно это и позволяет выполнять логику на стороне сервера БД.

Попробуйте передать Expression<Func<T, bool>> в метод, ожидающий IEnumerable — получите ошибку компиляции. И наоборот, передача Func в

Блоги

Рекомендаций по книгам