JavaRush /Курсы /Swift SELF /Свой Sequence: прост...

Свой Sequence: простой пример генератора и обёртки

Swift SELF
18 уровень , 4 лекция
Открыта

1. Зачем делать свой Sequence

Если вы только-только подружились с массивами, вполне логично думать: «Ну и зачем мне ещё какая-то Sequence? Я же могу всё положить в [String] и перебирать». Можете — и часто это действительно лучший вариант. Но иногда вам не нужен готовый список. Иногда вам нужен поток значений, который появляется по мере необходимости: по одному, с возможностью остановиться в середине, не создавая промежуточные массивы и не выполняя лишнюю работу.

Здесь очень важна интуиция: Sequence — это про перебор, а не про хранение. Массив — это склад. Sequence — это конвейер, который по запросу подаёт следующую деталь. Причём конвейер может быть устроен по-разному: он может идти по готовому складу (обёртка), а может «производить» детали на лету (генератор). И именно поэтому у последовательности нет обязанности иметь count: ей не всегда известно, сколько элементов будет (или она не хочет считать это заранее). Это хорошо сочетается с тем, что для Sequence не требуется count, а часто доступны только оценки вроде underestimatedCount.

Чтобы зафиксировать картинку, держите маленькую табличку (без попытки запомнить наизусть):

Что у нас есть На что похоже Главная сила Типичная слабость
Array
«Склад» Быстрый доступ по индексу, есть count Нужно хранить всё сразу
Sequence
«Конвейер» Можно выдавать элементы по одному, можно быть бесконечной Нет индексов как гарантии, count может отсутствовать

2. Два способа получить свой Sequence

Слово «свой» обычно пугает, потому что кажется, что сейчас начнётся: «реализуйте протокол, напишите пять вложенных типов, поседейте». Но сегодня мы идём по дружелюбному пути: в стандартной библиотеке Swift уже есть фабрики, которые позволяют собрать последовательность, не объявляя никаких новых сложных типов.

Мы будем использовать две функции:

  • sequence(first:next:) — когда удобно мыслить «первый элемент + правило для следующего».
  • sequence(state:next:) — когда нужно хранить состояние (например, индекс в массиве) и уметь «пропускать» элементы, пока не найдёте подходящий.

А чтобы вам было проще читать дальнейшие примеры, зафиксируем главную идею механики перебора: компилятор мыслит примерно так — «создай итератор, затем вызывай next() пока не придёт nil». В документах и обсуждениях Swift очень часто показывают похожий каркас: тип Sequence даёт итератор, а итератор выдаёт Element? через next().

3. Генератор через sequence(first:next:)

Представим мини-приложение «учёт книг» в консоли. Пока без структур и классов (они будут позже), пусть книги — это просто названия строками. Допустим, мы хотим печатать список книг с «номерами полок» 1, 2, 3… но иногда нам нужно, например, только первые 3 номера, не обязательно строить массив [1, 2, 3, ...].

В этом месте и появляется генератор: последовательность чисел, которые появляются по одному.

Конечная последовательность: от 1 до 5

Нам нужен генератор, который остановится на 5. Обратите внимание: остановка выражается через nil.

import Foundation

let shelfNumbers = sequence(first: 1) { current in
    let next = current + 1
    return next <= 5 ? next : nil
}

for n in shelfNumbers {
    print("Полка №\(n)")
}
// Полка №1
// Полка №2
// Полка №3
// Полка №4
// Полка №5

Здесь есть маленькая «ловушка для мозга новичка»: first: 1 уже является первым элементом, а closure next вычисляет следующий. Поэтому условие остановки вы обычно пишете про next, а не про current.

Бесконечная последовательность: счётчик без конца

Иногда последовательность может быть бесконечной. Это не ошибка само по себе — ошибка начинается, когда вы пытаетесь превратить бесконечность в массив (спойлер: ваш компьютер не оценит).

import Foundation

let infinite = sequence(first: 1) { $0 + 1 }

var printed = 0
for x in infinite {
    print(x)
    printed += 1
    if printed == 3 { break }
}
// 1
// 2
// 3

