Перейти к основному содержимому
Мобилка03ГЛАВА 03Условия и циклы на Kotlinwhen

Условия и циклы на Kotlin

Прочитано0%
Квизы0/5
Тренажёрыне пройден0/1
Уровень 1 · Новичок

Зачем это нужно​

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

Экран приложения показывает список — это цикл. Кнопка становится серой, пока форма не заполнена, — это условие. Приложение решает, показать список или заглушку «пока пусто», — снова условие. Подсчёт суммы заказа по корзине — цикл с накоплением. Больше в программе, по сути, ничего и нет: остальное — оформление этих двух конструкций.

Эта глава разбирает их до последнего символа именно в Kotlin. И сразу главное: в Kotlin условие — это не команда, а выражение. У if есть результат, который можно присвоить переменной. У when — тоже. Тот, кто раньше писал на Java, C# или JavaScript, на этом месте обычно спотыкается: там if ничего не возвращает, а тут возвращает, и половина привычных конструкций становится лишней.

Все выводы и все сообщения об ошибках в этой главе сняты с живого запуска компилятора Kotlin 2.4.10 — того же, что стоит за кнопкой «Запустить» в примерах ниже. Ничего не придумано: если в тексте написано «компилятор скажет вот так», значит, он действительно так сказал.

if: развилка, у которой есть результат​

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

fun main() { val level = 7 if (level > 5) { println("уровень выше среднего") } }

Что выведет: уровень выше среднего. Условие level > 5 истинно, поэтому тело выполняется.

Разбор по частям — здесь важен каждый символ:

Нажми на часть выражения

Обрати внимание на две вещи, которых нет в Java и JavaScript: точка с запятой в конце строк не нужна и отступ в Kotlin — четыре пробела, а не два. Оба правила прописаны в официальном стиле языка, и Android Studio форматирует код именно так.

Полная форма добавляет вторую ветку:

fun main() { val level = 3 if (level > 5) { println("уровень выше среднего") } else { println("новичок") } }

Что выведет: новичок.

ГЛАВНОЕ ОТЛИЧИЕ ОТ JAVA: У IF ЕСТЬ РЕЗУЛЬТАТJava: сначала пустая переменная, потом две записи в неёString name;if (lvl > 5) name = "старший";else name = "младший";переменная какое-то время живёт пустойKotlin: одно присваивание, val остаётся valval name = if (lvl > 5) "старший" else "младший"ветка отдаёт значение — его сразу забирает vallvl > 5значениеval nameнет else — нет и результата: компилятор потребует вторую ветку
В Java if — команда: сначала объявляют пустую переменную, потом две ветки в неё пишут. В Kotlin if сам отдаёт значение, поэтому переменная объявляется один раз и остаётся val.

А теперь — то, ради чего эта глава начинается именно с if. В Kotlin if — выражение. У него есть результат, и этот результат можно присвоить:

fun main() { val level = 7 val name = if (level > 5) "старший" else "младший" println(name) }

Что выведет: старший.

Результатом ветки становится последнее выражение в её теле. Если тело в фигурных скобках — берётся последняя строка:

fun main() { val level = 7 val name = if (level > 5) { println("считаю разряд...") "старший" } else { "младший" } println(name) }

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

Отсюда правило, которое компилятор проверяет жёстко: если if используется как выражение, ветка else обязательна. Иначе непонятно, что за значение получится, когда условие ложно. Убери else — и получишь ошибку:

'if' must have both main and 'else' branches when used as an expression.
Важно

В Kotlin нет тернарного оператора условие ? а : б. Он не нужен: if (условие) а else б — это и есть тернарный оператор, только словами. Тот, кто ищет в Kotlin знак вопроса с двоеточием, ищет напрасно — его убрали намеренно, чтобы в языке была одна конструкция вместо двух.

Из чего собирают условие: сравнения и логика​

Условие — это выражение типа Boolean, то есть дающее true или false. Собирают его из сравнений и логических операторов.

Сравнения:

ОператорЧитаетсяПример
==равноstatus == "confirmed"
!=не равноstatus != "unknown"
> <больше, меньшеlevel > 5
>= <=больше или равно, меньше или равноscore >= passing
===это один и тот же объектa === b
!==это разные объектыa !== b

Про == в Kotlin надо сказать отдельно, потому что здесь язык расходится и с Java, и с JavaScript.

  • В Java == для строк сравнивает ссылки, и сравнивать строки через == — классическая ошибка новичка; там нужен .equals().
  • В JavaScript == приводит типы, и "3" == 3 истинно.
  • В Kotlin == вызывает equals(), то есть сравнивает содержимое. А === — сравнивает, один ли это объект в памяти.
