🎯 Kotlin

Iteratoren und Generatoren in Kotlin: Eine ausführliche Anleitung

Что это такое и зачем? (аналогия из реальной жизни)

Представьте, что вы — библиотекарь, а перед вами стоит огромный стеллаж с книгами. Вам нужно выписать названия всех книг, которые стоят на полках. У вас есть два способа это сделать:

Способ 1 (без итератора): Вы берёте стопку бумаги, подходите к стеллажу, снимаете все книги разом, тащите их к столу, выписываете названия, а потом несёте обратно. Если книг 10 — это нормально. А если их 10 миллионов? Вы просто не сможете их все унести, у вас не хватит места на столе, да и времени на то, чтобы таскать их туда-сюда, уйдёт масса.

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

А теперь представьте, что вам нужно не просто перебрать книги, а создать для кого-то список книг, которых ещё нет в библиотеке, но они появятся в будущем. Вы не можете дать человеку сразу все 10 миллионов названий — их же ещё нет! Вместо этого вы говорите: «Когда тебе понадобится следующее название — просто спроси у меня, и я его придумаю на ходу». Это и есть генератор — функция, которая не выдаёт всё сразу, а «рождает» значения по одному, только когда их просят.

Итератор — это «указка», которая движется по коллекции (списку, массиву, строке) и показывает на текущий элемент. Генератор — это «фабрика», которая производит элементы по запросу, не храня их все в памяти.

Зачем это нужно в программировании? Представьте, что вы обрабатываете данные с датчика температуры. Датчик выдаёт миллион показаний в секунду. Если вы сохраните их все в список, программа «упадёт» от нехватки памяти. А если вы будете обрабатывать их как генератор — получили одно значение, обработали, забыли, получили следующее — то памяти нужно ровно на одно значение. Экономия колоссальная!

Теория: как это работает изнутри

Чтобы понять итераторы и генераторы в Kotlin, нужно разобраться с двумя ключевыми понятиями: интерфейс и состояние.

Интерфейс Iterator

В Kotlin любой итератор — это объект, который реализует интерфейс Iterator. У этого интерфейса всего два метода:

  • hasNext(): Boolean — проверяет, есть ли ещё элементы. Возвращает true, если следующий элемент существует, и false, если мы дошли до конца.
  • next(): T — возвращает следующий элемент и передвигает «указку» дальше.

Внутри итератора всегда хранится текущее состояние — например, индекс текущего элемента в массиве. Каждый вызов next() изменяет это состояние (увеличивает индекс на 1).

Важно: Итератор — это одноразовый объект. Вы можете пройти по коллекции только один раз. Если нужно пройти ещё раз — создавайте новый итератор.

Как Kotlin делает итерацию удобной

В Kotlin есть специальная конструкция for, которая автоматически использует итератор. Когда вы пишете:

for (элемент in коллекция) { ... }

Компилятор Kotlin превращает это в такой код:

val итератор = коллекция.iterator()
while (итератор.hasNext()) {
    val элемент = итератор.next()
    // ... ваши действия
}

То есть for — это просто «синтаксический сахар», который делает код красивее, но под капотом всё равно работают hasNext() и next().

Генераторы: ленивые последовательности

Генератор в Kotlin — это функция, помеченная ключевым словом sequence. Она не выполняется сразу, а создаёт специальный объект — Sequence. Этот объект хранит в себе не данные, а алгоритм их получения.

Когда вы вызываете next() у итератора такой последовательности, выполняется кусочек кода генератора до следующего ключевого слова yield. yield — это команда «выдать значение и приостановиться». После этого выполнение генератора «засыпает» до следующего запроса.

Это называется ленивые вычисления (lazy evaluation) — значения вычисляются только тогда, когда они реально нужны, а не заранее.

Синтаксис в Kotlin: все формы и варианты

1. Создание итератора для своей коллекции

Если вы пишете свой класс, который должен поддерживать итерацию, нужно реализовать интерфейс Iterable (который содержит метод iterator()) или Iterator напрямую.

// Создаём свой класс, который можно перебирать в цикле for
class СписокЧисел(private val числа: List<Int>) : Iterable<Int> {
    
