Какие интерфейсы и типы для отложенного выполнения вы знаете в C#?

«Какие интерфейсы и типы для отложенного выполнения вы знаете в C#?» — вопрос из категории C# Core, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В C# и .NET отложенное (ленивое) выполнение — это стратегия, при которой вычисления откладываются до момента, когда результат действительно понадобится. Основные интерфейсы и типы:

1. IEnumerable<T> и IQueryable<T> (Отложенное выполнение запросов)

  • IEnumerable<T>: Базовый интерфейс для последовательностей. Запросы LINQ к IEnumerable (например, Where, Select) используют отложенное выполнение в памяти. Сама последовательность не обрабатывается, пока не начата итерация (например, в foreach или при вызове ToList()).

    var numbers = Enumerable.Range(1, 10); // Ещё ничего не вычислено
    var evenNumbers = numbers.Where(n => n % 2 == 0); // Построено выражение
    // Выполнение происходит здесь:
    foreach (var num in evenNumbers) { Console.WriteLine(num); }
    // Или при материализации:
    var list = evenNumbers.ToList();
  • IQueryable<T>: Наследует от IEnumerable<T>. Используется в LINQ провайдерах (как Entity Framework). Выражение запроса не выполняется немедленно, а преобразуется в другое представление (например, в SQL-запрос). Выполнение происходит при материализации или итерации.

    // EF Core пример
    var query = dbContext.Users
                        .Where(u => u.Age > 18)
                        .OrderBy(u => u.Name); // Строится выражение дерева
    // Запрос выполнится в БД только здесь:
    var adultUsers = query.ToList();

2. Lazy<T> (Отложенная инициализация)

  • Класс, который инкапсулирует логику создания ресурсоёмкого объекта. Объект создаётся ровно один раз, при первом обращении к свойству .Value.
  • Зачем? Для оптимизации: избегаем затрат на создание объекта, если он может не понадобиться. Гарантирует потокобезопасное создание по умолчанию.

    public class ExpensiveService
    {
        private readonly Lazy<HeavyResource> _resource = 
            new Lazy<HeavyResource>(() => new HeavyResource());
    
        public void Process()
        {
            // HeavyResource будет создан только при этом первом вызове
            _resource.Value.DoWork();
        }
    }

3. IObservable<T> / IEnumerable<T> (Pull vs Push)

  • IEnumerable представляет pull-модель: потребитель "вытягивает" данные (через foreach).
  • IObservable<T> (из библиотеки Reactive Extensions - Rx.NET) представляет push-модель: источник данных "проталкивает" уведомления подписчикам. Последовательность вычисляется и отправляется по мере готовности, что также является формой отложенного выполнения для асинхронных потоков событий.