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() |
Мініперевірка на розуміння: якщо вам потрібно повторити обхід, ви створюєте новий ітератор, але використовуєте ту саму колекцію.
2. Як працює 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)
}
Це не обовʼязково буквально той самий байткод у всіх ситуаціях, але як модель мислення — працює чудово.
3. Практика: 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("Купити молоко", "Прочитати документацію Kotlin", "Поспати")
printTasksManual(tasks)
// 1. Купити молоко
// 2. Прочитати документацію Kotlin
// 3. Поспати
}
Так, це трохи довше, ніж 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++
}
}
Зміст той самий, але читається легше.
4. Коли потрібен явний ітератор і що важливо памʼятати
На перших кроках програмування можна роками жити без ручного ітератора й почуватися чудово. Але іноді ручний контроль обходу корисний: ви хочете керувати кроками, зупинитися за умовою або виконати особливу операцію під час обходу, не перетворюючи for на кашу.
Ще один поширений сценарій — акуратна зміна mutable-колекції під час обходу (але це окрема тема; тут ми лише закладаємо основу, без подробиць).
Як Kotlin розуміє, що обʼєкт можна використовувати в for
Kotlin нічого не вгадує — він шукає «правильну форму».
Обʼєкт можна використовувати в for (x in obj), якщо Kotlin може отримати від нього ітератор. На практиці це зводиться до правила: в обʼєкта має бути функція iterator(), а в повернутого ітератора — методи hasNext() і next().
Для нас сьогодні достатньо запамʼятати:
- List, Set, Map (через entries, keys, values) і багато інших стандартних колекцій уже «придатні до обходу» — їх можна використовувати в for одразу.
- Якщо ви створите свій клас, сам по собі він не стане «придатним до обходу», доки не підтримуватиме iterator().
5. Типові помилки під час роботи з 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 буде і коротшим, і безпечнішим, і зрозумілішим. У програмуванні загалом є чимало місць, де «коротше й зрозуміліше» — це не лінощі, а якість.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