    // Переопределяем метод iterator(), который возвращает наш собственный итератор
    override fun iterator(): Iterator<Int> {
        return ЧисловойИтератор(числа)
    }
}

// Создаём класс итератора
class ЧисловойИтератор(private val числа: List<Int>) : Iterator<Int> {
    
    // Храним текущую позицию (начинаем с -1, так как первый элемент будет с индексом 0)
    private var текущаяПозиция = -1
    
    // Проверяем, есть ли следующий элемент
    override fun hasNext(): Boolean {
        // Следующий элемент есть, если текущая позиция + 1 меньше размера списка
        return текущаяПозиция + 1 < числа.size
    }
    
    // Получаем следующий элемент
    override fun next(): Int {
        // Если следующего элемента нет — выбрасываем исключение
        if (!hasNext()) {
            throw NoSuchElementException("Больше нет элементов в списке")
        }
        // Перемещаемся на следующую позицию
        текущаяПозиция++
        // Возвращаем элемент на этой позиции
        return числа[текущаяПозиция]
    }
}

// Использование
fun main() {
    val мойСписок = СписокЧисел(listOf(10, 20, 30, 40, 50))
    
    // Цикл for автоматически использует наш итератор
    for (число in мойСписок) {
        println("Текущее число: $число")
    }
}

2. Функции-генераторы с sequence

Самый распространённый способ создания генератора в Kotlin — использовать функцию sequence и ключевое слово yield.

// Создаём генератор, который выдаёт числа от 1 до 5
val простойГенератор = sequence {
    // yield "выдаёт" одно значение и приостанавливается
    yield(1)
    yield(2)
    yield(3)
    yield(4)
    yield(5)
}

fun main() {
    // Перебираем значения из генератора
    for (значение in простойГенератор) {
        println("Получено: $значение")
    }
}

3. Генератор с yieldAll

Если нужно выдать сразу несколько значений из другой коллекции или последовательности, используйте yieldAll.

val генераторИзСписка = sequence {
    // Выдаём все элементы из списка
    yieldAll(listOf("Январь", "Февраль", "Март"))
    // Потом выдаём ещё одно значение
    yield("Апрель")
    // И ещё одно
    yield("Май")
}

fun main() {
    for (месяц in генераторИзСписка) {
        println(месяц)
    }
}

4. Бесконечные последовательности

Генераторы могут быть бесконечными — они будут выдавать значения, пока вы их просите, и никогда не закончатся.

// Бесконечный генератор чётных чисел
val чётныеЧисла = sequence {
    var число = 0
    while (true) {  // Бесконечный цикл!
        yield(число)  // Выдаём текущее число
        число += 2    // Увеличиваем на 2
    }
}

fun main() {
    // Берём только первые 10 чётных чисел
    чётныеЧисла.take(10).forEach {
        println(it)
    }
    // Программа не зависнет, потому что take() ограничивает количество
}

5. Генератор с условием остановки

// Генератор чисел Фибоначчи до определённого предела
fun числаФибоначчи(предел: Int): Sequence<Int> = sequence {
    var a = 0
    var b = 1
    
    // Выдаём числа, пока они меньше предела
    while (a < предел) {
        yield(a)  // Выдаём текущее число
        val следующий = a + b
        a = b
        b = следующий
    }
}

fun main() {
    println("Числа Фибоначчи до 100:")
    for (число in числаФибоначчи(100)) {
        print("$число ")
    }
    // Вывод: 0 1 1 2 3 5 8 13 21 34 55 89
}

6. Использование итератора напрямую

Иногда нужно управлять итератором вручную, а не через цикл for.

fun main() {
    val список = listOf("A", "B", "C", "D")
    
    // Получаем итератор напрямую
    val итератор = список.iterator()
    
    // Используем итератор вручную
    while (итератор.hasNext()) {
        val элемент = итератор.next()
        println("Элемент: $элемент")
        
        // Можем прервать итерацию досрочно
        if (элемент == "C") {
            println("Дошли до C, останавливаемся")
            break
        }
    }
}

7. Функции-расширения для последовательностей

Kotlin предоставляет множество удобных функций для работы с последовательностями.

