Iterable и Iterator: что стоит за for ( x in collection)

Kotlin SELF
20 уровень , 0 лекция
Открыта

1. Как работает for под капотом

Если вы только начинаете программировать, for (x in collection) выглядит как волшебная фраза из книги заклинаний: произнёс — и элементы сами пошли в руки. И это правда удобно. Но как только вы захотите сделать что-то чуть сложнее, например, “пройти по коллекции особым способом”, “остановиться на середине”, “пропустить некоторые элементы” или “объяснить странную ошибку времени выполнения”, магии станет слишком много — и она начнёт кусаться.

Хорошая новость: никакой настоящей магии там нет. В Kotlin for опирается на простую идею: объект можно обойти, если он умеет выдавать итератор. Итератор — это такой “пульт управления обходом”: он знает, есть ли следующий элемент, и может его отдать.

Iterable и Iterator: базовые роли

Когда вы видите в коде for (x in something), Kotlin должен понять: “Окей, а как перебрать это something?”. Механика держится на двух сущностях:

  • Iterable<T> — это контракт “меня можно перебрать”.
  • Iterator<T> — это объект, который выполняет обход прямо сейчас.

Удобно держать в голове простую картинку: коллекция/источник даёт итератор, а итератор уже “шагает” по элементам.

flowchart LR
    A["Iterable⟨T⟩
(данные/источник)"] -->|"iterator()"| B["Iterator⟨T⟩
(процесс обхода)"] B -->|"next()"| C["T (очередной элемент)"]

Iterable<T>: “обещание, что меня можно перебрать”

В стандартной библиотеке Kotlin существует интерфейс Iterable<T>. Это не “контейнер данных” и не “список”, а именно контракт: объект обещает, что по нему можно идти шаг за шагом, получая элементы.

Формально контракт очень простой: Iterable<T> должен уметь вернуть Iterator<T> через функцию iterator().

Чтобы упростить модель:

  • Iterable<T> — это “объект, который можно обходить”.
  • Iterator<T> — это “устройство, которое выполняет обход прямо сейчас”.

Iterator<T>: два шага — hasNext() и next()

Итератор хранит “текущую позицию” обхода и умеет двигаться вперёд. У него есть два базовых метода:

  • hasNext(): отвечает на вопрос “есть ли ещё элементы?”
  • next(): выдаёт следующий элемент и сдвигает позицию вперёд

Звучит сухо, но на практике это прямо как очередь в кофейне: пока есть люди (hasNext()), можно брать следующего (next()), обслуживать — и очередь двигается.

Мини-пример: берём итератор у списка и печатаем элементы.

fun main() {
    val names = listOf("Ann", "Bob", "Cate")
    val it = names.iterator()

    while (it.hasNext()) {
        val name = it.next()
        println(name) // Ann, потом Bob, потом Cate
    }
}

Да, это выглядит длиннее, чем for. И да, именно поэтому for существует.

Итератор одноразовый: почему нельзя “перемотать назад”

Когда вы смотрите кино на стриминге, вы можете отмотать назад. Когда вы пользуетесь итератором, вы как будто смотрите кино в прямом эфире: кадры уходят, и итератор не обязан уметь возвращаться. Это важная концепция, которая часто удивляет новичков.

Покажем это на коде:

fun main() {
    val nums = listOf(1, 2, 3)
    val it = nums.iterator()

    while (it.hasNext()) {
        print(it.next())
    }
    println() // 123

    // Вторая попытка тем же итератором:
    println(it.hasNext()) // false
}

Обратите внимание: nums не “исчез”, исчезла возможность у этого конкретного итератора продолжать обход, потому что он уже дошёл до конца.

Коллекция vs итератор: кто хранит данные, а кто хранит “прогресс”

Здесь легко запутаться, поэтому зафиксируем модель. Коллекция — это данные. Итератор — это “курсор обхода”.

Небольшая таблица (её реально стоит перечитать два раза):

Сущность Что это Что хранит Что умеет
Iterable<T> / коллекция источник элементов сами элементы выдать итератор
Iterator<T> процесс обхода текущую позицию hasNext() и next()

Мини-проверка на понимание: если вам нужно повторить обход, вы создаёте новый итератор, но используете ту же коллекцию.

3. Как работает for (x in collection)

Ключевая мысль: for в Kotlin — это не отдельный “особый режим”. Это удобный синтаксис, который делает тот же самый обход, но без рутины.

Сравним два варианта (оба правильные). Вариант “человеческий”:

fun main() {
    val names = listOf("Ann", "Bob", "Cate")
    for (name in names) {
        println(name)
        // Ann
        // Bob
        // Cate
    }
}

И вариант “механический”, но честный:

fun main() {
    val names = listOf("Ann", "Bob", "Cate")
    val it = names.iterator()

    while (it.hasNext()) {
        val name = it.next()
        println(name)
        // Ann
        // Bob
        // Cate
    }
}

Если вы поймёте эту эквивалентность, вы перестанете воспринимать for как магию. Это как автопилот: вы не обязаны каждый раз крутить руль вручную, но полезно знать, что руль вообще существует.

“Эквивалент for” как шпаргалка для чтения кода

Иногда вы читаете чужой код и видите: “Ого, тут ручной итератор, while, hasNext()… что происходит?”. Чтобы не теряться, держите в голове простую шпаргалку:

for (x in collection) {
    body(x)
}

мысленно превращается в:

val it = collection.iterator()
while (it.hasNext()) {
    val x = it.next()
    body(x)
}

Это не обязательно буквально тот же байт-код во всех ситуациях, но как модель мышления — работает отлично.

4. Практика: SimplePlanner и печать задач

Давайте развивать наше консольное приложение (условно назовём его SimplePlanner): оно хранит список строк-задач в MutableList<String>. Сегодня мы не пишем весь командный интерфейс, но добавим одну полезную функцию: печатать все задачи.

Печать задач “вручную”, через итератор

Почему через итератор? Не потому что так нужно всегда, а чтобы вы увидели механику и перестали её бояться.

fun printTasksManual(tasks: List<String>) {
    val it = tasks.iterator()
    var number = 1

    while (it.hasNext()) {
        val task = it.next()
        println("${number}. $task") // 1. ..., 2. ...
        number++
    }
}

Проверим в main:

fun main() {
    val tasks = mutableListOf("Buy milk", "Read Kotlin docs", "Sleep")
    printTasksManual(tasks)
    // 1. Buy milk
    // 2. Read Kotlin docs
    // 3. Sleep
}

Да, это чуть длиннее, чем for ((i, task) in tasks.withIndex()). Но сейчас наша цель — увидеть “двигатель”, а не только красивый кузов.

Тот же смысл, но короче: for как стиль по умолчанию

После того как вы увидели “мотор”, можно спокойно вернуться к нормальному синтаксису. В большинстве случаев for — лучший вариант: он короче, читабельнее и даёт меньше шансов ошибиться (например, забыть hasNext() и вызвать next() в пустоту).

Код печати задач обычно будет таким:

fun printTasks(tasks: List<String>) {
    var number = 1
    for (task in tasks) {
        println("${number}. $task") // 1. ..., 2. ...
        number++
    }
}

Смысл тот же, но читается легче.

5. Когда нужен явный итератор и что важно помнить

На первых шагах программирования можно жить годами без ручного итератора и чувствовать себя прекрасно. Но иногда ручной контроль обхода полезен: вы хотите управлять шагами, остановиться по условию, или сделать особую операцию в момент обхода, не превращая for в кашу.

Ещё один частый сценарий — аккуратное изменение mutable-коллекции во время обхода (но это отдельная тема; здесь мы только закладываем основу, без подробностей).

Как Kotlin понимает, что объект можно использовать в for

Kotlin не угадывает — он ищет “правильную форму”.

Объект можно использовать в for (x in obj), если Kotlin может получить у него итератор. На практике это сводится к правилу: у объекта должна быть функция iterator(), а у возвращаемого итератора — методы hasNext() и next().

Для нас сегодня достаточно запомнить:

  • List, Set, Map (через entries, keys, values) и многие другие стандартные коллекции уже “обходимые” — их можно использовать в for сразу.
  • Если вы создадите свой класс, сам по себе он не станет “обходимым”, пока не будет поддерживать iterator().

6. Типичные ошибки при работе с Iterable и Iterator

Ошибка №1: путать коллекцию и итератор (“я взял итератор, где мои элементы?”).
Коллекция — это хранилище элементов, итератор — это временный объект, который показывает “где мы сейчас” в обходе. Итератор не обязан уметь всё то, что умеет коллекция: вы не спросите у него size, не “перемотаете” его назад и не ожидаете, что он будет жить вечно. Обычно помогает проговаривать вслух: “коллекция — данные, итератор — процесс”.

Ошибка №2: вызывать next() без проверки hasNext().
В ручном обходе while есть железное правило: сначала спросили “есть ли следующий?”, потом взяли следующий. Если вызвать next() на итераторе, который уже закончился, вы получите исключение времени выполнения (то есть программа упадёт). for защищает вас от этой ошибки автоматически, поэтому ручной стиль требует больше дисциплины.

Ошибка №3: ожидать, что итератор можно использовать повторно (“сейчас ещё раз пройдусь тем же it”).
Итератор одноразовый. Если вы дошли до конца, hasNext() будет false, и “повторить” обход не получится. Нужно заново вызвать collection.iterator() и получить новый итератор.

Ошибка №4: считать for “магией языка”, которую нельзя объяснить.
for — это синтаксический комфорт. Он просто прячет от вас рутину итератора. Как только вы понимаете, что for основан на iterator()/hasNext()/next(), код становится более предсказуемым: вы легче читаете чужие решения, быстрее находите причины ошибок и спокойнее переходите к более сложным сценариям обхода.

Ошибка №5: писать ручной итератор там, где нужен обычный for.
Ручной итератор — не “более профессионально”. Это просто инструмент. Если вам не нужна особая логика управления обходом, то for будет и короче, и безопаснее, и понятнее. В программировании вообще много мест, где “короче и понятнее” — это не лень, а качество.

1
Задача
Kotlin SELF, 20 уровень, 0 лекция
Недоступна
Маршрут путешествия
Маршрут путешествия
1
Задача
Kotlin SELF, 20 уровень, 0 лекция
Недоступна
Список дел
Список дел
1
Задача
Kotlin SELF, 20 уровень, 0 лекция
Недоступна
Плюс к настроению
Плюс к настроению
1
Задача
Kotlin SELF, 20 уровень, 0 лекция
Недоступна
Поиск по префиксу
Поиск по префиксу
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