fun main() { val part = "БТ" val a = "БТК" val b = part + "К" println(a == b) println(a === b) }

Что выведет: true, потом false. Содержимое одинаковое, а объекты в памяти разные: строка b собрана во время работы программы. Практический вывод простой: строки сравнивай через ==, а === тебе почти никогда не понадобится.

Интересный факт

Если написать val b = "БТ" + "К" прямо в одну строку константами, то a === b внезапно даст true. Компилятор складывает константы ещё на этапе компиляции и подставляет одну и ту же строку из общего пула. Это не магия и не ошибка — это оптимизация, унаследованная от JVM. И это ровно та причина, по которой сравнивать строки через === нельзя: результат зависит от того, что компилятор смог посчитать заранее.

Логические операторы:

ОператорЧитаетсяРезультат
&&Иистинно, когда истинны обе стороны
два знака | подрядИЛИистинно, когда истинна хотя бы одна сторона
!НЕпереворачивает логическое значение
if (isOnline && score > 0) { /* обе стороны истинны */ }
if (!isOnline) { /* isOnline ложно */ }

&& и ИЛИ работают с коротким вычислением (short-circuit): если результат уже понятен по левой стороне, правая не вычисляется вовсе. Это не оптимизация «где-то в глубине» — на этом строят защиту:

if (user != null && user.name.isNotEmpty()) { /* безопасно */ }

Правая часть не выполнится, когда user равен null, поэтому обращения к полю у отсутствующего объекта не произойдёт.

Важно

Есть ещё and и or — они выглядят как англоязычные синонимы, но вычисляют обе стороны всегда, без короткого замыкания. В условиях их не пишут: user != null and user.name.isNotEmpty() упадёт на пустом пользователе. and/or нужны для побитовых операций над числами, а не для if.

Ещё одно важное отличие от JavaScript: в условие нельзя подставить что попало. В JavaScript if (name) — рабочий код: пустая строка ложна, непустая истинна. В Kotlin так нельзя, условие обязано быть Boolean:

val name = "Соня"
if (name) { } // не скомпилируется
if (name.isNotEmpty()) { } // так правильно

Никаких truthy и falsy в Kotlin нет — и это одна из причин, по которым его код читается однозначнее.

Отвечено вопросов: 0 из 4
Почему в Kotlin нет тернарного оператора «условие ? а : б»?
Что произойдёт, если использовать if как выражение без ветки else?
Чем == отличается от === в Kotlin?
Почему if (name) не компилируется, если name — строка?
Отвечено верно: 0 из 4

Лестница условий и ранний выход​

Когда вариантов больше двух, ветки выстраивают в лестницу через else if:

fun main() { val hour = 21 if (hour < 6) { println("ночь") } else if (hour < 12) { println("утро") } else if (hour < 18) { println("день") } else { println("вечер") } }

Что выведет: вечер.

Проверки идут сверху вниз, и выполняется ровно одна ветка — первая подошедшая. Поэтому порядок важен: если поставить hour < 18 первой, то hour < 6 никогда не сработает — шесть меньше восемнадцати, и лестница закончится на первой ступеньке.

Такую лестницу в Kotlin почти всегда переписывают на when — про него следующий раздел. Но сначала — приём, который пригодится в любом языке.

Ранний выход. Вложенные условия быстро превращаются в лестницу вправо:

fun describe(user: User?): String {
if (user != null) {
if (user.isActive) {
if (user.score > 0) {
return "активный участник"
}
}
}
return "нет данных"
}

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

fun describe(user: User?): String {
if (user == null) return "нет данных"
if (!user.isActive) return "нет данных"
if (user.score <= 0) return "нет данных"
return "активный участник"
}

Плохие случаи отсекаются сверху, основной путь остаётся без вложенности вообще. Скорость выполнения одинакова — ценность целиком в читаемости. На разборе кода в команде за такую переделку скажут спасибо.

when с предметом: главная конструкция выбора​

when — родственник switch из Java и JavaScript, но устроен принципиально иначе и потому используется гораздо чаще.

fun main() { val level = 3 when (level) { 1 -> println("новичок") 2 -> println("уверенный") 3 -> println("опытный") else -> println("неизвестный уровень") } }

Что выведет: опытный.

Нажми на часть выражения

Три отличия от switch, каждое из которых важнее предыдущего:

Первое: никакого проваливания. В Java и JavaScript switch ищет точку входа и дальше выполняет всё подряд, пока не встретит break. Забыл break — выполнились две ветки. В Kotlin выполняется ровно одна ветка, и слова break внутри when вообще нет.

WHEN ВЫБИРАЕТ ОДНУ ВЕТКУ И ОСТАНАВЛИВАЕТСЯwhen (level)level = 5in 1..3 ->не совпало, идём дальшеin 4..6 ->совпало: только эта веткаin 7..9 ->уже не проверяетсяelse ->запасной выходрезультат when:"средний"никакого break нети проваливания вниз тоже
when проверяет ветки сверху вниз, выполняет первую подошедшую и на этом заканчивает. Ни break, ни проваливания в следующую ветку в Kotlin не существует.

Второе: несколько значений в одной ветке — через запятую.

fun main() { val n = 3 val text = when (n) { 1, 2 -> "мало" 3, 4 -> "средне" else -> "много" } println(text) }

Что выведет: средне.

Третье: в ветке может стоять не только значение. Диапазон через in, проверка типа через is, да и просто вызов функции:

fun main() { val level = 7 val grade = when (level) { in 1..3 -> "новичок" in 4..6 -> "средний" else -> "опытный" } println(grade) }

Что выведет: опытный.

when без предмета: замена лестницы else if​

Если убрать скобки после when, каждая ветка становится самостоятельным условием. Это и есть тот самый способ переписать лестницу:

fun main() { val hour = 21 val part = when { hour < 6 -> "ночь" hour < 12 -> "утро" hour < 18 -> "день" else -> "вечер" } println(part) }

Что выведет: вечер.

Сравни с лестницей if / else if выше: тот же смысл, но вместо девяти строк — шесть, и вся логика выстроена в один столбик, где условие и результат стоят рядом. Именно в таком виде выбор из нескольких вариантов пишут в Kotlin чаще всего.

Ветки по-прежнему проверяются сверху вниз, поэтому порядок так же важен, как в лестнице: первая подошедшая выигрывает.

Правило выбора между двумя формами простое:

СитуацияЧто писать
Сравниваем одно значение с набором вариантовwhen (значение) { ... }
Каждая ветка — своё независимое условиеwhen { ... }
Ровно два исходаif ... else

Когда when обязан быть исчерпывающим​

when, как и if, — выражение. И к нему применяется то же требование: если результат куда-то присваивается, все случаи должны быть покрыты.

val n = 3
val s = when (n) {
1 -> "один"
2 -> "два"
}

Компилятор откажется это собирать:

'when' expression must be exhaustive. Add an 'else' branch.

Он прав: n равно трём, ни одна ветка не подошла, а значение переменной s взять неоткуда. Добавляем else — и всё в порядке.

А вот when, который используется как команда и ничего не возвращает, else не требует:

fun main() { val n = 5 when (n) { 1 -> println("один") 2 -> println("два") } println("дошли до конца") }

Что выведет: только дошли до конца. Ни одна ветка не подошла, when просто ничего не сделал — и это законно, потому что результат никому не нужен.

Есть один случай, когда else не нужен даже у выражения: когда компилятор видит все возможные варианты сам. Так бывает с enum и с запечатанными классами — про них подробно в главе про ООП на Kotlin. Пока достаточно знать: если варианты перечислены в самом типе, when по нему можно написать без else, и при добавлении нового варианта компилятор придёт и укажет на этот when. Это одна из самых полезных вещей во всём языке.

Отвечено вопросов: 0 из 4
Что произойдёт в Kotlin, если в ветке when забыть break?
Чем when (значение) отличается от when без скобок?
Когда when обязан иметь ветку else?
Как в одной ветке when обработать сразу значения 3 и 4?
Отвечено верно: 0 из 4

is и умное приведение типа​

Иногда значение приходит «неизвестно чем» — например, из внешних данных. Тип такого значения — Any, общий предок всех типов. Проверить настоящий тип позволяет оператор is:

fun main() { val x: Any = "TON" if (x is String) { println("длина ${x.length}") } }

Что выведет: длина 3.

Обрати внимание: внутри if мы обращаемся к x.length — свойству строки. Хотя объявлен x как Any, у которого никакой длины нет. Это умное приведение (smart cast): раз проверка прошла, компилятор дальше в этой ветке считает x строкой и разрешает всё, что разрешено строке.

В Java на этом месте пришлось бы писать приведение руками: if (x instanceof String) { String s = (String) x; ... }. Kotlin делает это сам.

