Перейти к основному содержимому

Циклы и управляющие конструкции в Kotlin

Разработчику Архитектору

Перед чтением: Операторы — общие понятия оператора, операнда, приоритетов и типов операций без привязки к языку.

Сначала: Циклы в коде — общая идея повторений, виды циклов и типичные ошибки без привязки к синтаксису языка.


Kotlin — for-in, диапазоны и while

В Kotlin нет C-подобного for (i = 0; i < n; i++). Цикл for перебирает диапазон (1..10), коллекцию или любой Iterable. Для "пока условие истинно" — while и do-while.

ЗадачаИдиома
N раз по счётчикуfor (i in 0 until n) или repeat(n) { … }
Элементы спискаfor (item in list)
Неизвестное число шаговwhile (condition)

На JVM под капотом часто тот же итератор, что в Java, но синтаксис короче и без ручного индекса, если он не нужен.

Интерактивное демо — пошаговый цикл на примере JavaScript (for, while). В Kotlin синтаксис другой, но порядок шагов тот же. Обобщённо: циклы в коде.

Play ITЗагрузка интерактивного демо…

Play ITЗагрузка интерактивного демо…


Цикл for — итерация как основа выразительности

Конструкция for в Kotlin — это итеративный цикл, ориентированный на перебор элементов итерируемых объектов (iterables). Под итерируемым объектом понимается любой объект, предоставляющий доступ к последовательности значений через стандартный протокол итерации. К таким объектам относятся:

  • диапазоны целых (IntRange, LongRange, CharRange);
  • строки (String);
  • коллекции (List, Set, Map и их подтипы);
  • массивы (Array<T>, IntArray, DoubleArray и прочие);
  • пользовательские типы, реализующие интерфейс Iterable<T> или предоставляющие метод iterator() с сигнатурой, совместимой с контрактом языка.

Синтаксис цикла for в Kotlin единообразен и минимален:

for (element in collection) {
// тело цикла
}

Разбор:

  • for (... in ...) перебирает каждый элемент итерируемого источника по очереди.
  • element создаётся как локальная переменная каждой итерации и не требует ручного управления индексом.
  • Ключевое слово in здесь задаёт схему обхода, а не сравнение.
  • Такая форма уменьшает риск ошибок со счётчиком и границами.

Здесь ключевое слово in обозначает принадлежность элемента к итерируемой последовательности, а не операцию сравнения. Имя element — это параметр цикла, локальная неизменяемая переменная, которая на каждой итерации принимает очередное значение из последовательности. Важно подчеркнуть: в Kotlin параметр цикла for всегда является val, то есть не может быть изменён внутри тела цикла. Это исключает распространённую ошибку — случайное изменение счётчика, что могло бы привести к зацикливанию или пропуску элементов.


Диапазоны как основа числовых итераций

Наиболее частый сценарий использования for — перебор целочисленных значений. Kotlin предоставляет встроенную поддержку диапазонов (ranges) через операторы .. (включающий диапазон), until (исключающий верхнюю границу) и функции downTo, step, которые позволяют управлять направлением и шагом итерации.

Пример простого включающего диапазона:

for (i in 1..5) {
println(i) // напечатает 1, 2, 3, 4, 5
}

Разбор:

  • 1..5 создаёт закрытый диапазон, включающий обе границы.
  • На каждой итерации i принимает следующее целое значение диапазона.
  • println(i) выводит текущий элемент, поэтому получаем последовательность 1, 2, 3, 4, 5.
  • Это идиоматичный способ для простого числового перебора без явного счётчика.

Здесь выражение 1..5 создаёт объект типа IntRange, эквивалентный математической записи [1, 5]. Диапазон является закрытым: обе границы включаются в последовательность.

Для перебора без включения верхней границы используется функция until:

for (i in 0 until 5) {
println(i) // напечатает 0, 1, 2, 3, 4
}

Разбор:

  • until строит полуоткрытый диапазон: правая граница (5) не входит в перебор.
  • Такой стиль идеально подходит для индексов массивов и списков, где верхняя граница обычно равна size.
  • Вывод заканчивается на 4, что соответствует диапазону валидных индексов для коллекции из пяти элементов.
  • Конструкция снижает вероятность ошибки "выход за границу".

