Условия и циклы на Kotlin
Зачем это нужно
В прошлой главе программа была прямой: объявили переменную, напечатали, объявили следующую. Так пишутся только самые первые примеры. Настоящий код почти целиком собран из двух вещей: выбрать одно из нескольких и повторить одно и то же для каждого элемента.
Экран приложения показывает список — это цикл. Кнопка становится серой, пока форма не заполнена, — это условие. Приложение решает, показать список или заглушку «пока пусто», — снова условие. Подсчёт суммы заказа по корзине — цикл с накоплением. Больше в программе, по сути, ничего и нет: остальное — оформление этих двух конструкций.
Эта глава разбирает их до последнего символа именно в Kotlin. И сразу главное: в Kotlin условие — это не команда, а выражение. У if есть результат, который можно присвоить переменной. У when — тоже. Тот, кто раньше писал на Java, C# или JavaScript, на этом месте обычно спотыкается: там if ничего не возвращает, а тут возвращает, и половина привычных конструкций становится лишней.
Все выводы и все сообщения об ошибках в этой главе сняты с живого запуска компилятора Kotlin 2.4.10 — того же, что стоит за кнопкой «Запустить» в примерах ниже. Ничего не придумано: если в тексте написано «компилятор скажет вот так», значит, он действительно так сказал.
- Зачем это нужно
if: развилка, у которой есть результат- Из чего собирают условие: сравнения и логика
- Лестница условий и ранний выход
whenс предметом: главная конструкция выбораwhenбез предмета: замена лестницыelse if- Когда
whenобязан быть исчерпывающим isи умное приведение типаnullв условиях:?.,?:и!!- Диапазоны: откуда цикл берёт числа
for: цикл по готовому наборуwhileиdo..while: цикл без известного числа проходовbreak,continueи меткиrepeatи обход коллекций без цикла- Ошибки этой главы и что они значат
- Тренажёр: конструкции этой главы
- Символы этой главы
- Если рядом учат TypeScript
- Что должен уметь
- Куда дальше: проверенные ресурсы
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("новичок")
}
}
Что выведет: новичок.
А теперь — то, ради чего эта глава начинается именно с 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 нет — и это одна из причин, по которым его код читается однозначнее.
Лестница условий и ранний выход
Когда вариантов больше двух, ветки выстраивают в лестницу через 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 вообще нет.
Второе: несколько значений в одной ветке — через запятую.
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. Это одна из самых полезных вещей во всём языке.
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:
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 — нет.
Диапазоны: откуда цикл берёт числа
Прежде чем разбирать 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..5 | 1 2 3 4 5 | обе границы включены — «с первого по пятый» |
1..<5 | 1 2 3 4 | правая граница не включена — «до пятого» |
1 until 5 | 1 2 3 4 | то же самое, старая запись |
5 downTo 1 | 5 4 3 2 1 | обратный ход |
1..9 step 4 | 1 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-типы.
Ошибки этой главы и что они значат
Все сообщения ниже сняты с настоящего запуска компилятора 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 "младший"
Символы этой главы
| Знак | Название | Что делает |
|---|---|---|
( ) | круглые скобки | держат условие у 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
На платформе есть параллельная глава про условия и циклы в блокчейн-треке. Тем, кто сидит за соседним компьютером, будет полезно сравнить — а тебе полезно знать, где у соседа по-другому:
| Что | Kotlin | TypeScript |
|---|---|---|
if как выражение | да, отдаёт значение | нет, только команда |
| Тернарный оператор | отсутствует, не нужен | условие ? а : б |
| Выбор из вариантов | when, без проваливания | switch, с break в каждой ветке |
| Условие принимает | только Boolean | что угодно, по правилам truthy/falsy |
| Сравнение строк | == | === |
Заголовок for | for (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.
Дальше по теме:
- Kotlin docs: Conditions and loops — официальная страница про
if,whenи циклы - Kotlin docs: Ranges and progressions — все формы диапазонов
- Kotlin docs: Null safety —
?.,?:,!!от первоисточника - Kotlin docs: Returns and jumps — метки и
return@
Сводная проверка по всей главе: 14 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
-
Kotlin docs: Conditions and loops —
if,when,for,whileот создателей языка. -
Kotlin docs: Ranges and progressions —
..,..<,downTo,stepв одном месте. -
Kotlin docs: Null safety — безопасный вызов, оператор Элвиса и почему
!!существует. -
Kotlin docs: Type checks and casts —
is,asи правила умного приведения. -
Kotlin docs: Returns and jumps —
break,continue, метки иreturn@. -
Kotlin docs: Equality — чем
==отличается от===. -
Kotlin docs: Coding conventions — официальный стиль: отступы, переносы, оформление
when. -
Kotlin Koans: Introduction — задачи прямо в браузере, с проверкой.
-
Kotlin Playground — тот же компилятор, что запускает примеры на этой странице.
-
Видео и материалы сообщества — ниже — ролики по теме и ссылки от студентов и преподавателей смотри в самом низу страницы.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.