Тот же приём работает и в when:

fun main() { val raw: Any = 42 when (raw) { is Int -> println("Int плюс один = ${raw + 1}") is String -> println("строка длиной ${raw.length}") else -> println("что-то ещё") } }

Что выведет: Int плюс один = 43.

Умное приведение работает не всегда, и отказ компилятора выглядит непривычно. Вот класс с изменяемым свойством:

class Wallet { var nick: String? = "ton" }

val w = Wallet()
if (w.nick != null) println(w.nick.length)

Ошибка:

Smart cast to 'String' is impossible, because 'nick' is a mutable property
that could be mutated concurrently.

Перевод: «nick — изменяемое свойство, его могли поменять из другого потока между проверкой и использованием, поэтому гарантировать, что там не null, нельзя». Лечится копированием в локальную переменную:

val nick = w.nick
if (nick != null) println(nick.length)

Теперь nick — локальный val, изменить его никто не может, и умное приведение срабатывает.

null в условиях: ?., ?: и !!​

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

val nick: String = null
Null cannot be a value of a non-null type 'String'.

Чтобы разрешить отсутствие, к типу дописывают знак вопроса: String?. Но тогда обращаться к такому значению напрямую нельзя:

val nick: String? = "ton"
println(nick.length)
Only safe (?.) or non-null asserted (!!.) calls are allowed
on a nullable receiver of type 'String?'.

Компилятор буквально перечисляет разрешённые варианты. Разберём все три.

NULL ПРИШЁЛ — ТРИ РАЗНЫХ ОТВЕТАnull?.nullвызов пропускается, дальше идёт null?:0подставляется запасное значение!!NPEпадение прямо здесь и сейчас!! — просьба уронить программу; в учебном коде ей места нет
Три способа обойтись с возможным null: ?. пропускает вызов и отдаёт null дальше, ?: подставляет запасное значение, !! требует уронить программу немедленно.

?. — безопасный вызов. Если слева null, вызов не выполняется, и всё выражение даёт null:

fun main() { val nick: String? = null println(nick?.length) }

Что выведет: null.

?: — оператор Элвиса. Читается «если слева null, возьми то, что справа». Название шуточное: повёрнутый на бок, знак похож на причёску и глаза Элвиса Пресли.

fun main() { val nick: String? = null println(nick?.length ?: 0) }

Что выведет: 0.

Элвис особенно хорош в паре с ранним выходом — справа от него можно поставить return:

fun greet(user: User?): String {
val name = user?.name ?: return "гость"
return "привет, $name"
}

!! — утверждение «здесь точно не null». Если там всё-таки null, программа падает:

val nick: String? = null
println(nick!!.length)
java.lang.NullPointerException

Исключение приходит без сообщения — просто NullPointerException и стек вызовов. Поэтому правило простое: !! в учебном коде не пишут вообще. Каждый такой знак — это фраза «я уверен, что здесь не null», а компилятор ровно наоборот считает, что уверенности нет. Если в проекте на ревью встречается !!, автору задают вопрос.

Есть и удобные проверки для строк:

fun main() { val nick: String? = null println(nick.isNullOrBlank()) println(nick.isNullOrEmpty()) val other: String? = " " println(other.isNullOrBlank()) }

Что выведет: true, true, true. Последняя строка — важная: isNullOrBlank считает пустой и строку из одних пробелов, а isNullOrEmpty — нет.

Отвечено вопросов: 0 из 4
Что вернёт выражение nick?.length, если nick равен null?
Что делает оператор ?: (Элвис)?
Почему !! не пишут в обычном коде?
Почему умное приведение может не сработать для w.nick != null?
Отвечено верно: 0 из 4

Диапазоны: откуда цикл берёт числа​

Прежде чем разбирать for, нужно понять, по чему он ходит. В Kotlin нет привычного по Java заголовка for (int i = 0; i < n; i++) — его вообще нет в языке. Вместо него — диапазон (range): готовый набор значений.

fun main() { println(1..5) println((1..5).toList()) println((1..<5).toList()) println((5 downTo 1).toList()) println((1..9 step 4).toList()) println((1..0).toList()) }

Что выведет:

1..5
[1, 2, 3, 4, 5]
[1, 2, 3, 4]
[5, 4, 3, 2, 1]
[1, 5, 9]
[]

Первая строка показательна: 1..5 сам по себе — это значение, объект типа IntRange. Его можно положить в переменную, передать в функцию, спросить у него .first, .last, .isEmpty().