Эквивалент 0 until 5 — это 0..4, но запись с until предпочтительна при работе с индексами, так как она явно выражает намерение "до, но не включая", что соответствует общепринятой практике индексации в нуле.

Обратный перебор реализуется с помощью функции downTo:

for (i in 5 downTo 1) {
println(i) // напечатает 5, 4, 3, 2, 1
}

Комбинирование downTo с step позволяет задавать произвольный шаг убывания:

for (i in 10 downTo 0 step 2) {
println(i) // напечатает 10, 8, 6, 4, 2, 0
}

Разбор:

  • downTo задаёт обратное направление обхода, от большего к меньшему.
  • step 2 пропускает каждый второй элемент, формируя арифметическую прогрессию.
  • Конечная точка 0 включается, так как диапазон закрытый и шаг корректно приводит к границе.
  • Такой шаблон часто применяют для обратного обхода индексов и пакетной обработки.

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

for (i in 1..10 step 3) {
println(i) // напечатает 1, 4, 7, 10
}

Важно отметить: step не изменяет исходный диапазон — она создаёт отдельный объект Progression, который представляет собой арифметическую прогрессию с заданным шагом. Это означает, что 1..10 step 3 и 1..10 — это разные объекты с разным поведением итерации.


Итерация по строкам и символам

Строка в Kotlin (String) реализует интерфейс CharSequence и может быть непосредственно использована в цикле for, поскольку компилятор автоматически предоставляет итератор по её символам:

for (c in "Kotlin") {
println(c) // напечатает 'K', 'o', 't', 'l', 'i', 'n'
}

Разбор:

  • Строка рассматривается как последовательность символов Char.
  • Цикл проходит по символам слева направо, сохраняя исходный порядок.
  • Переменная c получает по одному символу на итерацию и может участвовать в проверках/фильтрации.
  • Подход удобен для текстовых алгоритмов без ручной индексации.

Каждая итерация присваивает переменной c очередной символ типа Char. Такой подход позволяет легко реализовывать алгоритмы обработки текста — подсчёт частот, проверка палиндромов, фильтрация по шаблону и т.д. — без необходимости явного обращения к индексам или вызова методов вроде charAt(i).

При необходимости одновременного доступа и к индексу, и к значению, Kotlin предлагает вспомогательную функцию withIndex():

for ((index, char) in "Kotlin".withIndex()) {
println("Позиция $index: $char")
}

Разбор:

  • withIndex() оборачивает исходную последовательность в пары "индекс + значение".
  • Деструктуризация (index, char) сразу раскладывает пару на две отдельные переменные.
  • Внутри цикла доступен и номер позиции, и сам символ, что удобно для форматированного вывода и диагностики.
  • Этот паттерн убирает необходимость отдельного внешнего счётчика.

Это выражение создаёт последовательность объектов IndexedValue<Char>, каждый из которых содержит index и value. Деструктуризация в заголовке цикла позволяет удобно распаковать эти значения. Альтернативно — использовать indices:

val text = "Kotlin"
for (i in text.indices) {
println("[$i] = ${text[i]}")
}

Итерация по коллекциям и массивам

Любая коллекция, реализующая Iterable<T>, может быть перебрана через for. Это включает стандартные реализации List, Set, а также их неизменяемые и изменяемые варианты:

val numbers = listOf(10, 20, 30)
for (n in numbers) {
println(n * 2)
}

Для Map напрямую итерация идёт по паре ключ-значение, представленной объектом Map.Entry<K, V>:

val capitals = mapOf("RU" to "Moscow", "US" to "Washington")
for (entry in capitals) {
println("${entry.key}: ${entry.value}")
}

Разбор:

  • mapOf(...) создаёт неизменяемую карту из пар ключ -> значение.
  • При обычном переборе for (entry in capitals) каждый элемент имеет тип Map.Entry.
  • entry.key и entry.value дают доступ к ключу и соответствующему значению.
  • Шаблон полезен, когда нужно обрабатывать словарь целиком, сохраняя связку ключ-значение.

С использованием деструктуризации заголовок цикла можно упростить:

for ((code, city) in capitals) {
println("$code -> $city")
}

Массивы, несмотря на то что они не реализуют Iterable<T> (по соображениям производительности и совместимости с Java), также поддерживаются в for за счёт специальной обработки компилятором: генерируется эффективный цикл по индексам с прямым доступом к элементам:

val arr = intArrayOf(1, 2, 3)
for (x in arr) {
println(x)
}

Этот цикл компилируется в аналог for (int i = 0; i < arr.length; i++), без создания промежуточных объектов-итераторов, что делает его столь же эффективным, как и ручной перебор по индексу.


Семантика и ограничения цикла for

Цикл for в Kotlin является синтаксическим сахаром для вызова iterator() и последовательного вызова next() и hasNext(). Конкретно:

for (item in collection) { body }

эквивалентен (на уровне компиляции) следующему блоку:

val iterator = collection.iterator()
while (iterator.hasNext()) {
val item = iterator.next()
// тело цикла
}

Разбор:

  • Сначала создаётся итератор, который хранит состояние обхода коллекции.
  • hasNext() проверяет, есть ли следующий элемент; это условие продолжения цикла.
  • next() извлекает текущий элемент и сдвигает внутренний указатель итератора.
  • Этот фрагмент показывает, как for разворачивается компилятором в более низкоуровневую форму.

Эта трансляция означает, что цикл for не требует от коллекции реализации интерфейса Iterable — достаточно наличия метода iterator(), возвращающего объект, совместимый с Iterator<T>. Это позволяет легко расширять возможность итерации за счёт extension-функций:

operator fun ClosedRange<Int>.iterator(): Iterator<Int> = ...

(Хотя для диапазонов такой итератор уже встроен.)

Критически важно — цикл for в Kotlin не поддерживает операторы continue и break с метками, относящимися к внешним циклам, без явного указания метки. Однако локальные break и continue работают без ограничений и прерывают/продолжают текущий цикл. Для управления вложенными циклами Kotlin предлагает метки (labels):

outerLoop@ for (i in 1..3) {
for (j in 1..3) {
if (i * j > 4) break@outerLoop
println("$i * $j = ${i * j}")
}
}

Разбор:

  • outerLoop@ задаёт метку внешнего цикла, чтобы управлять выходом на нужный уровень вложенности.
  • Внутренний цикл перебирает j, а условие i * j > 4 определяет момент раннего завершения.
  • break@outerLoop немедленно останавливает оба цикла, а не только внутренний.
  • Метки делают контроль потока явным в сложных вложенных алгоритмах.

Использование меток — это осознанный выбор: он делает поток управления явным и избегает неочевидных побочных эффектов, характерных для безусловных break в многоуровневых циклах.


Циклы while и do-while — императивное управление условием выполнения

В отличие от for, который декларативно выражает намерение перебрать фиксированную последовательность, циклы while и do-while предназначены для ситуаций, когда количество итераций заранее неизвестно и определяется динамическими условиями. Эти конструкции сохраняют классическую императивную семантику: выполнение тела цикла продолжается до тех пор, пока условие остаётся истинным. Однако Kotlin вносит в них ряд уточнений, направленных на повышение безопасности и предсказуемости.


Цикл while — проверка условия до выполнения тела

Синтаксис:

while (condition) {
// тело цикла
}

Разбор:

  • while сначала проверяет условие и только потом выполняет тело.
  • Если condition изначально ложно, код внутри блока не выполнится ни разу.
  • После каждой итерации условие вычисляется заново, поэтому цикл управляется текущим состоянием данных.
  • Конструкция подходит для сценариев с заранее неизвестным числом повторений.

Цикл while выполняет следующую последовательность действий:

  1. Вычисляется выражение condition (логического типа Boolean);
  2. Если результат — true, выполняется тело цикла;
  3. После завершения тела управления возвращается к шагу 1;
  4. Если результат — false, выполнение цикла прекращается, и управление передаётся следующему за циклом оператору.

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

Типичные области применения:

  • Чтение данных из потока до достижения маркера конца (например, null, EOF);
  • Ожидание изменения состояния в многопоточной среде (с осторожностью и синхронизацией);
  • Реализация конечных автоматов или интерактивных циклов (например, REPL-интерфейсы);
  • Обработка очередей или стеков до опустошения.