fun main() {
    val числа = sequence {
        var i = 1
        while (i <= 20) {
            yield(i)
            i++
        }
    }
    
    // Цепочка преобразований
    val результат = числа
        .filter { it % 2 == 0 }      // Оставляем только чётные
        .map { it * it }              // Возводим в квадрат
        .take(5)                      // Берём только первые 5
        .toList()                     // Превращаем в обычный список
    
    println("Результат: $результат")  // Вывод: [4, 16, 36, 64, 100]
}

Примеры кода — от простого к сложному

Пример 1: Простой перебор списка

fun main() {
    // Создаём список имён
    val имена = listOf("Анна", "Борис", "Виктор", "Галина")
    
    // Самый простой способ перебора — цикл for
    // Kotlin сам создаёт итератор и управляет им
    for (имя in имена) {
        println("Привет, $имя!")
    }
    
    // Этот код выведет:
    // Привет, Анна!
    // Привет, Борис!
    // Привет, Виктор!
    // Привет, Галина!
}

Пример 2: Перебор с индексом

fun main() {
    val цвета = listOf("Красный", "Зелёный", "Синий", "Жёлтый")
    
    // Используем функцию withIndex(), которая возвращает итератор пар (индекс, значение)
    for ((индекс, цвет) in цвета.withIndex()) {
        println("Цвет номер ${индекс + 1}: $цвет")
    }
    
    // Вывод:
    // Цвет номер 1: Красный
    // Цвет номер 2: Зелёный
    // Цвет номер 3: Синий
    // Цвет номер 4: Жёлтый
}

Пример 3: Генератор случайных чисел

import kotlin.random.Random

// Генератор, который выдаёт случайные числа от 1 до 100
fun генераторСлучайныхЧисел(количество: Int): Sequence<Int> = sequence {
    for (i in 1..количество) {
        // yield возвращает случайное число и приостанавливает выполнение
        yield(Random.nextInt(1, 101))
    }
}

fun main() {
    println("Пять случайных чисел:")
    val случайные = генераторСлучайныхЧисел(5)
    
    // Перебираем сгенерированные числа
    for (число in случайные) {
        println("Выпало: $число")
    }
}

Пример 4: Генератор для чтения файла построчно

import java.io.File

// Функция, которая читает файл построчно, но не загружает его целиком в память
fun читатьФайлПострочно(путьКФайлу: String): Sequence<String> = sequence {
    // Используем функцию useLines, которая автоматически закрывает файл
    File(путьКФайлу).useLines { строки ->
        // Перебираем все строки и выдаём их по одной
        for (строка in строки) {
            yield(строка)  // Выдаём строку и приостанавливаемся
        }
    }
}

fun main() {
    val путь = "example.txt"  // Предположим, такой файл существует
    
    // Читаем только первые 10 строк
    val первые10Строк = читатьФайлПострочно(путь).take(10)
    
    println("Первые 10 строк файла:")
    for (строка in первые10Строк) {
        println(строка)
    }
    // Файл не загружен в память целиком — только 10 строк!
}

Пример 5: Генератор с состоянием (счётчик посещений)

// Генератор, который считает, сколько раз его вызвали
fun счётчикПосещений(): Sequence<String> = sequence {
    var счётчик = 0
    
    while (true) {
        счётчик++
        yield("Посещение номер $счётчик")
    }
}

fun main() {
    val счётчик = счётчикПосещений()
    
    // Получаем итератор для ручного управления
    val итератор = счётчик.iterator()
    
    // Вызываем next() несколько раз
    println(итератор.next())  // Посещение номер 1
    println(итератор.next())  // Посещение номер 2
    println(итератор.next())  // Посещение номер 3
    
    // Состояние сохраняется между вызовами!
    println("Сделали паузу...")
    
    println(итератор.next())  // Посещение номер 4
}

Пример 6: Комбинация нескольких генераторов

// Генератор чётных чисел
fun чётные(): Sequence<Int> = sequence {
    var i = 0
    while (true) {
        yield(i)
        i += 2
    }
}

// Генератор нечётных чисел
fun нечётные(): Sequence<Int> = sequence {
    var i = 1
    while (true) {
        yield(i)
        i += 2
    }
}

