В
Dart есть инструмент, который многие недооценивают. Это оператор
async*. Он позволяет создавать потоки данных так же легко, как обычные функции. Без лишних классов, ручного управления подписками и бесконечных циклов с обработчиками ошибок.
Что такое генераторы:
Генераторы - это функции, которые возвращают данные по частям, а не все сразу. Они позволяют экономить память и время выполнения. Но обычно такие функции синхронные. А что делать, если нужна асинхронность?
Здесь и вступает
async*. Вместо
Iterable такая функция возвращает
Stream. Внутри нее доступны все возможности генератора плюс
await.
Как это выглядит на практике:
Вот пример функции, которая генерирует числа с задержкой:
Stream generateNumbers() async* {
for (var i = 0; i < 5; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
Все просто. Мы используем привычные конструкции:
цикл,
await,
yield. Никаких
StreamController, никаких подписок, никаких ручных закрытий.
Что делает ключевое слово yield:
yield - это оператор, который возвращает очередное значение из генератора или асинхронного генератора. Когда функция помечена как
async*, она не выполняется целиком сразу. Вместо этого она возвращает
Stream. Каждый раз, когда выполнение доходит до
yield, значение отправляется в поток и функция приостанавливается до следующего запроса.
Простыми словами:
yield - это как
return, но не завершает функцию, а отдает одно значение и ждет, когда попросят следующее. Функция продолжает работу с того же места, а не начинает заново.
Есть еще
yield*, который передает управление другому генератору и отдает все его значения подряд. Это удобно, когда нужно объединить несколько источников данных в один поток.
Пример с реальной задачей:
Представьте, что нужно периодически опрашивать
API и отправлять обновления в поток. С
async* это выглядит так:
Stream watchStatus() async* {
while (true) {
final status = await api.getStatus();
yield status;
await Future.delayed(Duration(seconds: 30));
}
}
Код, который вызвал эту функцию, может потреблять обновления так, как ему удобно. Например, через
await for или подписку. Поток закроется, когда функция завершится, или будет работать бесконечно, пока его не остановят.
Почему это удобно:
Главное преимущество
async* - читаемость. Логика генерации данных находится в одном месте и выглядит как обычный код. Не нужно думать о том, как создать
StreamController, как добавить данные, как обработать ошибку, как закрыть поток. Все это берет на себя язык.
Еще один плюс - предсказуемость. Поток, созданный через
async*, ведет себя как обычный
Stream. Его можно слушать, преобразовывать, комбинировать с другими потоками.
Когда использовать async*:
async* стоит использовать всегда, когда нужно создать поток данных на основе какой-то логики. Это может быть опрос
API, чтение файла по частям, генерация событий, обработка очереди. Все, что требует последовательного выполнения с возможностью асинхронных операций.
Если же нужен полный контроль над потоком, например добавлять данные из разных мест или управлять подписками вручную, тогда
StreamController остается лучшим выбором.
Вывод:
async* - это не просто синтаксический сахар. Это способ писать потоки данных чисто и предсказуемо. Он экономит код, упрощает чтение и делает логику более явной. Логика генерации данных находится в одном месте и выглядит как обычный код. Все, что связано с созданием потока, обработкой ошибок и закрытием, берет на себя язык.