Значения приходят по одному, а не все сразу
Обычная коллекция — массив или словарь — уже вся целиком лежит в памяти: вы просто перебираете то, что есть. Но бывают источники данных, у которых значений заранее нет: строки, которые приходят по сети одна за другой, события от датчика, сообщения из чата, отсчёты таймера. Значение может появиться через секунду, а может через час, и до этого момента цикл должен ждать, не занимая поток целиком.
Для таких случаев в Swift есть асинхронная последовательность — тип, который отдаёт значения по одному, каждый раз с возможной паузой перед следующим.
AsyncSequence и цикл for await
Официально это называется AsyncSequence — протокол, похожий на обычный Sequence, только его метод получения следующего элемента асинхронный. Перебирать такую последовательность позволяет специальная форма цикла:
for await score in scoreStream() {
print(score)
}
Внешне это обычный for-in с добавленным словом await. Но смысл другой: перед каждой итерацией выполнение может приостановиться и подождать, пока источник не подготовит очередное значение. Пока идёт ожидание, поток не простаивает впустую — он свободен для другой работы, ровно как при обычном await у асинхронной функции. Цикл завершается сам, когда последовательность сообщает, что значений больше не будет.
Что скрыто внутри протокола
Устроено это похоже на обычный Sequence: там итератор синхронно отдаёт следующий элемент методом next(), а у AsyncSequence есть свой асинхронный итератор с методом:
mutating func next() async -> Element?
for await — это просто короткая запись вызова такого next() в цикле: пока он возвращает значение, цикл выполняет тело, а как только приходит nil — останавливается. Писать это вручную почти никогда не нужно: готовые реализации AsyncSequence берут на себя всю асинхронную механику, а вам остаётся только for await.
AsyncStream: готовый поток значений
Чаще всего свою асинхронную последовательность не пишут с нуля, а собирают из готового типа AsyncStream. Ему достаточно один раз объяснить, откуда брать значения:
func countUp(to limit: Int) -> AsyncStream<Int> {
AsyncStream { continuation in
var current = 1
while current <= limit {
continuation.yield(current)
current += 1
}
continuation.finish()
}
}
Функция ничего не помечает как async — она обычная и просто возвращает готовый AsyncStream<Int>. А вот воспользоваться результатом можно только через for await:
for await number in countUp(to: 3) {
print(number)
}
// напечатает 1, 2 и 3, каждое на отдельной строке
continuation: как наполнять поток изнутри
continuation — это объект, который получает замыкание внутри AsyncStream { ... }, и через него поток наполняется значениями:
continuation.yield(value)— добавляет очередное значение, его получит следующая итерацияfor await;continuation.finish()— сообщает, что значений больше не будет, и циклfor awaitзавершится.
По умолчанию AsyncStream буферизует всё, что передано через yield, даже если пока никто не читает поток циклом — значения просто дождутся своей очереди. Именно поэтому в примере выше все три числа успевают попасть в поток ещё до того, как начнётся перебор.
Мост из колбэков в поток
Официальная терминология для этого — «мост» (bridge): оборачивание старого API на колбэках в современный AsyncSequence. Представим, что где-то в проекте есть функция, сообщающая о результатах через замыкания, а не через async:
func fetchScores(onScore: @escaping (Int) -> Void, onDone: @escaping () -> Void) {
for score in [10, 20, 30] {
onScore(score)
}
onDone()
}
Такую функцию легко превратить в поток: колбэк onScore вызывает continuation.yield, а onDone — continuation.finish:
func scoreStream() -> AsyncStream<Int> {
AsyncStream { continuation in
fetchScores(
onScore: { continuation.yield($0) },
onDone: { continuation.finish() }
)
}
}
После этого весь остальной код может забыть про колбэки и работать через привычный for await, как с любой другой асинхронной последовательностью. Такой мост особенно полезен, когда старый код на колбэках менять нельзя или не хочется, а новый писать удобнее в стиле async/await.
Частые ошибки
- Забыть вызвать
continuation.finish()— тогдаfor awaitбудет вечно ждать следующее значение, которое никогда не придёт, и программа зависнет. - Пытаться перебрать
AsyncStreamобычнымfor-inбезawait— компилятор такое не пропустит: асинхронную последовательность нельзя перебирать синхронно. - Вызывать
continuation.yieldуже послеcontinuation.finish()— поток уже закрыт, и такие значения будут проигнорированы. - Думать, что
awaitвfor awaitозначает фиксированную паузу — на самом деле это просто точка приостановки, которая срабатывает мгновенно, если следующее значение уже готово в буфере.
Резюме
AsyncSequence— протокол для последовательностей, значения которых появляются со временем, а не все сразу.for await value in sequenceперебирает такую последовательность, приостанавливаясь перед каждым новым значением.AsyncStream— готовый тип для сборки собственного асинхронного потока без ручной реализации протокола.continuation.yieldдобавляет значение в поток,continuation.finish()сообщает о его завершении.- Через
continuationудобно строить мост: превращать старые API на колбэках в современныйAsyncSequence.
Проверьте себя
3 вопроса
Поток чётных чисел
Реализуйте функцию evenStream(upTo limit: Int) -> AsyncStream<Int>, которая строит асинхронный поток на основе AsyncStream.
Поток должен по очереди отдать все чётные числа от 2 до limit включительно, по возрастанию, а затем корректно завершиться (после последнего числа continuation.finish() обязан быть вызван, иначе перебор через for await зависнет).
Если подходящих чётных чисел нет (например, limit меньше 2), поток должен сразу завершиться, не отдав ни одного значения.