// Функция, которая чередует элементы из двух последовательностей
fun чередовать(a: Sequence<Int>, b: Sequence<Int>): Sequence<Int> = sequence {
    val итераторA = a.iterator()
    val итераторB = b.iterator()
    
    while (итераторA.hasNext() && итераторB.hasNext()) {
        yield(итераторA.next())  // Берём из первой
        yield(итераторB.next())  // Берём из второй
    }
}

fun main() {
    val чередование = чередовать(чётные(), нечётные())
    
    println("Первые 10 чисел (чётные и нечётные по очереди):")
    чередование.take(10).forEach { print("$it ") }
    // Вывод: 0 1 2 3 4 5 6 7 8 9
}

Пример 7: Генератор для обхода дерева

// Простая структура данных — дерево
data class Узел(
    val значение: Int,
    val левый: Узел? = null,
    val правый: Узел? = null
)

// Функция для обхода дерева в глубину (in-order)
fun обходВГлубину(корень: Узел?): Sequence<Int> = sequence {
    if (корень != null) {
        // Сначала обходим левое поддерево
        yieldAll(обходВГлубину(корень.левый))
        // Потом выдаём значение текущего узла
        yield(корень.значение)
        // Потом обходим правое поддерево
        yieldAll(обходВГлубину(корень.правый))
    }
}

fun main() {
    // Строим дерево:
    //       5
    //      / 
    //     3   8
    //    /    
    //   1   4   9
    
    val дерево = Узел(
        значение = 5,
        левый = Узел(3, Узел(1), Узел(4)),
        правый = Узел(8, null, Узел(9))
    )
    
    println("Обход дерева в глубину:")
    for (значение in обходВГлубину(дерево)) {
        print("$значение ")
    }
    // Вывод: 1 3 4 5 8 9
}

Сравнение с Python: чем похоже и чем отличается

Характеристика Kotlin Python
Ключевое слово для генератора sequence { yield(...) } def gen(): yield ...
Тип генератора Sequence<T> generator
Ленивость По умолчанию ленивый (Sequence) По умолчанию ленивый (генераторы)
Бесконечные последовательности Поддерживаются через while(true) Поддерживаются через while True
Методы для работы .map(), .filter(), .take(), .toList() map(), filter(), itertools.islice(), list()
Синтаксис for for (x in seq) for x in gen:
Исключение StopIteration NoSuchElementException StopIteration
Создание итератора из коллекции коллекция.iterator() iter(коллекция)
Ручное управление итератором итератор.next(), итератор.hasNext() next(итератор), перехват StopIteration

Главные отличия:

  1. Типизация: В Kotlin генераторы строго типизированы (Sequence<Int>), в Python типы динамические.
  2. Синтаксис: В Kotlin используется блок sequence { } с yield внутри, в Python — функция с yield.
  3. Методы: В Kotlin последовательности имеют богатый набор встроенных методов (map, filter, take), в Python нужно использовать модуль itertools или встроенные функции.
  4. Проверка наличия: В Kotlin есть hasNext(), в Python нужно ловить исключение StopIteration.

Типичные ошибки и подводные камни

Ошибка 1: Попытка использовать итератор дважды

fun main() {
    val список = listOf(1, 2, 3)
    val итератор = список.iterator()
    
    // Первый проход — всё работает
    for (x in итератор) {
        println(x)
    }
    
    // Второй проход — ничего не выведется!
    for (x in итератор) {
        println(x)  // Этот код не выполнится
    }
    // Итератор уже "исчерпан" — он дошёл до конца и не может вернуться назад
}

Решение: Создавайте новый итератор для каждого прохода.

Ошибка 2: Забыли про ленивость последовательностей

fun main() {
    val последовательность = sequence {
        println("Генератор запущен!")
        yield(1)
        yield(2)
        yield(3)
    }
    
    // Код генератора ещё не выполнился!
    println("Генератор создан, но не запущен")
    
    // Только когда мы начинаем итерацию, генератор запускается
    for (x in последовательность) {
        println("Получено: $x")
    }
}

Решение: Помните, что код внутри sequence { } выполняется лениво — только при итерации.

Ошибка 3: Изменение коллекции во время итерации

fun main() {
    val список = mutableListOf(1, 2, 3, 4, 5)
    
    // ОПАСНО! Изменяем список во время итерации
    for (элемент in список) {
        if (элемент == 3) {
            список.remove(элемент)  // ConcurrentModificationException!
        }
    }
}