Обратите внимание на важную привычку: если последовательность потенциально бесконечна — вы обязаны ограничить потребление (счётчиком, break, каким-то условием). Иначе цикл будет работать до тех пор, пока вы не закроете программу… или пока она не закроет вас.

4. Обёртка через sequence(state:next:)

С генератором всё приятно, но «обёртка» — это ещё полезнее: мы берём существующие данные (например, массив строк) и делаем последовательность, которая выдаёт только нужные элементы, не создавая промежуточный массив результатов.

Да, у нас уже есть filter. Но сегодня важен сам принцип: мы можем устроить перебор так, как нам удобно, и отдать это в for-in как единый поток.

Для примера возьмём «библиотеку» как массив названий:

import Foundation

let titles = [
    "Swift для начинающих",
    "Алгоритмы на массивах",
    "Swift Regex (страшно, но интересно)",
    "Паттерны с Dictionary",
    "Секреты Unicode в строках"
]

Допустим, мы хотим перебирать только книги, которые начинаются на "Swift" (без учёта регистра). Сделаем последовательность, которая внутри хранит индекс, двигается по массиву и возвращает следующий подходящий элемент.

Функция-обёртка, возвращающая «что-то перебираемое»

Под капотом sequence(state:next:) хранит состояние, а closure получает его как inout и может менять.

import Foundation

func titlesStartingWithSwift(_ titles: [String]) -> AnySequence<String> {
    let seq = sequence(state: 0) { (i: inout Int) -> String? in
        while i < titles.count {
            let current = titles[i]
            i += 1
            if current.lowercased().hasPrefix("swift") { return current }
        }
        return nil
    }
    return AnySequence(seq)
}

Здесь мы использовали AnySequence, чтобы «упаковать» последовательность и не заставлять себя (и компилятор в подсказках IDE) таскать длинное внутреннее имя типа. Это не новая магия — просто удобная коробка.

Теперь используем:

import Foundation

for t in titlesStartingWithSwift(titles) {
    print("Нашли:", t)
}
// Нашли: Swift для начинающих
// Нашли: Swift Regex (страшно, но интересно)

Почему это похоже на обёртку

Потому что данные (titles) уже есть, а мы лишь меняем способ чтения. Мы не копируем массив и не строим новый массив результатов. Мы строим «ленивый взгляд» на исходный массив: «иди по нему и выдавай только то, что подходит».

Кстати, именно такой стиль «обёрток» встречается и в стандартных обсуждениях Swift: берётся базовая коллекция, плюс правило, и вокруг этого делается wrapper, который выдаёт элементы иначе (например, «ленивое разбиение на части»).

5. Ранняя остановка: когда Sequence выгоднее filter

Сейчас может возникнуть справедливый вопрос: «Но titles.filter { ... } короче. Зачем городить sequence(state:next:)?» В большинстве случаев — не надо городить. Но важно понять ситуацию, когда Sequence-подход объективно делает меньше работы.

Представим, что нам нужно найти и вывести только первые 2 книги, начинающиеся на "Swift". Если вы сделаете filter, вы по умолчанию пройдёте весь массив, соберёте новый массив, и только потом возьмёте первые два. Если же вы идёте через последовательность и break, вы можете остановиться сразу после второго совпадения.

import Foundation

var printed = 0
for t in titlesStartingWithSwift(titles) {
    print(t)
    printed += 1
    if printed == 2 { break }
}
// Swift для начинающих
// Swift Regex (страшно, но интересно)

Этот «выигрыш» особенно заметен, когда исходные данные большие, а вам нужен маленький кусочек в начале. На уровне идеи это очень близко к тому, как работают lazy-обёртки в стандартной библиотеке: они стараются не делать работу заранее, а выполнять её при реальном потреблении.

6. Комбинации с map/filter/reduce

Важно почувствовать ещё одну вещь: когда вы сделали «перебираемую штуку», с ней можно работать как с любой другой последовательностью. Вы можете преобразовывать элементы, фильтровать дальше, считать, агрегировать.

Поскольку мы пока не изучали extension как тему проектирования API (это будет позже), мы сделаем всё «в лоб» прямо на месте: сначала получим последовательность, потом посчитаем элементы циклом.