Пример: эмуляция ввода команд до получения команды выхода:

var command: String? = null
while (command != "exit") {
print("Введите команду: ")
command = readLine()
if (command == "help") {
println("Доступные команды: help, exit")
}
}

Разбор:

  • command объявлен как String?, потому что readLine() может вернуть null.
  • Условие цикла держит программу в режиме командной строки до ввода exit.
  • На каждой итерации читается новая команда и выполняется условная обработка help.
  • Это типичный шаблон интерактивного REPL-цикла с динамическим завершением.

Здесь важно, что переменная command должна быть изменяемой (var), поскольку её значение меняется внутри цикла. Это принципиальное отличие от for: в while и do-while управление итерациями возлагается на программиста, включая явное изменение условий завершения. Kotlin не проверяет, изменяется ли условие внутри цикла — отсутствие прогресса может привести к бесконечному циклу. Тем не менее, компилятор может предупредить о неизменяемых переменных в условии (например, val x = 5; while (x > 0) { ... }), но не гарантирует анализ всех потенциально зацикливающихся конструкций.


Цикл do-while — гарантированное выполнение тела хотя бы один раз

Синтаксис:

do {
// тело цикла
} while (condition)

Разбор:

  • do-while сначала выполняет тело и лишь затем проверяет условие продолжения.
  • За счёт этого блок гарантированно запускается минимум один раз.
  • Такой порядок особенно удобен для сценариев "сначала запросить данные, потом проверить".
  • После проверки condition == true цикл повторяется, иначе выполнение идёт дальше.

Порядок выполнения:

  1. Выполняется тело цикла;
  2. Вычисляется condition;
  3. Если результат — true, возврат к шагу 1;
  4. Если false — выход из цикла.

Таким образом, тело цикла do-while выполняется один или более раз. Эта конструкция полезна, когда первая итерация должна быть выполнена независимо от начального состояния условия.

Классический пример — валидация пользовательского ввода:

var input: String
do {
print("Введите число от 1 до 10: ")
input = readLine() ?: ""
} while (input.toIntOrNull() !in 1..10)

Разбор:

  • Переменная input перезаписывается на каждой итерации, пока введено некорректное значение.
  • readLine() ?: "" защищает код от null и подставляет пустую строку при отсутствии ввода.
  • toIntOrNull() выполняет безопасный парсинг числа и не бросает исключение при ошибке.
  • Условие !in 1..10 оставляет цикл активным, пока число не попадёт в допустимый диапазон.

Здесь мы сначала запрашиваем ввод, затем проверяем его корректность. Использование while в этом случае потребовало бы дублирования кода ввода до цикла (для первого запроса) и внутри цикла (для повторных), что нарушает принцип DRY.

Другой сценарий — инициализация ресурса с последующей проверкой его состояния:

val connection = establishConnection()
do {
process(connection.receive())
} while (connection.isConnected)

Обратите внимание — условие в do-while вычисляется после тела, поэтому важно, чтобы состояние, от которого зависит условие, могло быть корректно обновлено в теле цикла. В противном случае возможна ситуация, когда тело выполняет некорректные действия на последней итерации (например, обрабатывает null, если соединение разорвано в процессе).


Сравнение while и do-while — выбор конструкции по семантике

Выбор между while и do-while должен определяться логикой задачи:

  • Используйте while, если выполнение тела цикла не требуется, когда условие изначально ложно. Это соответствует модели "проверить — действовать".
  • Используйте do-while, если тело должно быть выполнено минимум один раз, независимо от начального состояния. Это модель "действовать — проверить".

Например, поиск первого элемента, удовлетворяющего условию, в связном списке обычно реализуется через while, так как список может быть пустым:

var node = head
while (node != null && !node.matches(criteria)) {
node = node.next
}

Тогда как повторный опрос API при временном сбое — через do-while, поскольку мы обязаны выполнить первый запрос независимо от ожиданий:

var attempt = 0
do {
attempt++
result = fetchWithRetry(url)
} while (result == null && attempt < 3)