ЧЕТЫРЕ ЗАПИСИ — ЧЕТЫРЕ НАБОРА ЧИСЕЛ1..51 2 3 4 5обе границы включены1..<51 2 3 4правая граница НЕ включена5 downTo 15 4 3 2 1обратный ход1..9 step 41 5 9через шагuntil — старое имя для ..<; в новом коде пишут ..<
Четыре записи — четыре набора чисел. Разница между .. и ..< всего в один символ, а последнее число либо есть, либо его нет.
ЗаписьЧто даётКогда нужно
1..51 2 3 4 5обе границы включены — «с первого по пятый»
1..<51 2 3 4правая граница не включена — «до пятого»
1 until 51 2 3 4то же самое, старая запись
5 downTo 15 4 3 2 1обратный ход
1..9 step 41 5 9через шаг

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

Последняя строка вывода — [] — тоже важная. 1..0 — пустой диапазон, потому что двигаться от единицы вверх до нуля некуда. Цикл по нему просто не выполнится ни разу, без всякой ошибки. А вот 1 downTo 0 даст [1, 0].

Диапазоны нужны не только циклам. Через in они превращаются в проверку принадлежности:

fun main() { println(5 in 1..10) println(15 !in 1..10) println('c' in 'a'..'z') println("O" in "TON") }

Что выведет: четыре раза true. Последняя строка — не диапазон, а проверка вхождения подстроки: in работает и со строками, и со списками.

for: цикл по готовому набору​

В Kotlin for устроен одинаково всегда: берём набор и проходим по нему.

fun main() { for (i in 1..3) { println("проход ${i}") } }

Что выведет: три строки — проход 1, проход 2, проход 3.

Нажми на часть выражения

Переменная цикла в Kotlin — val. Присвоить ей что-то внутри тела нельзя, и это не ограничение, а страховка: счётчик, который меняют изнутри цикла, — источник самых мутных багов.

По списку ходят точно так же:

fun main() { val coins = listOf("BTC", "ETH", "TON") for (coin in coins) { println(coin) } }

Что выведет: три строки с названиями.

Нужен и элемент, и его номер — есть withIndex():

fun main() { val coins = listOf("BTC", "ETH", "TON") for ((i, coin) in coins.withIndex()) { println("$i: $coin") } }

Что выведет:

0: BTC
1: ETH
2: TON

Круглые скобки вокруг (i, coin) — это деструктуризация: пара «номер и значение» разбирается на две переменные прямо в заголовке цикла.

Нужны только номера — есть свойство indices:

fun main() { val coins = listOf("BTC", "ETH", "TON") println(coins.indices) for (i in coins.indices) { println("$i -> ${coins[i]}") } }

Что выведет: сначала 0..2, потом три строки. indices — это готовый диапазон индексов, его не надо считать руками.

Совет

Именно indices спасает от классической ошибки выхода за границы. Если написать for (i in 0..coins.size) — с двумя точками вместо ..< — то последний проход обратится к элементу с номером 3, которого в списке из трёх элементов нет. Программа напечатает все три названия, а потом упадёт: ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3. Нумерация с нуля, поэтому у трёх элементов номера 0, 1 и 2.

По словарю ходят парами:

fun main() { val fees = mapOf("BTC" to 1200, "TON" to 5) for ((coin, fee) in fees) { println("$coin -> $fee") } }

Что выведет: BTC -> 1200 и TON -> 5.

И по строке — посимвольно:

fun main() { for (c in "TON") { print("$c-") } println() }

Что выведет: T-O-N-. Здесь print вместо println: он печатает без перевода строки, поэтому всё оказывается в одну.

while и do..while: цикл без известного числа проходов​

for хорош, когда набор известен заранее. Когда неизвестно, сколько будет проходов, берут while:

fun main() { var n = 3 while (n > 0) { print("$n ") n-- } println() }

Что выведет: 3 2 1.

Здесь переменная обязана быть var — её меняют на каждом проходе. Попытка написать val даёт короткую и понятную ошибку:

'val' cannot be reassigned.

do..while отличается ровно одним: проверка стоит после тела, поэтому тело выполняется минимум один раз, даже если условие ложно с самого начала:

fun main() { var m = 0 do { println("выполнилось хотя бы раз") } while (m > 0) }

Что выведет: одну строку. Условие m > 0 ложно, но проверили его уже после выполнения тела.

Важно