Подсчёт подходящих элементов без построения массива

import Foundation

var count = 0
for _ in titlesStartingWithSwift(titles) {
    count += 1
}
print("Книг про Swift:", count) // Книг про Swift: 2

Вообще идея «посчитать элементы последовательности по условию» настолько типовая, что вокруг неё даже шли обсуждения добавления отдельного метода count(where:): чтобы не писать filter + count и не создавать промежуточные массивы.

Триггеры потребления: когда вычисления реально происходят

Тут полезно держать в голове «правило реальности»: последовательность делает работу, когда вы её потребляете. Потребление — это for-in, reduce, forEach, преобразование в Array(...). Если вы создали последовательность и ничего с ней не сделали — она, как ленивый кот, тоже ничего не сделает.

Мини-схема: что происходит при for-in

Чтобы не оставлять это «магией», нарисуем простую блок-схему. Она одинаково полезна и для понимания итератора, и для отладки бесконечных циклов.

flowchart TD
    A["for-in начинает цикл"] --> B["создаём итератор (внутри sequence...)"]
    B --> C["it.next()"]
    C -->|вернул Element| D["обрабатываем элемент в теле цикла"]
    D --> C
    C -->|вернул nil| E["цикл завершён"]

Ключевой момент: nil — это не «ошибка», а нормальный сигнал «элементы кончились». Именно поэтому next() возвращает Optional.

7. Типичные ошибки

Ошибка №1: забыли двигать состояние (i += 1) и получили «вечный цикл».
В sequence(state:next:) вы сами отвечаете за прогресс. Если забыть увеличить индекс, while будет снова и снова смотреть на один и тот же элемент. В лучшем случае — вы увидите бесконечную печать одного названия. В худшем — программа зависнет, а вы почувствуете себя персонажем фильма «День сурка», только без Билла Мюррея.

Ошибка №2: неверно поставили условие остановки и потеряли последний элемент (или добавили лишний).
В sequence(first:next:) часто ошибаются на один шаг: путают, проверять ли current или next. Помните простую мысль: first уже попал в последовательность; closure должен либо вернуть следующий элемент, либо nil. Если проверять не то, легко получить диапазон 1…4 вместо 1…5.

Ошибка №3: попытались материализовать бесконечную последовательность в массив.
Array(infiniteSequence) звучит как «давайте измерим бесконечность в килобайтах». У бесконечных последовательностей нет естественного конца, поэтому материализация не завершится. Если очень нужно превратить кусочек в массив, сначала ограничивайте потребление (break, счётчик, логика остановки), а уже потом думайте про Array(...).

Ошибка №4: спрятали во внешнем мире изменяемое состояние и получили непредсказуемость.
Если внутри next-логики вы используете внешние var, которые меняются где-то ещё, последовательность может вести себя «странно»: один проход даст одно, другой — другое. На уровне стандартной библиотеки это даже считается опасным, потому что некоторые алгоритмы ожидают согласованного поведения при переборе, а последовательности могут не иметь точного count и не обязаны быть «стабильными» как коллекции.

Ошибка №5: ожидаете от Sequence того, что обещает только Collection.
Это тихая логическая ошибка: вы сделали классную последовательность, а потом вдруг захотели «взять 10‑й элемент» или «узнать точное количество заранее». Для Sequence это не является обязанностью. Если вам нужна индексация и гарантии по границам — это уже зона Collection, и там правила строже.

1
Задача
Swift SELF, 18 уровень, 4 лекция
Недоступна
Нумерация полок
Нумерация полок
1
Задача
Swift SELF, 18 уровень, 4 лекция
Недоступна
Экономный счётчик
Экономный счётчик
1
Задача
Swift SELF, 18 уровень, 4 лекция
Недоступна
Полка подсказок
Полка подсказок
1
Задача
Swift SELF, 18 уровень, 4 лекция
Недоступна
Фибоначчи лимит
Фибоначчи лимит
1
Опрос
Коллекции Swift, 18 уровень, 4 лекция
Недоступен
Коллекции Swift
Протоколы, коллекции и итераторы
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