В чем разница между yield return и async/await в C#?

Ответ

yield return и async/await — это два разных механизма языка C# для реализации ленивого выполнения и асинхронности соответственно. Их нельзя смешивать.

yield return (Итераторы)

  • Назначение: Поэлементная ленивая генерация последовательностей (IEnumerable<T>) без загрузки всей коллекции в память.
  • Контекст: Работает строго в синхронном коде.
  • Управление потоком: Не освобождает текущий поток. Выполнение метода-итератора приостанавливается на yield return и возобновляется при следующем запросе элемента.

Пример:

IEnumerable<int> GenerateFibonacci(int count) {
    int a = 0, b = 1;
    for (int i = 0; i < count; i++) {
        yield return a;
        int temp = a;
        a = b;
        b = temp + b;
    }
}
// Использование: foreach (var num in GenerateFibonacci(10)) { ... }
// Память: в один момент времени в памяти только одно число.

async/await (Асинхронное программирование)

  • Назначение: Выполнение неблокирующих операций, обычно связанных с вводом-выводом (I/O): работа с сетью, файлами, БД.
  • Контекст: Работает в асинхронном контексте.
  • Управление потоком: Ключевое слово await освобождает текущий поток (например, поток пула) на время выполнения асинхронной операции, позволяя ему выполнять другую работу.

Пример:

async Task<string> DownloadContentAsync(string url) {
    using var httpClient = new HttpClient();
    // Поток освобождается на время загрузки
    string content = await httpClient.GetStringAsync(url);
    return content.ToUpper();
}

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

Характеристика yield return async / await
Возвращаемый тип IEnumerable<T>, IEnumerator<T> Task, Task<T>, ValueTask
Цель Ленивые последовательности Асинхронные операции (I/O, CPU-bound)
Освобождение потока Нет Да (на время await)
Ключевые слова yield return, yield break async, await
Можно ли совмещать? Нет. yield return нельзя использовать в async-методе. Нет. await нельзя использовать в методе с yield return.

Важно: Для асинхронного ленивого перечисления в .NET существует IAsyncEnumerable<T> с await foreach.

Ответ 18+ 🔞

Давай разберем эту тему, а то народ постоянно путает, будто это одно и то же. Нет, блядь, это как сравнивать велосипед и реактивный самолёт — оба транспорт, но задачи и принципы работы — пиздец какие разные.

yield return — это про ленивые последовательности, а не про асинхронность

Представь, что у тебя есть бесконечный список чисел, но грузить их все в память — это самоубийство. Вот yield return и придумали для такого. Он выплёвывает элементы по одному, прямо по требованию.

Как работает? Ты вызываешь метод, он доходит до yield return, отдаёт тебе значение и засыпает. Не освобождает поток, не делает никакой асинхронной магии — просто замирает до следующего вызова MoveNext(). Всё синхронно, чётко, предсказуемо.

IEnumerable<string> СчитатьФайлЛениво(string путь)
{
    using var reader = new StreamReader(путь);
    while (!reader.EndOfStream)
    {
        // Отдаём строку и замираем до следующего запроса
        yield return reader.ReadLine();
    }
}
// Используем
foreach (var строка in СчитатьФайлЛениво("log.txt"))
{
    Console.WriteLine(строка); // В память грузится только одна строка
}

Красота, да? Память не ебёт, всё летает. Но если файл на сетевом диске и чтение тормозит — то и foreach будет тормозить, потому что поток заблокирован. Вот тут и начинается боль.

async/await — это про то, чтобы не жрать поток попусту

А это уже другая история. Допустим, твоему приложению нужно скачать сто картинок из интернета. Если делать это синхронно, то поток будет тупо стоять столбом, пока сервер отвечает, а пользователь смотрит на зависший интерфейс и материт тебя последними словами.

async/await говорит потоку: «Знаешь что, иди займись чем-нибудь полезным, а я тебя позову, когда данные придут».

async Task<byte[]> СкачатьКартинкуAsync(string url)
{
    using var client = new HttpClient();
    // Вот тут поток ОСВОБОЖДАЕТСЯ и идёт делать другие дела
    byte[] data = await client.GetByteArrayAsync(url);
    return data; // Когда данные пришли, выполнение продолжается (возможно, уже в другом потоке)
}

Это про эффективность, а не про ленивость. Поток не висит вхолостую, пока ждёт ввод-вывод.

А что, их нельзя скрестить?

Вот тут-то и собака зарыта. НЕТ, БЛЯДЬ, НЕЛЬЗЯ. Язык тебе просто не даст.

  • Нельзя написать async-метод с yield return. Компилятор посмотрит на тебя как на идиота и скажет: «Чувак, ты в своём уме?».
  • Нельзя использовать await внутри метода-итератора (с yield return). Потому что yield — это state machine (машина состояний) для ленивости, а async — это state machine для асинхронности. Две разные машины в одной куче железа — получится пиздец, а не код.

Представь себе такую хуйню (КОД НЕРАБОЧИЙ, НЕ ПЫТАЙСЯ):

// ЭТО НЕ СКОМПИЛИРУЕТСЯ! ПРИМЕР БРЕДА.
IEnumerable<Task<string>> КакойТоБред()
{
    using var client = new HttpClient();
    for (int i = 0; i < 10; i++)
    {
        // yield хочет приостановить метод и вернуть управление КАРУСЕЛИ.
        // await хочет приостановить метод и вернуть управление ПОТОКУ.
        // Конфликт интересов, пиздец.
        yield return await client.GetStringAsync($"https://api.com/data/{i}");
    }
}

Так как же быть, если нужна и ленивость, и асинхронность?

А вот для этого, дружище, в C# 8.0 завезли отдельный, специальный инструментIAsyncEnumerable<T>. Это именно то, что нужно: асинхронно и лениво.

async IAsyncEnumerable<string> СчитатьФайлАсинхронноИЛениво(string путь)
{
    using var reader = new StreamReader(путь);
    while (!reader.EndOfStream)
    {
        // Читаем строку асинхронно, не блокируя поток
        string line = await reader.ReadLineAsync();
        // И лениво её отдаём
        yield return line;
    }
}

// Используем через await foreach
await foreach (var строка in СчитатьФайлАсинхронноИЛениво("bigfile.txt"))
{
    Console.WriteLine(строка);
}

Вот это — правильный путь. yield return для ленивости, await для асинхронного чтения, и всё это упаковано в специальный тип IAsyncEnumerable<T>, который все понимают.

Итог простой, как три копейки:

  • yield return — это «давай по одному, но прямо сейчас, в этом же потоке». Синхронная ленивость.
  • async/await — это «иди погуляй, поток, я тебя позову». Асинхронность без ленивости.
  • IAsyncEnumerable<T> с await foreach — это «давай по одному, и не тормози поток, пока ждёшь». Асинхронная ленивость. Их и нужно смешивать, когда это требуется.

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