У любого цикла, который чего-то ждёт — ответа сервера, подтверждения, готовности файла, — обязательно должно быть ограничение по числу попыток. Иначе при неудаче приложение зависает навсегда: не падает, не пишет ошибку, просто перестаёт отвечать. Такой баг отлаживается тяжелее любого исключения, потому что в логах пусто. Пиши while (attempts < 20), а не while (true).

break, continue и метки​

Два слова управляют ходом цикла изнутри:

  • break — выйти из цикла целиком;
  • continue — пропустить остаток тела и перейти к следующему проходу.
fun main() { for (i in 0..4) { if (i == 2) continue if (i == 4) break print("$i ") } println() }

Что выведет: 0 1 3. Двойку пропустил continue, на четвёрке break прервал цикл до печати.

Оба слова работают только внутри цикла. Написать break в обычном if нельзя:

'break' and 'continue' are only allowed inside loops.

Обычный break выходит только из ближайшего цикла. Чтобы выйти сразу из двух вложенных, цикл помечают меткой — имя со знаком @:

fun main() { outer@ for (i in 1..3) { for (j in 1..3) { if (j == 2) continue@outer if (i == 3) break@outer print("$i$j ") } } println() }

Что выведет: 11 21.

Разберём, почему именно так. При i = 1: j = 1 — печатаем 11, j = 2 — continue@outer бросает внутренний цикл и переходит к следующему i. При i = 2 — то же самое, печатаем 21. При i = 3: j = 1, условие i == 3 истинно, break@outer выходит из внешнего цикла целиком.

Синтаксис меток в Kotlin отличается от Java: метка ставится перед циклом как outer@, а используется как break@outer. В Java было бы outer: и break outer.

Метки нужны и ещё в одном месте — для выхода из forEach:

fun main() { listOf(1, 2, 3, 4).forEach { if (it == 3) return@forEach print("$it ") } println() }

Что выведет: 1 2 4. Обрати внимание — тройка пропущена, а четвёрка напечатана. return@forEach ведёт себя как continue, а не как break: он завершает обработку одного элемента, а перебор продолжается. Настоящего досрочного выхода из forEach не существует — нужен выход, бери обычный for с break.

repeat и обход коллекций без цикла​

Когда нужно просто повторить действие N раз и номер прохода не важен — есть repeat:

fun main() { repeat(3) { println("шаг $it") } }

Что выведет: шаг 0, шаг 1, шаг 2. Внутри доступна переменная it — номер прохода, считая с нуля.

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

fun main() { val txs = listOf(10, 250, 40, 900) println(txs.filter { it > 50 }) println(txs.map { it * 2 }) println(txs.sumOf { it }) println(txs.count { it > 50 }) println(txs.any { it > 500 }) println(txs.all { it > 5 }) println(txs.firstOrNull { it > 500 }) println(txs.maxOrNull()) println(txs.sorted()) }

Что выведет:

[250, 900]
[20, 500, 80, 1800]
1200
2
true
true
900
900
[10, 40, 250, 900]
ФункцияЧто делаетЧто возвращает
filterоставляет подходящиесписок той же или меньшей длины
mapпреобразует каждыйсписок той же длины
sumOfскладываетодно число
countсчитает подходящиеодно число
anyесть ли хоть один подходящийtrue / false
allвсе ли подходятtrue / false
firstOrNullпервый подходящийэлемент или null
maxOrNullнаибольшийэлемент или null
sortedсортируетновый список

Все они не меняют исходный список, а возвращают новый. Подробно про лямбды — фигурные скобки с it внутри — в следующей главе; здесь важно увидеть, что цикл с накоплением суммы в четыре строки заменяется одним sumOf.

Хвостик OrNull в именах — не украшение. maxOrNull() на пустом списке вернёт null, а не упадёт; это тот же способ языка честно сказать «результата нет», что и nullable-типы.

Отвечено вопросов: 0 из 5
Почему в Kotlin нет заголовка for (int i = 0; i < n; i++)?
В чём разница между 1..5 и 1..<5?
Что напечатает for (i in 0..4) { if (i == 2) continue; if (i == 4) break; print(i) }?
Почему return@forEach не работает как break?
Что обязательно должно быть у цикла, ожидающего ответа сервера?
Отвечено верно: 0 из 5

Ошибки этой главы и что они значат​

Все сообщения ниже сняты с настоящего запуска компилятора Kotlin 2.4.10. Их стоит научиться узнавать в лицо — на экзамене и на чемпионате время уходит именно на расшифровку незнакомых формулировок.