Разбор:

  • Счётчик attempt ограничивает число повторов и предотвращает бесконечные ретраи.
  • fetchWithRetry(url) выполняется минимум один раз, потому что это do-while.
  • Условие сочетает две проверки: продолжаем только если ответа нет и лимит попыток не исчерпан.
  • Такой шаблон удобен для временно нестабильных внешних сервисов.

Управление потоком — break, continue, метки

В while и do-while, как и в for, поддерживаются операторы:

  • break — немедленный выход из ближайшего цикла;
  • continue — переход к следующей итерации (повторная проверка условия);
  • break@label / continue@label — управление с указанием метки.

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

search@ for (row in matrix) {
for (col in row.indices) {
if (row[col] == target) {
println("Найдено: строка ${matrix.indexOf(row)}, столбец $col")
break@search
}
}
}

Разбор:

  • Метка search@ помечает внешний цикл для управляемого досрочного выхода.
  • При нахождении target выполняется break@search, что останавливает сразу оба уровня перебора.
  • Без метки break прервал бы только внутренний цикл, и поиск продолжился бы лишний раз.
  • Паттерн полезен для матриц и других вложенных структур, где важен ранний выход.

Аналог с while:

var i = 0
outer@ while (i < matrix.size) {
var j = 0
while (j < matrix[i].size) {
if (matrix[i][j] == target) {
println("Найдено: [$i][$j]")
break@outer
}
j++
}
i++
}

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


Функции высшего порядка для итераций — forEach, map, filter и их место в экосистеме

Помимо императивных циклов, Kotlin предоставляет богатый набор функций высшего порядка, определённых в стандартной библиотеке для Iterable, Sequence, Array и других коллекций. Наиболее близкой к циклу for по семантике является функция forEach.


forEach — функциональный аналог for

Сигнатура:

inline fun <T> Iterable<T>.forEach(action: (T) -> Unit)

Пример использования:

val list = listOf("a", "b", "c")
list.forEach { item ->
println(item)
}

Разбор:

  • forEach принимает лямбду и вызывает её для каждого элемента коллекции.
  • item по очереди принимает значения "a", "b", "c".
  • Семантически это полный проход по коллекции без явного индекса.
  • Внутри лямбды удобно выполнять побочные действия, например логирование или печать.

С точки зрения поведения, forEach и for (x in collection) эквивалентны: оба выполняют действие для каждого элемента в порядке итерации. Однако между ними есть принципиальные различия:

АспектforforEach
Тип конструкцииуправляющий оператор языкафункция высшего порядка
Поддержка break/continueданет (внутри лямбды работают как return@forEach, что эквивалентно continue)
Inline-оптимизацияне применимода (функция объявлена inline, накладные расходы минимальны)
Наличие this внутри теланетв forEach у лямбды this — текущий элемент (при вызове forEach { println(this) })
Расширяемостьчерез итераторычерез extension-функции и обобщённое программирование

Критически важно: внутри лямбды forEach, нельзя использовать break или continue в их обычном смысле. Попытка написать:

list.forEach {
if (it == "b") break // ОШИБКА КОМПИЛЯЦИИ
println(it)
}

Разбор:

  • break работает только внутри языковых циклов (for, while, do-while), а не в лямбде forEach.
  • Поэтому компилятор выдаёт ошибку: из контекста функции высшего порядка нельзя выйти как из цикла.
  • Пример демонстрирует границу между управляющей конструкцией языка и библиотечной функцией.
  • Для похожего поведения используют return@forEach, takeWhile или обычный for.

приведёт к ошибке, поскольку break вне цикла не имеет смысла. Для досрочного завершения итерации требуется использовать non-local return или return@forEach:

list.forEach {
if (it == "b") return@forEach // пропустить текущий элемент (аналог continue)
println(it)
}

Для полного выхода из forEach возможны два пути:

  1. Обернуть в run и использовать non-local return:
run {
list.forEach {
if (it == "b") return@run // выход из run, а значит — из forEach
println(it)
}
}
  1. Использовать takeWhile, firstOrNull, или перейти к for, если требуется частое досрочное завершение.

Поэтому рекомендуется:

  • Использовать forEach для полного перебора коллекции без досрочного выхода — например, при логировании, побочных эффектах, инициализации;
  • Использовать for, если требуется гибкое управление потоком (break, continue, метки).

