Зачем нужны Enumerable, Observable, AsyncEnumerable и какие модели получения данных они реализуют?csharp-96

1. IEnumerable/IEnumerable

Назначение: Классическая pull-модель, где потребитель запрашивает данные по мере необходимости.

Характеристики:

  • Ленивое выполнение (deferred execution)
  • Последовательный доступ к элементам
  • Синхронная работа

Реализация:

IEnumerable<int> GetNumbers() {
    yield return 1;
    yield return 2;
}

Использование:

foreach (var num in GetNumbers()) {
    Console.WriteLine(num);
}

Преимущества:

  • Минимальное потребление памяти
  • Поддержка LINQ
  • Простота реализации

2. IObservable

Назначение: Реактивная push-модель, где источник сам уведомляет подписчиков о новых данных.

Характеристики:

  • Множественные подписчики
  • Асинхронные уведомления
  • Поддержка обработки ошибок и завершения

Реализация (System.Reactive):

IObservable<int> observable = Observable.Create<int>(observer => {
    observer.OnNext(1);
    observer.OnNext(2);
    return Disposable.Empty;
});

Использование:

var subscription = observable.Subscribe(
    x => Console.WriteLine(x),
    ex => Console.Error.WriteLine(ex),
    () => Console.WriteLine("Completed"));

Преимущества:

  • Реактивное программирование
  • Обработка потоков данных в реальном времени
  • Композиция событий

3. IAsyncEnumerable

Назначение: Асинхронная pull-модель для работы с потоками данных с ожиданием.

Характеристики:

  • Асинхронное получение элементов
  • Ленивое выполнение
  • Поддержка cancellation

Реализация:

async IAsyncEnumerable<int> GetAsyncNumbers() {
    await Task.Delay(100);
    yield return 1;
    await Task.Delay(100);
    yield return 2;
}

Использование:

await foreach (var num in GetAsyncNumbers()) {
    Console.WriteLine(num);
}

Преимущества:

  • Асинхронная работа с последовательностями
  • Эффективное потребление ресурсов
  • Интеграция с async/await

Сравнительная таблица

Модель Паттерн Особенности Типичные сценарии использования
IEnumerable<T> Pull Синхронный, ленивый LINQ, обработка коллекций
IObservable<T> Push Многоподписочный, реактивный UI события, потоковые данные
IAsyncEnumerable<T> Async Pull Асинхронный, ленивый Асинхронные потоки данных

Глубокое сравнение

Обработка данных

// IEnumerable
var sum = numbers.Sum();

// IObservable
observable.Aggregate(0, (acc, x) => acc + x)

// IAsyncEnumerable
await asyncNumbers.SumAsync();

Потребление памяти

  • IEnumerable: Минимальное (по одному элементу)
  • IObservable: Зависит от реализации (может буферизировать)
  • IAsyncEnumerable: Как IEnumerable, но с async overhead

Обработка ошибок

  • IEnumerable: Бросает исключения сразу
  • IObservable: OnError канал
  • IAsyncEnumerable: Бросает исключения при await

Практические примеры

IEnumerable:

// Чтение большого файла построчно
IEnumerable<string> ReadLines(string path) {
    using var reader = new StreamReader(path);
    while (!reader.EndOfStream)
        yield return reader.ReadLine();
}

IObservable:

// Отслеживание изменений файла
var watcher = new FileSystemWatcher();
IObservable<EventPattern<FileSystemEventArgs>> changes =
    Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
        h => watcher.Changed += h,
        h => watcher.Changed -= h);

IAsyncEnumerable:

// Асинхронное чтение из базы данных
async IAsyncEnumerable<Product> GetProductsAsync() {
    await using var connection = new SqlConnection(connString);
    await connection.OpenAsync();

    var command = new SqlCommand("SELECT * FROM Products", connection);
    await using var reader = await command.ExecuteReaderAsync();

    while (await reader.ReadAsync()) {
        yield return MapProduct(reader);
    }
}

Резюмируем:

каждая модель решает свои задачи - IEnumerable для синхронных pull-сценариев, IObservable для реактивных push-сценариев, и IAsyncEnumerable для асинхронных pull-потоков. Выбор зависит от требований к обработке данных: синхронность/асинхронность, push/pull модель, и необходимость реактивности.