СообщениеЧто случилосьКак чинить
'if' must have both main and 'else' branches when used as an expression.if присваивается переменной, но ветки else нетдописать else
'when' expression must be exhaustive. Add an 'else' branch.то же самое для whenдописать else
'val' cannot be reassigned.пытаемся изменить valзаменить на var, если менять действительно надо
Only expressions are allowed in this context.написали = вместо == в условиипоставить двойное равно
Operator '==' cannot be applied to 'Int' and 'String'.сравниваем число со строкойпривести к одному типу
'break' and 'continue' are only allowed inside loops.break вне циклаубрать или обернуть в цикл
Null cannot be a value of a non-null type 'String'.кладём null в тип без ?объявить тип как String?
Only safe (?.) or non-null asserted (!!.) calls are allowed…обращаемся к nullable напрямуюиспользовать ?. и ?:
Smart cast to 'String' is impossible, because 'nick' is a mutable property…проверяем на null изменяемое свойствоскопировать в локальный val
Unresolved reference 'inside'.переменная объявлена внутри блока, а используется снаружиобъявить до блока
ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3вышли за границы списка при обходе..< вместо .., а лучше indices

Про = вместо == стоит сказать отдельно, потому что формулировка неочевидная. Код if (n = 4) даёт:

Only expressions are allowed in this context.

Компилятор говорит: «в условии допустимы только выражения», а присваивание в Kotlin выражением не является. В C и Java такой код собрался бы и работал не так, как задумано, — Kotlin эту ловушку закрыл целиком.

Тренажёр: конструкции этой главы​

Цель: скорость ≥ 110 зн/мин, точность ≥ 90%

val name = if (level > 5) "старший" else "младший"
клавиша ``клавиша 11клавиша 22клавиша 33клавиша 44клавиша 55клавиша 66клавиша 77клавиша 88клавиша 99клавиша 00клавиша --клавиша ==клавиша ⌫⌫клавиша TabTabклавиша QQклавиша WWклавиша EEклавиша RRклавиша TTклавиша YYклавиша UUклавиша IIклавиша OOклавиша PPклавиша [[клавиша ]]клавиша \\клавиша Caps LockCaps Lockклавиша AAклавиша SSклавиша DDклавиша FFклавиша GGклавиша HHклавиша JJклавиша KKклавиша LLклавиша ;;клавиша ''клавиша EnterEnterклавиша ShiftShiftклавиша ZZклавиша XXклавиша CCклавиша VVклавиша BBклавиша NNклавиша MMклавиша ,,клавиша ..клавиша //клавиша ShiftShiftклавиша CtrlCtrlклавиша WinWinклавиша AltAltклавиша ПробелПробелклавиша AltAltклавиша WinWinклавиша MenuMenuклавиша CtrlCtrl
следующая: V — указательный левой руки

Символы этой главы​

ЗнакНазваниеЧто делает
( )круглые скобкидержат условие у if, while, предмет у when и заголовок for
{ }фигурные скобкитело блока или лямбда
->стрелкаотделяет вариант от тела ветки в when
..диапазонобе границы включены: 1..5
..<диапазон без правой границы1..<5 даёт 1 2 3 4
inпринадлежностьи в заголовке for, и в проверке 5 in 1..10
!inне принадлежитотрицание проверки
isпроверка типавключает умное приведение
? после типаnullable«здесь может не быть значения»
?.безопасный вызовпри null слева вызов пропускается
?:оператор Элвисаподставляет запасное значение вместо null
!!утверждение не-nullроняет программу, если там null; в коде не пишут
&&логическое Иобе стороны истинны; правая не вычисляется при ложной левой
два знака | подрядлогическое ИЛИхотя бы одна сторона истинна
!логическое НЕпереворачивает истинность
==равно по содержимомувызывает equals()
===тот же объектсравнивает ссылки; нужен крайне редко
@ в outer@метка циклаадрес для break@outer и continue@outer
itимя по умолчаниюединственный параметр лямбды в repeat, filter, map

Если рядом учат TypeScript​

На платформе есть параллельная глава про условия и циклы в блокчейн-треке. Тем, кто сидит за соседним компьютером, будет полезно сравнить — а тебе полезно знать, где у соседа по-другому:

ЧтоKotlinTypeScript
if как выражениеда, отдаёт значениенет, только команда
Тернарный операторотсутствует, не нуженусловие ? а : б
Выбор из вариантовwhen, без проваливанияswitch, с break в каждой ветке
Условие принимаеттолько Booleanчто угодно, по правилам truthy/falsy
Сравнение строк=====
Заголовок forfor (i in 1..5)for (let i = 0; i < 5; i++)
Отсутствие значенияnull, запрещён без ? в типеnull и undefined, оба разрешены
Точка с запятойне ставитсяставится
Отступ4 пробела2 пробела

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

