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

5.09. Циклы

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

Циклы

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

В Kotlin отсутствует классический цикл for (initialization; condition; increment), привычный по языкам C-семейства. Вместо него используется конструкция for, основанная на понятии итерации по диапазонам и итерируемым последовательностям. Такой подход устраняет распространённые ошибки (например, выход за границы массива, ошибки в условии завершения), и позволяет легко расширять поведение цикла за счёт пользовательских итераторов и диапазонов. В то же время язык сохраняет императивные циклы while и do-while для случаев, когда требуется гибкое управление условием выполнения, особенно в ситуациях, когда момент завершения не может быть определён заранее или зависит от внешних факторов (например, ввода пользователя, сетевого ответа или состояния потока).

Таким образом, в Kotlin циклы можно рассматривать как декларативные выражения намерений: «повторить действие для каждого элемента X», «продолжать, пока сохраняется состояние Y», «выполнить хотя бы один раз, а затем — при условии Z». Эта семантическая разгрузка значительно упрощает проектирование алгоритмов и повышает сопровождаемость кода.

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

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

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

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

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

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

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

for (i in 0 until 5) {
println(i) // напечатает 0, 1, 2, 3, 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
}

Аналогично, для возрастающих последовательностей необязательно использовать шаг, равный единице. Функция 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'
}

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

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

for ((index, char) in "Kotlin".withIndex()) {
println("Позиция $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}")
}

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

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()
// тело цикла
}

Эта трансляция означает, что цикл 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}")
}
}

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


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

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

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

Синтаксис:

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

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

Синтаксис:

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

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

  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)

Здесь мы сначала запрашиваем ввод, затем проверяем его корректность. Использование 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)

Управление потоком: 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
}
}
}

Аналог с 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 и 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 вне цикла не имеет смысла. Для досрочного завершения итерации требуется использовать 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)
    }
    }
  2. Использовать 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()

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


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

В 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-функции.

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

class BitIterator(private var value: Int) : Iterator<Boolean> {
private var bitIndex = 0
override fun hasNext() = bitIndex < 32
override fun next(): Boolean {
if (!hasNext()) throw NoSuchElementException()
return (value shr bitIndex++ and 1) == 1
}
}

operator fun Int.iterator() = BitIterator(this)

// Использование:
for (bit in 5) { // 5 = 101₂
print(if (bit) '1' else '0')
}
// Выведет: 10100000000000000000000000000000

Такой подход позволяет интегрировать сторонние библиотеки (например, 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 у диапазона/прогрессии.