Другие функции-итераторы — map, filter, fold, reduce

Хотя они не являются циклами в прямом смысле, эти функции реализуют повторяющиеся вычисления и часто заменяют императивные циклы в функциональном стиле:

  • map — трансформация каждого элемента в новый (возвращаемый тип — List<R>);
  • filter — отбор элементов по предикату;
  • fold / reduce — агрегация значений в одно (например, сумма, конкатенация);
  • find / firstOrNull — поиск первого подходящего элемента (аналог for + break);
  • any / all / none — проверки условий на всей коллекции.

Пример сравнения:

Императивный стиль:

var sum = 0
for (n in numbers) {
if (n % 2 == 0) {
sum += n * n
}
}

Функциональный стиль:

val sum = numbers
.filter { it % 2 == 0 }
.map { it * it }
.sum()

Второй вариант более декларативен: он явно выражает последовательность операций фильтрация → трансформация → свёртка. Однако он создаёт промежуточные коллекции (если не использовать Sequence). Для больших данных предпочтителен asSequence():

val sum = numbers.asSequence()
.filter { it % 2 == 0 }
.map { it * it }
.sum()

Разбор:

  • asSequence() переводит коллекцию в ленивую последовательность и убирает промежуточные списки.
  • filter и map теперь применяются поэлементно во время терминальной операции.
  • sum() запускает выполнение всей цепочки и агрегирует итог.
  • Подход уменьшает аллокации и полезен на больших объёмах данных.

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


Диапазоны и прогрессии — математическая основа числовых итераций

В Kotlin диапазоны — это полноценные объекты, реализующие строгие математические и программные контракты. Основа иерархии — интерфейс ClosedRange<T>, параметризованный типом, который должен реализовывать Comparable<T>:

interface ClosedRange<T : Comparable<T>> {
val start: T
val endInclusive: T
operator fun contains(value: T): Boolean = value >= start && value <= endInclusive
}

Для целочисленных типов (Int, Long, Char) стандартная библиотека предоставляет конкретные реализации — IntRange, LongRange, CharRange. Все они:

  • неизменяемы (val start, val endInclusive);
  • реализуют Iterable<T> через арифметическую прогрессию с шагом по умолчанию 1;
  • оптимизированы на уровне компилятора (например, цикл по IntRange компилируется в эффективный for (int i = start; i <= end; i++), без создания итератора в куче, если не используется step).

Открытые и закрытые границы

Выражение a..b создаёт закрытый диапазон [a, b]. Для построения полуоткрытого диапазона [a, b) (часто необходимого при работе с индексами) используется функция-расширение until:

infix fun Int.until(to: Int): IntRange = this..(to - 1)

Это инфиксная функция, поэтому 0 until 5 читается как "от 0 до 5, не включая". Важно: until не является частью языка — это обычная функция, которую можно переопределить (хотя делать это настоятельно не рекомендуется).


Прогрессии и шаг итерации

Диапазон сам по себе не содержит информации о шаге. При добавлении шага через step создаётся объект типа IntProgression — подкласс Iterable<Int>, представляющий арифметическую прогрессию:

val p: IntProgression = 1..10 step 3 // 1, 4, 7, 10

IntProgression содержит три поля — first, last, step. При итерации вычисляется следующее значение как current + step, пока оно не выйдет за пределы направления прогрессии (учитывая знак шага). Для отрицательного шага (downTo + step) используется та же структура.

Важные свойства:

  • step должен быть ненулевым. Попытка указать step 0 вызовет исключение IllegalArgumentException.
  • При обратном обходе (downTo) шаг по умолчанию равен -1, но может быть уточнён: 10 downTo 0 step 2.
  • Прогрессии не проверяют корректность границ при создании: 10..1 step 1 — это валидный объект, но он будет пустым при итерации (поскольку 10 > 1, а шаг положительный). Такое поведение аналогично Range в Python и является намеренным — оно позволяет писать универсальные алгоритмы без предварительной проверки порядка границ.

Производительность и компиляция

Цикл for (i in 1..n) компилируется в Java-код, эквивалентный:

for (int i = 1; i <= n; i++) { ... }

— без создания объекта IntRange и без вызовов виртуальных методов iterator(), hasNext(), next(). Это достигается за счёт специализированной обработки компилятором Kotlin для встроенных диапазонов.

Однако при использовании step, downTo, или при присваивании диапазона переменной:

val r = 1..10
for (i in r) { ... }

— создаётся реальный объект IntRange, и итерация происходит через вызовы итератора. В большинстве случаев это незаметно, но в hot-пути (например, в циклах вложенных 3–4 уровней, выполняемых миллионы раз) может иметь значение. Рекомендация: избегать сохранения простых диапазонов в переменные, если они используются только в одном for.


Расширенные вспомогательные конструкции для итераций

Помимо базовых циклов, Kotlin предоставляет ряд функций и свойств, значительно упрощающих типичные шаблоны итерации.


indices и lastIndex

Любой Collection<T> и CharSequence (включая String) имеет свойство indices, возвращающее IntRange от 0 до size - 1:

val s = "Kotlin"
for (i in s.indices) {
println("$i: ${s[i]}")
}

Эквивалентно 0 until s.length, но семантически точнее: "все допустимые индексы для контейнера s". Аналогично, lastIndex возвращает size - 1, что удобно при прямом доступе к последнему элементу: list[list.lastIndex].


withIndex() — индекс + значение без ручного счётчика

Функция withIndex() преобразует Iterable<T> в Iterable<IndexedValue<T>>, где каждый элемент — пара (index, value). Это позволяет избежать введения внешней переменной-счётчика:

for ((i, c) in "abc".withIndex()) {
println("[$i] = $c")
}

При компиляции создаётся лёгкий объект IndexedValue, но благодаря инлайну и оптимизациям JVM накладные расходы минимальны. Альтернатива — forEachIndexed, которая принимает лямбду с двумя параметрами:

list.forEachIndexed { index, value ->
if (index % 2 == 0) println(value)
}

zip — параллельная итерация по двум последовательностям

Функция zip объединяет две коллекции в последовательность пар:

val keys = listOf("a", "b", "c")
val values = listOf(1, 2, 3)
val map = keys.zip(values).toMap() // {a=1, b=2, c=3}

Можно передать трансформирующую функцию:

keys.zip(values) { k, v -> "$k -> $v" }.forEach(::println)

Если длины коллекций различаются, zip завершает итерацию по достижении конца более короткой последовательности.


chunked и windowed — итерация блоками и окнами

Эти функции предназначены для пошаговой обработки данных большими порциями:

  • chunked(size) разбивает коллекцию на непересекающиеся куски заданного размера:
listOf(1, 2, 3, 4, 5).chunked(2) // [[1, 2], [3, 4], [5]]
  • windowed(size, step = 1, partialWindows = false) создаёт скользящее окно:
"abcdef".windowed(3) // ["abc", "bcd", "cde", "def"]
"abcdef".windowed(3, step = 2) // ["abc", "cde"]
"abc".windowed(5, partialWindows = true) // ["abc"]

Обе функции возвращают List<List<T>>, но существуют также asSequence().chunked(...) и asSequence().windowed(...) для экономии памяти.

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


Пользовательские итераторы — адаптация сторонних типов под for

Kotlin позволяет использовать for с любым объектом, предоставляющим метод iterator(), возвращающий совместимый итератор. Это не требует реализации интерфейса Iterable — достаточно extension-функции.

Пример: итерация по битам целого числа:

Код ITЗагрузка примера кода…

Такой подход позволяет интегрировать сторонние библиотеки (например, ByteBuffer, Stream<T>) в нативный синтаксис Kotlin без обёрток.

Аналогично, можно сделать итерируемой бесконечную последовательность (например, генератор простых чисел), но с осторожностью: цикл for по ней без break или take(n) приведёт к зависанию.


Рекомендации по выбору циклической конструкции

Выбор между for, while, do-while, forEach и функциями высшего порядка должен основываться на следующих критериях:

КритерийРекомендуемая конструкция
Известное количество итераций (индексы, диапазоны)for (i in 0 until n) или for (i in indices)
Перебор всех элементов коллекции без досрочного выходаfor (x in list) или list.forEach { ... } (предпочтительно for для единообразия)
Необходимость доступа к индексу и значениюfor ((i, x) in list.withIndex()) или list.forEachIndexed { i, x -> ... }
Условие завершения зависит от внешнего состояния (ввод, сеть, поток)while
Тело должно выполниться хотя бы один раз (валидация ввода)do-while
Трансформация/фильтрация/агрегация без побочных эффектовmap/filter/fold/sum и т.п.
Досрочный выход (break) или пропуск (continue) требуется частоfor (только он поддерживает break/continue напрямую)
Работа с большими данными, важна экономия памятиasSequence() + цепочка операций + терминальная функция (toList(), sum())

Общие принципы:

  • Предпочитайте for — он наиболее идиоматичен, безопасен (неизменяемый параметр), и поддерживает полный набор управляющих конструкций.
  • Используйте while/do-while только при необходимости — когда логика завершения не может быть выражена через конечную последовательность.
  • Функции высшего порядка — для чистых преобразований. Избегайте forEach с побочными эффектами в глубоко вложенных цепочках — это затрудняет отладку.
  • Не смешивайте стили без причины. Например, не стоит писать list.filter { ... }.forEach { ... }, если можно использовать один for с условием — это создаёт лишнюю коллекцию.

Антипаттерны и типичные ошибки

1. Изменение коллекции во время итерации for

val list = mutableListOf(1, 2, 3)
for (x in list) {
if (x == 2) list.remove(x) // ConcurrentModificationException (в JVM)
}

Это приводит к неопределённому поведению. Решения:

  • Использовать list.removeIf { it == 2 };
  • Собрать индексы/элементы для удаления и выполнить после итерации;
  • Использовать MutableIterator.remove():
val iterator = list.iterator()
while (iterator.hasNext()) {
if (iterator.next() == 2) iterator.remove()
}

2. Бесконечные циклы из-за неизменного условия

val x = 5
while (x > 0) { ... } // x не меняется — цикл бесконечен

Компилятор не всегда может это отловить. Рекомендация — использовать var только при необходимости, и явно обозначать, какие переменные управляют циклом.


3. Неэффективные вложенные циклы без early exit

for (a in largeList1) {
for (b in largeList2) {
if (a.id == b.id) {
process(a, b)
// Забыт break — продолжается перебор!
}
}
}

Решение — break@label или вынос логики в отдельную функцию с return.


4. Злоупотребление forEach с return@forEach вместо continue

list.forEach {
if (it < 0) return@forEach // плохо читается
println(it)
}

Предпочтительнее:

for (x in list) {
if (x < 0) continue
println(x)
}

— более прямолинейно и соответствует ожиданиям разработчика.


5. Использование step 0 или некорректных границ без проверки

for (i in 10..1 step 1) { ... } // тело не выполнится ни разу

Хотя это не ошибка, такое поведение может быть неочевидным. При сомнениях — проверяйте isEmpty у диапазона/прогрессии.


Практические сниппеты

fun retryFetch(maxAttempts: Int): String {
var attempt = 0
var result: String? = null

while (attempt < maxAttempts && result == null) {
attempt++
result = if (attempt == 3) "OK" else null
}

return result ?: "FAILED"
}

Разбор:

  • while контролирует повторение сразу по двум условиям: лимит попыток и наличие результата.
  • attempt++ двигает состояние цикла вперёд и защищает от бесконечного повтора.
  • В примере успех имитируется на третьей попытке, но шаблон подходит и для реального сетевого запроса.
  • Финальный Elvis гарантирует детерминированный итог даже при полном исчерпании ретраев.
fun oddSquares(input: List<Int>): List<Int> {
val out = mutableListOf<Int>()
for ((index, value) in input.withIndex()) {
if (index % 2 == 0) continue
out += value * value
}
return out
}

Разбор:

  • withIndex() даёт доступ и к индексу, и к значению в одном проходе.
  • continue пропускает элементы на чётных позициях без вложенного else.
  • out += value * value показывает типичный аккумулятор внутри императивного цикла.
  • Сниппет иллюстрирует сценарий, где for с явным контролем потока читается проще цепочки HOF.