Что должен уметь​

  • Писать if и else и объяснять, почему в Kotlin у них есть результат.
  • Присваивать результат if переменной и помнить, что тогда else обязателен.
  • Объяснять, почему в Kotlin нет тернарного оператора.
  • Собирать условие из сравнений и логических операторов, объяснять короткое вычисление.
  • Понимать разницу == и === и не сравнивать строки через ===.
  • Знать, что в условие нельзя подставить строку или число: только Boolean.
  • Переписывать вложенные условия на ранний выход.
  • Писать when в обеих формах — с предметом и без — и выбирать между ними по задаче.
  • Объяснять, почему в when не нужен break.
  • Перечислять несколько значений в одной ветке через запятую.
  • Понимать, когда when обязан быть исчерпывающим.
  • Проверять тип через is и пользоваться умным приведением.
  • Объяснять, почему умное приведение не работает для var-свойства.
  • Обрабатывать null через ?. и ?: и не писать !!.
  • Строить диапазоны через .., ..<, downTo, step и объяснять разницу.
  • Ходить циклом по диапазону, списку, словарю и строке.
  • Пользоваться withIndex() и indices.
  • Отличать while от do..while и всегда ставить ограничитель у цикла ожидания.
  • Пользоваться break, continue и метками outer@.
  • Знать, что return@forEach ведёт себя как continue, а не как break.
  • Заменять простые циклы на filter, map, sumOf, count, any, all.
  • Узнавать в лицо одиннадцать сообщений компилятора из таблицы ошибок.
Углубиться

Что такое Unit. У if, использованного как команда, результат всё-таки есть — это значение Unit, единственное значение типа Unit. Оно означает «результата нет, но формально что-то вернулось». Функция без возвращаемого значения на самом деле возвращает Unit — просто это можно не писать. Это тот же приём, что void в Java, только Unit — настоящий тип, который можно указать и передать.

Почему for в Kotlin один. Заголовок for (int i = 0; i < n; i++) из C и Java — это три независимых куска кода, склеенных точками с запятой, и в нём легко ошибиться: перепутать < и <=, забыть шаг, изменить счётчик внутри тела. Kotlin заменил его перебором готового набора. Это ограничение, и иногда оно неудобно, но оно закрывает сразу четыре класса ошибок. Тот же путь прошли Python, Swift и Rust.

Во что превращаются диапазоны при компиляции. Может показаться, что for (i in 1..1_000_000) создаёт в памяти миллион чисел. Не создаёт: компилятор распознаёт цикл по IntRange и разворачивает его в обычный счётчик на уровне байт-кода — ровно такой же, как написал бы человек на Java руками. Объект диапазона при этом не создаётся вообще. А вот for (i in list.filter { ... }) уже создаёт промежуточный список, и для больших данных на это стоит смотреть.

when вместо таблицы переходов. switch в Java по целым числам компилируется в таблицу переходов — очень быстрый механизм, где переход выполняется за одно действие. when по константам компилируется так же. Но when без предмета — это последовательные проверки сверху вниз, как лестница if. Разница заметна только на очень горячем коде, но знать, что первая форма может быть быстрее, стоит.

Умное приведение и потоки. Формулировка «could be mutated concurrently» — не перестраховка теоретика. В Android приложение почти всегда работает в нескольких потоках: интерфейс в одном, сеть в другом. Свойство объекта действительно может измениться между строкой с проверкой и следующей строкой. Компилятор не разбирается, есть ли в конкретной программе второй поток, — он просто не даёт написать код, который сломается, когда поток появится.

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

Дальше по теме:

Сводная проверка по всей главе: 14 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

ВидеоУрок 5: Условные операторы if else when. Операции ветвления // Котлин курс с нуляKotlin & Android с нуля – Иван Ветров
ВидеоУрок 6: Циклы while, do-while. Обратный отсчет с Thread.sleep() // Котлин курс с нуляKotlin & Android с нуля – Иван Ветров
ВидеоУрок 7: Цикл for, диапазоны (интервалы). break, continue, return // Котлин курс с нуляKotlin & Android с нуля – Иван Ветров
ВидеоУчим Kotlin с нуля | №5 Циклы while, do-while, for.SyntaxSugarDaddy
ВидеоУРОКИ ПО KOTLIN - цикл for, while | Сравниваем с JavaТимофей Коваленко

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.