Решение: Используйте toList() для создания копии или итератор с явным удалением через iterator.remove().

Ошибка 4: Бесконечный цикл без ограничения

fun main() {
    val бесконечный = sequence {
        var i = 0
        while (true) {
            yield(i++)
        }
    }
    
    // ОПАСНО! Это будет работать вечно
    for (x in бесконечный) {
        println(x)  // Программа никогда не остановится
    }
}

Решение: Всегда используйте take(n) или другое условие остановки.

Ошибка 5: Путаница между Sequence и Iterable

fun main() {
    val список = listOf(1, 2, 3, 4, 5)
    
    // Это Iterable — все операции выполняются сразу
    val результат1 = список
        .filter { it % 2 == 0 }
        .map { it * 2 }
    // filter и map выполнились сразу, создав новый список
    
    // Это Sequence — операции ленивые
    val результат2 = список.asSequence()
        .filter { it % 2 == 0 }
        .map { it * 2 }
    // filter и map пока не выполнились!
    // Они выполнятся только когда мы начнём итерацию
}

Решение: Используйте asSequence() для больших данных или цепочек преобразований.

Лучшие практики и советы профессионалов

  1. Используйте последовательности для больших данных. Если вы работаете с коллекцией из миллионов элементов, преобразуйте её в Sequence с помощью asSequence(). Это сэкономит память и ускорит выполнение.
  2. Комбинируйте операции в цепочки. Вместо нескольких циклов используйте map, filter, flatMap и другие функции последовательностей. Это делает код более читаемым и эффективным.
  3. Используйте take() для ограничения. При работе с бесконечными последовательностями всегда указывайте лимит через take(n).
  4. Не злоупотребляйте генераторами. Если данные уже есть в памяти и их немного, обычный список будет проще и быстрее.
  5. Используйте yieldAll для вложенных последовательностей. Это делает код чище и избегает вложенных циклов.
  6. Помните про однократность итераторов. Если нужно пройти по данным дважды, сохраните результат в список с помощью toList().
  7. Используйте итераторы для сложных алгоритмов обхода. Обход деревьев, графов, файловых систем — идеальные задачи для генераторов.
Золотое правило: Если вы можете обработать данные за один проход — используйте последовательность. Если нужно несколько проходов или случайный доступ — используйте коллекцию.

Вопросы для самопроверки

  1. Вопрос: В чём разница между Iterable и Iterator в Kotlin?

    Ответ: Iterable — это интерфейс, который говорит, что объект можно перебирать (у него есть метод iterator()). Iterator — это объект, который непосредственно выполняет перебор (у него есть методы hasNext() и next()). Iterable создаёт Iterator.

  2. Вопрос: Что произойдёт, если вызвать next() у итератора, который уже дошёл до конца?

    Ответ: Будет выброшено исключение NoSuchElementException. Поэтому перед вызовом next() всегда нужно проверять hasNext().

  3. Вопрос: В чём преимущество Sequence перед List?

    Ответ: Sequence использует ленивые вычисления — элементы вычисляются только по мере необходимости. Это позволяет экономить память (не нужно хранить все элементы) и время (не нужно выполнять все преобразования, если нужны только первые несколько результатов).

  4. Вопрос: Можно ли использовать один и тот же итератор дважды?

    Ответ: Нет. Итератор — это одноразовый объект. После того как он дошёл до конца, его нельзя использовать снова. Нужно создать новый итератор.

  5. Вопрос: Как создать бесконечную последовательность в Kotlin?

    Ответ: Используйте sequence { while (true) { yield(...) } }. Но обязательно ограничивайте её с помощью take(n), иначе программа будет работать вечно.

  6. Вопрос: В чём разница между yield и yieldAll?

    Ответ: yield выдаёт одно значение, yieldAll выдаёт все элементы из другой коллекции или последовательности. yieldAll удобен, когда нужно «вложить» одну последовательность в другую.

  7. Вопрос: Как преобразовать Sequence в обычный список?

    Ответ: Используйте метод .toList(). Это «материализует» последовательность — все элементы будут вычислены и сохранены в списке.

  8. Вопрос: Что такое «ленивые вычисления»?