Kotlin guide ready #5

Merged
roma merged 3 commits from AK_Additions into master 2026-10-20 13:38:06 +03:00
13 changed files with 2120 additions and 0 deletions
Showing only changes of commit da3ad9dd1a - Show all commits
+145
View File
@@ -0,0 +1,145 @@
---
title: Kotlin
description: Полное пособие по Kotlin — синтаксис, ООП, корутины, инструментарий
tags:
- kotlin
---
Статически типизированный язык от JetBrains, работающий на [JVM](../Программирование%20на%20Java/JVM/JVM.md), а также компилируемый в JavaScript, WebAssembly и нативный код.
- Первый релиз - 2011, стабильная 1.0 - 2016
- Официальный язык разработки для Android с 2019
- Полная двусторонняя совместимость с Java: один проект может содержать оба языка
- Текущая версия - 2.4.x, компилятор K2 по умолчанию
Главные отличия от Java: [null-безопасность](Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md) на уровне типов, [корутины](Корутины/Корутины.md) вместо потоков, выражения вместо инструкций, отсутствие проверяемых исключений, в 2-3 раза меньше кода на ту же задачу.
## Маршрут
Последовательность для изучения с нуля. Каждая заметка заканчивается ссылкой `Далее:`, так что можно просто идти по цепочке.
1. [Hello World!](Синтаксис%20и%20типы/Базовый%20синтаксис/Hello%20World!.md) - начало
2. [Null-безопасность](Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md) - ядро системы типов
3. [ООП](ООП/ООП.md) - классы и объекты
4. [Коллекции](Коллекции/Коллекции.md) - структуры данных
5. [Функции высшего порядка](Функциональный%20Kotlin/Функции%20высшего%20порядка.md) - функциональный стиль
6. [Корутины](Корутины/Корутины.md) - асинхронность
## Синтаксис и типы
### Базовый синтаксис
- [Hello World!](Синтаксис%20и%20типы/Базовый%20синтаксис/Hello%20World!.md)
- [Типы данных](Синтаксис%20и%20типы/Базовый%20синтаксис/Типы%20данных.md)
- [Переменные](Синтаксис%20и%20типы/Базовый%20синтаксис/Переменные.md)
- [Операторы и выражения](Синтаксис%20и%20типы/Базовый%20синтаксис/Операторы%20и%20выражения.md)
- [Условные конструкции](Синтаксис%20и%20типы/Базовый%20синтаксис/Условные%20конструкции.md)
- [Циклы и диапазоны](Синтаксис%20и%20типы/Базовый%20синтаксис/Циклы%20и%20диапазоны.md)
- [Строковые шаблоны и вывод](Синтаксис%20и%20типы/Базовый%20синтаксис/Строковые%20шаблоны%20и%20вывод.md)
- [Пакеты и импорты](Синтаксис%20и%20типы/Базовый%20синтаксис/Пакеты%20и%20импорты.md)
### Null-безопасность
- [Null-безопасность](Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md)
- [Операторы null-безопасности](Синтаксис%20и%20типы/Null-безопасность/Операторы%20null-безопасности.md)
- [Умные приведения](Синтаксис%20и%20типы/Null-безопасность/Умные%20приведения.md)
- [Платформенные типы](Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md)
### Работа с данными
- [Строки](Синтаксис%20и%20типы/Работа%20с%20данными/Строки.md)
- [Char](Синтаксис%20и%20типы/Работа%20с%20данными/Char.md)
- [Числа и переполнение](Синтаксис%20и%20типы/Работа%20с%20данными/Числа%20и%20переполнение.md)
- [Массивы](Синтаксис%20и%20типы/Работа%20с%20данными/Массивы.md)
- [Функции](Синтаксис%20и%20типы/Работа%20с%20данными/Функции.md)
- [Лямбды и функциональные типы](Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md)
- [Деструктуризация](Синтаксис%20и%20типы/Работа%20с%20данными/Деструктуризация.md)
- [Приведение типов](Синтаксис%20и%20типы/Работа%20с%20данными/Приведение%20типов.md)
### Базовые типы ООП
- [Any, Unit, Nothing](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md)
- [Data class](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
- [Enum class](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Enum%20class.md)
- [Sealed class и interface](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
- [Value class](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Value%20class.md)
- [Object и companion object](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md)
- [Typealias](Синтаксис%20и%20типы/Базовые%20типы%20ООП/Typealias.md)
- [equals, hashCode и toString](Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md)
## ООП
- [ООП](ООП/ООП.md)
- [Классы и конструкторы](ООП/Классы%20и%20конструкторы.md)
- [Свойства](ООП/Свойства.md)
- [Инициализация](ООП/Инициализация.md)
- [Модификаторы видимости](ООП/Модификаторы%20видимости.md)
- [Наследование](ООП/Наследование.md)
- [Переопределение](ООП/Переопределение.md)
- [Абстрактные классы](ООП/Абстрактные%20классы.md)
- [Интерфейсы](ООП/Интерфейсы.md)
- [Делегирование](ООП/Делегирование.md)
- [Вложенные и внутренние классы](ООП/Вложенные%20и%20внутренние%20классы.md)
- [Полиморфизм](ООП/Полиморфизм.md)
## Исключения
- [Система исключений](Исключения/Система%20исключений.md)
- [Result и runCatching](Исключения/Result%20и%20runCatching.md)
- [Проверки и контракты](Исключения/Проверки%20и%20контракты.md)
## Коллекции
- [Коллекции](Коллекции/Коллекции.md)
- [List, Set, Map](Коллекции/List,%20Set,%20Map.md)
- [Изменяемость](Коллекции/Изменяемость.md)
- [Операции](Коллекции/Операции.md)
- [Sequence](Коллекции/Sequence.md)
- [Коллекции под капотом](Коллекции/Коллекции%20под%20капотом.md)
## Дженерики
- [Параметрический полиморфизм](Дженерики/Параметрический%20полиморфизм.md)
- [Вариантность](Дженерики/Вариантность.md)
- [Проекции типов](Дженерики/Проекции%20типов.md)
- [Reified и inline](Дженерики/Reified%20и%20inline.md)
## Функциональный Kotlin
- [Функции высшего порядка](Функциональный%20Kotlin/Функции%20высшего%20порядка.md)
- [Inline-функции](Функциональный%20Kotlin/Inline-функции.md)
- [Функции-расширения](Функциональный%20Kotlin/Функции-расширения.md)
- [Scope-функции](Функциональный%20Kotlin/Scope-функции.md)
- [Инфиксные и операторные функции](Функциональный%20Kotlin/Инфиксные%20и%20операторные%20функции.md)
- [Receivers и context parameters](Функциональный%20Kotlin/Receivers%20и%20context%20parameters.md)
- [DSL](Функциональный%20Kotlin/DSL.md)
- [Паттерны проектирования](Функциональный%20Kotlin/Паттерны%20проектирования.md)
## Корутины
- [Корутины](Корутины/Корутины.md)
- [Suspend-функции](Корутины/Suspend-функции.md)
- [Структурированная конкурентность](Корутины/Структурированная%20конкурентность.md)
- [Билдеры корутин](Корутины/Билдеры%20корутин.md)
- [Диспетчеры и контекст](Корутины/Диспетчеры%20и%20контекст.md)
- [Отмена и таймауты](Корутины/Отмена%20и%20таймауты.md)
- [Исключения в корутинах](Корутины/Исключения%20в%20корутинах.md)
- [Flow](Корутины/Flow.md)
- [StateFlow и SharedFlow](Корутины/StateFlow%20и%20SharedFlow.md)
- [Channels](Корутины/Channels.md)
- [Разделяемое состояние](Корутины/Разделяемое%20состояние.md)
- [Корутины против потоков](Корутины/Корутины%20против%20потоков.md)
## Взаимодействие с Java
- [Взаимодействие с Java](Взаимодействие%20с%20Java/Взаимодействие%20с%20Java.md)
- [Java из Kotlin](Взаимодействие%20с%20Java/Java%20из%20Kotlin.md)
- [Kotlin из Java](Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
- [Kotlin в байт-коде](Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
## Инструментарий
- [Инструментарий](Инструментарий/Инструментарий.md)
- [kotlinc](Инструментарий/kotlinc.md)
- [Gradle и Kotlin DSL](Инструментарий/Gradle%20и%20Kotlin%20DSL.md)
- [Структура проекта](Инструментарий/Структура%20проекта.md)
- [Компилятор K2](Инструментарий/Компилятор%20K2.md)
- [KSP](Инструментарий/KSP.md)
- [Линтеры и формат](Инструментарий/Линтеры%20и%20формат.md)
## Тестирование
- [Тестирование](Тестирование/Тестирование.md)
- [kotlin.test и JUnit 5](Тестирование/kotlin.test%20и%20JUnit%205.md)
- [Kotest](Тестирование/Kotest.md)
- [MockK](Тестирование/MockK.md)
- [Тестирование корутин](Тестирование/Тестирование%20корутин.md)
## Рефлексия
- [Рефлексия](Рефлексия/Рефлексия.md)
- [Аннотации](Рефлексия/Аннотации.md)
@@ -0,0 +1,185 @@
---
title: Null-безопасность
description: Разделение типов на nullable и non-null, как компилятор предотвращает NPE
tags:
- kotlin
- null-safety
---
Главная особенность системы типов Kotlin: **возможность быть null - часть типа**.
```kotlin
var a: String = "text"
// a = null // ошибка компиляции
var b: String? = "text"
b = null // ok
```
`String` и `String?` - разные типы. `String` является подтипом `String?`, но не наоборот.
```mermaid
graph TD
SN["String? — допускает null"]
S["String — не допускает null"]
N["Nothing? — только null"]
SN --> S
SN --> N
```
## Зачем это нужно
`NullPointerException` в Java возникает в рантайме, иногда далеко от места, где null попал в переменную. В Kotlin компилятор отказывается собирать такой код:
```kotlin
fun length(s: String?): Int {
// return s.length // ошибка: only safe (?.) or non-null (!!) calls are allowed
return s?.length ?: 0
}
```
Проблема переносится с этапа выполнения на этап компиляции. Разыменовать nullable-значение без проверки невозможно.
## Работа с nullable
Пять подходов, от предпочтительного к крайнему:
### 1. Проверка на null
После проверки компилятор сам приводит тип:
```kotlin
fun describe(s: String?): String {
if (s == null) return "нет значения"
return "длина ${s.length}" // здесь s уже String, не String?
}
```
Это [умное приведение](Умные%20приведения.md).
### 2. Безопасный вызов - ?.
```kotlin
val len: Int? = s?.length // null, если s == null
```
### 3. Elvis - ?:
```kotlin
val len: Int = s?.length ?: 0 // значение по умолчанию
val name = user?.name ?: error("нет пользователя") // или выход
```
### 4. Безопасное приведение - as?
```kotlin
val n: Int? = value as? Int // null вместо ClassCastException
```
### 5. Утверждение - !!
```kotlin
val len: Int = s!!.length // NPE, если s == null
```
`!!` означает «я гарантирую, что здесь не null». Каждое его использование - это место, где вы отключили проверку компилятора.
Подробнее про операторы: [Операторы null-безопасности](Операторы%20null-безопасности.md)
## Nullable в коллекциях
`?` ставится раздельно для элементов и для самой коллекции:
```kotlin
val a: List<String> = listOf("x") // список есть, элементы не null
val b: List<String?> = listOf(null) // список есть, элементы могут быть null
val c: List<String>? = null // списка может не быть
val d: List<String?>? = null // и то, и другое
```
Для отсеивания null есть готовые функции:
```kotlin
val items: List<String?> = listOf("a", null, "b")
println(items.filterNotNull()) // [a, b]
```
## Nullable и дженерики
Параметр типа по умолчанию допускает null, его верхняя граница - `Any?`:
```kotlin
fun <T> process(item: T) { } // T может быть nullable
fun <T : Any> strict(item: T) { } // T гарантированно non-null
strict("text")
// strict(null) // ошибка компиляции
```
Подробнее: [Параметрический полиморфизм](../../Дженерики/Параметрический%20полиморфизм.md)
## Где NPE всё-таки возможен
Kotlin не даёт абсолютной гарантии. Источники NPE:
1. **Явный `!!`** - вы сами попросили
2. **Явный `throw NullPointerException()`**
3. **Код на Java** - типы оттуда компилятор проверить не может ([Платформенные типы](Платформенные%20типы.md))
4. **`lateinit` до инициализации** - технически `UninitializedPropertyAccessException`
5. **Утечка `this` из конструктора** - обращение к ещё не инициализированному свойству
```kotlin
class Broken {
val a: String = compute() // вызывается до инициализации b
val b: String = "value"
fun compute(): String = b.uppercase() // NPE: b ещё null
}
```
Последний случай компилятор обычно замечает и выдаёт предупреждение, но через несколько уровней вызовов - может и пропустить.
## Правило
Если у вас много `!!` - типы спроектированы неправильно. Nullable-тип должен появляться там, где отсутствие значения - реальная часть предметной области («у пользователя может не быть отчества»), а не там, где лень разбираться с инициализацией.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | любая ссылка может быть null | null допускают только типы с `?` |
> | NPE в рантайме | ошибка компиляции |
> | `Optional<T>` как обёртка | `T?` на уровне типов, без аллокации |
> | `@Nullable`/`@NotNull` - подсказки для IDE | часть системы типов, проверяется компилятором |
> | `Objects.requireNonNull(x)` | `requireNotNull(x)` или просто non-null тип |
>
> `Optional<T>` в Java - это объект-обёртка: аллокация, нет поддержки в полях, не композируется с дженериками. `T?` в Kotlin не создаёт обёртки: в байт-коде это обычная ссылка, а проверки расставляет компилятор.
>
> Публичные функции Kotlin получают проверки параметров автоматически - вызов из Java с null упадёт сразу на входе, с понятным сообщением, а не глубже по стеку:
> ```kotlin
> fun greet(name: String) = "Hi, $name"
> ```
> ```java
> GreetKt.greet(null);
> // NullPointerException: Parameter specified as non-null is null:
> // method GreetKt.greet, parameter name
> ```
> В сообщении указаны и метод, и имя параметра.
>
> Сравнить: [Приведение ссылочных типов в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Приведение%20ссылочных%20типов.md)
> [!warning] Подводные камни
> **`!!` - не решение, а отложенная проблема.** Строка `user!!.profile!!.name!!` падает без указания, какое из трёх звеньев было null.
>
> **Умное приведение не работает с `var` из другого потока и с открытыми свойствами:**
> ```kotlin
> class Holder {
> var value: String? = null
> fun use() {
> if (value != null) {
> // println(value.length) // ошибка: smart cast невозможен
> println(value!!.length) // или скопировать в локальную val
> }
> }
> }
> ```
> Подробнее: [Умные приведения](Умные%20приведения.md)
>
> **Платформенные типы из Java проверок не имеют:**
> ```kotlin
> val s: String = javaMethodThatReturnsNull() // скомпилируется, упадёт в рантайме
> ```
>
> **`Boolean?` нельзя использовать в `if` напрямую:**
> ```kotlin
> val flag: Boolean? = null
> // if (flag) { } // ошибка компиляции
> if (flag == true) { } // так
> if (flag ?: false) { } // или так
> ```
>
> **`null` в строковом шаблоне превращается в текст:**
> ```kotlin
> val x: String? = null
> println("значение: $x") // значение: null
> ```
Далее: [Операторы null-безопасности](Операторы%20null-безопасности.md)
@@ -0,0 +1,192 @@
---
title: Операторы null-безопасности
description: Безопасный вызов ?., elvis ?:, as?, !! и функции let, requireNotNull
tags:
- kotlin
- null-safety
---
Четыре оператора и несколько функций стандартной библиотеки для работы с nullable-типами.
## Безопасный вызов - ?.
Вызывает член, если значение не null, иначе возвращает null:
```kotlin
val s: String? = null
println(s?.length) // null
println(s?.uppercase()) // null
val t: String? = "text"
println(t?.length) // 4
```
Результат `?.` всегда nullable - даже если сам член возвращает non-null тип.
### Цепочки
```kotlin
class Address(val city: String?)
class User(val address: Address?)
val user: User? = null
val city: String? = user?.address?.city // null, если оборвалось где угодно
```
Цепочка прерывается на первом null, остальные вызовы не выполняются. Это «безопасная навигация» - в Java пришлось бы писать три вложенных `if`.
### С присваиванием
```kotlin
class Box { var value: Int = 0 }
val box: Box? = null
box?.value = 42 // ничего не произойдёт, если box == null
```
## Elvis - ?:
Возвращает левую часть, если она не null, иначе правую:
```kotlin
val name: String? = null
println(name ?: "анонимный") // анонимный
val len = name?.length ?: 0 // 0
```
Назван так из-за схожести `?:` с причёской Элвиса Пресли.
Правая часть вычисляется **только** при null (ленивость):
```kotlin
val value = cached ?: expensiveCompute() // вызов только при необходимости
```
### Для раннего выхода
Справа может стоять `return` или `throw` - они имеют тип [Nothing](../Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md), поэтому подходят под любой тип:
```kotlin
fun process(input: String?): Int {
val text = input ?: return -1 // выход из функции
val n = text.toIntOrNull() ?: error("не число") // исключение
return n * 2
}
```
Это идиома: вместо вложенных проверок - плоская последовательность «достали или вышли».
## Безопасное приведение - as?
Возвращает null вместо `ClassCastException`:
```kotlin
val value: Any = "text"
val n: Int? = value as? Int // null
val s: String? = value as? String // "text"
val safe = value as? Int ?: 0 // 0 — частая связка с elvis
```
Обычный `as` при несовпадении типов падает:
```kotlin
// val bad = value as Int // ClassCastException
```
## Утверждение non-null - !!
Превращает `T?` в `T`, бросая NPE при null:
```kotlin
val s: String? = "text"
println(s!!.length) // 4
val n: String? = null
// println(n!!.length) // NullPointerException
```
Применять только когда null означает ошибку программиста, и падение предпочтительнее тихого продолжения. В остальных случаях есть `?:`, `?.` или проверка.
## let для nullable
Выполняет блок, только если значение не null:
```kotlin
val name: String? = "Kotlin"
name?.let {
println(it.length) // внутри it — уже String, не String?
}
val upper = name?.let { it.uppercase() } ?: "ПУСТО"
```
Полезно, когда нужно несколько операций и нежелательно повторять `?.`:
```kotlin
user?.let {
log(it.name)
send(it.email)
it.lastSeen = now()
}
```
Подробнее про `let`, `run`, `also`, `apply`: [Scope-функции](../../Функциональный%20Kotlin/Scope-функции.md)
## Функции проверки
```kotlin
val value: String? = getValue()
val a = requireNotNull(value) // IllegalArgumentException
val b = requireNotNull(value) { "нет значения" }
val c = checkNotNull(value) // IllegalStateException
```
Обе возвращают non-null значение, то есть заменяют `!!` с внятным сообщением. Разница - в типе исключения: `require*` для проверки аргументов, `check*` для проверки состояния.
Подробнее: [Проверки и контракты](../../Исключения/Проверки%20и%20контракты.md)
## Полезные nullable-функции stdlib
```kotlin
val s: String? = null
println(s.isNullOrEmpty()) // true — работает на nullable-получателе
println(s.isNullOrBlank()) // true
println(s.orEmpty()) // "" — строка вместо null
val list: List<Int>? = null
println(list.orEmpty()) // []
println(list?.size ?: 0) // 0
println("42".toIntOrNull()) // 42
println("abc".toIntOrNull()) // null — вместо исключения
```
Функции с суффиксом `OrNull` есть у многих операций: `firstOrNull()`, `singleOrNull()`, `toIntOrNull()`, `getOrNull(i)`.
## Сводка
| Оператор | Результат при null | Когда применять |
|---|---|---|
| `?.` | `null` | навигация по цепочке |
| `?:` | правая часть | значение по умолчанию, ранний выход |
| `as?` | `null` | неуверенное приведение типа |
| `!!` | NPE | null невозможен по инварианту |
| `?.let {}` | блок не выполняется | несколько действий со значением |
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `if (x != null) x.foo()` | `x?.foo()` |
> | `x != null ? x : def` | `x ?: def` |
> | `x instanceof T ? (T) x : null` | `x as? T` |
> | `Objects.requireNonNull(x)` | `requireNotNull(x)` или `x!!` |
> | `Optional.ofNullable(x).map(f).orElse(d)` | `x?.let(f) ?: d` |
> | три вложенных `if` для цепочки | `a?.b?.c` |
>
> Java-аналог `Optional` требует оборачивания и разворачивания, работает только как возвращаемое значение и создаёт объект. Операторы Kotlin компилируются в обычные проверки `ifnull` в байт-коде - накладных расходов нет.
> [!warning] Подводные камни
> **`?.` возвращает nullable, цепочка «заражает» весь результат:**
> ```kotlin
> val list: List<String>? = listOf("a")
> val size: Int? = list?.size // Int?, не Int
> val sure: Int = list?.size ?: 0 // нужен elvis
> ```
>
> **Приоритет `?:` ниже арифметики** - без скобок получится не то:
> ```kotlin
> val a: Int? = null
> println((a ?: 0) + 1) // 1
> // println(a ?: 0 + 1) // это a ?: (0+1), то есть 1 — здесь совпало,
> // // но в сложных выражениях расхождение будет
> ```
>
> **`as?` не спасает от null, только от неверного типа:**
> ```kotlin
> val x: Any? = null
> val s: String? = x as? String // null — потому что x был null
> ```
>
> **`!!` в цепочке скрывает, что именно упало.** `a!!.b!!.c!!` даёт NPE без указания звена. Либо проверяйте по шагам, либо используйте `requireNotNull` с сообщением.
>
> **`?.let` возвращает null, если получатель null** - а не то, что вернул блок:
> ```kotlin
> val s: String? = null
> val r: Int? = s?.let { 42 } // null, блок не выполнялся
> ```
>
> **Безопасный вызов на функции-расширении с nullable-получателем работает иначе.** `s.isNullOrEmpty()` вызывается без `?.`, потому что расширение объявлено для `String?` и само обрабатывает null внутри.
Дополнительно: [Scope-функции](../../Функциональный%20Kotlin/Scope-функции.md)
Далее: [Умные приведения](Умные%20приведения.md)
@@ -0,0 +1,160 @@
---
title: Платформенные типы
description: Тип T! из Java-кода, где null-безопасность Kotlin перестаёт работать и как это чинить
tags:
- kotlin
- null-safety
- interop
---
Платформенный тип (platform type) - тип, пришедший из Java, про nullability которого компилятор Kotlin ничего не знает. Обозначается `T!`, но написать его в коде нельзя - он существует только в сообщениях компилятора и в IDE.
```java
// Java
public class Repo {
public String findName() { return null; }
}
```
```kotlin
// Kotlin
val repo = Repo()
val a: String = repo.findName() // компилируется! падает в рантайме
val b: String? = repo.findName() // тоже компилируется
```
`findName()` имеет тип `String!` - его можно присвоить и в `String`, и в `String?`. Проверку компилятор не делает.
## Почему так
Альтернативы были хуже:
- считать все Java-типы nullable → весь Java-код пришлось бы обвешивать `!!` и `?.`, включая `System.out`
- считать все non-null → ложное чувство безопасности без возможности это заметить
Платформенный тип - компромисс: ответственность за nullability возвращается программисту, как в Java.
## Где падает
NPE возникает в точке присваивания, а не использования - компилятор вставляет проверку на границе:
```kotlin
val a: String = repo.findName() // NPE здесь же
println(a.length) // до этой строки не дойдёт
```
Это лучше, чем в Java: падение происходит рядом с источником, а не там, куда значение утекло.
## Как чинить
### 1. Указывать nullable-тип явно
Любое значение из Java трактуйте как `T?`, пока не убедились в обратном:
```kotlin
val name: String? = repo.findName()
println(name?.length ?: 0)
```
### 2. Аннотации в Java-коде
Компилятор Kotlin читает аннотации nullability и начинает проверять типы:
```java
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.NotNull;
public class Repo {
@Nullable public String findName() { return null; }
@NotNull public String getId() { return "1"; }
}
```
```kotlin
val a: String = repo.findName() // теперь ошибка компиляции
val b: String? = repo.findName() // ok
val c: String = repo.getId() // ok, проверка не нужна
```
Поддерживаются аннотации из нескольких наборов:
| Набор | Пакет |
|---|---|
| JetBrains | `org.jetbrains.annotations` |
| JSpecify | `org.jspecify.annotations` |
| Android | `androidx.annotation` |
| JSR-305 | `javax.annotation` |
| Eclipse | `org.eclipse.jdt.annotation` |
| Lombok | `lombok.NonNull` |
### 3. Обёртка на границе
Для внешней библиотеки без аннотаций - свой слой, который один раз разбирается с nullability:
```kotlin
// вся работа с «грязными» типами собрана здесь
fun Repo.nameOrNull(): String? = findName()
fun Repo.requireName(): String = findName() ?: error("name отсутствует")
```
Дальше код работает с честными типами Kotlin.
### 4. Строгий режим компилятора
Флаг `-Xjsr305=strict` заставляет компилятор относиться к JSR-305-аннотациям как к обязательным:
```kotlin
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xjsr305=strict")
}
}
```
Значения: `ignore`, `warn` (по умолчанию), `strict`.
## Коллекции из Java
Платформенность распространяется на параметры типа:
```java
public List<String> getItems() { return null; }
```
Тип результата - `(Mutable)List<String!>!`. Неизвестно ни то, может ли быть null сам список, ни его элементы:
```kotlin
val items: List<String?>? = repo.items // самое честное предположение
items?.filterNotNull()?.forEach(::println)
```
Отдельная проблема - изменяемость. `java.util.List` отображается в `(Mutable)List`, то есть Kotlin позволит передать его туда, где ожидается `MutableList`, и Java-код может изменить «неизменяемый» список:
```kotlin
val list: List<String> = repo.items // на самом деле ArrayList
// list.add("x") // так нельзя, тип List
(list as MutableList).add("x") // а так — да, и это сработает
```
Подробнее: [Java из Kotlin](../../Взаимодействие%20с%20Java/Java%20из%20Kotlin.md)
## Обратное направление
Код на Kotlin для Java-вызывающего выглядит аннотированным: компилятор добавляет `@NotNull`/`@Nullable` в байт-код автоматически. Плюс в публичные функции вставляются проверки параметров:
```kotlin
fun greet(name: String) = "Hi, $name"
```
```java
GreetKt.greet(null);
// NullPointerException: Parameter specified as non-null is null:
// method GreetKt.greet, parameter name
```
Падение происходит на входе в функцию, с указанием метода и имени параметра.
> [!info] Отличия от Java
> В Java платформенных типов нет - там все ссылки одинаково могут быть null, и никакой проверки не существует.
>
> Платформенные типы - это механизм совместимости, а не фича языка. Их задача - позволить Kotlin вызывать существующие библиотеки без переписывания. Чем больше кода в проекте на Kotlin, тем реже они встречаются.
>
> Сравнить: [Система исключений в Java](../../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
> [!warning] Подводные камни
> **Самый опасный случай - значение «просачивается» в non-null тип:**
> ```kotlin
> data class User(val name: String)
> val user = User(repo.findName()) // NPE в конструкторе, а причина — в Java-методе
> ```
>
> **IDE подсказывает тип `String!`, но в коде так написать нельзя.** Если увидели `!` в подсказке - значит, компилятор вас не защищает.
>
> **Аннотации на уровне пакета легко пропустить.** `@NonNullApi` или `package-info.java` с `@ParametersAreNonnullByDefault` меняют поведение для всего пакета. Spring, например, аннотирует свои пакеты - поэтому его API в Kotlin выглядит типизированным.
>
> **`-Xjsr305=strict` может сломать сборку при обновлении библиотеки** - новые аннотации превратятся в ошибки компиляции. Включать осознанно.
>
> **Дженерик-методы Java особенно коварны:**
> ```java
> public <T> T getOrNull(String key) { return null; }
> ```
> ```kotlin
> val x: String = repo.getOrNull("k") // T выведется в String, NPE в рантайме
> ```
>
> **Массивы из Java тоже платформенные:** `String[]` → `Array<(out) String!>!`. Элементы могут быть null даже если сам массив не null.
Дополнительно: [Java из Kotlin](../../Взаимодействие%20с%20Java/Java%20из%20Kotlin.md)
Далее: [Строки](../Работа%20с%20данными/Строки.md)
@@ -0,0 +1,241 @@
---
title: Умные приведения
description: Smart cast после is и проверки на null, условия работы и случаи, когда компилятор отказывается
tags:
- kotlin
- null-safety
- types
---
Умное приведение (smart cast) - автоматическое приведение типа после проверки. Компилятор отслеживает поток управления и знает, что в этой ветке тип уже уточнён.
```kotlin
fun describe(value: Any): String {
if (value is String) {
return "строка длины ${value.length}" // value уже String
}
return "не строка"
}
```
Явного `(String) value` писать не нужно - в отличие от Java (до версии 16).
## После проверки типа - is
```kotlin
fun size(x: Any): Int = when (x) {
is String -> x.length // String
is List<*> -> x.size // List<*>
is Map<*, *> -> x.size // Map<*, *>
else -> 0
}
```
`!is` работает так же, с инверсией:
```kotlin
fun check(x: Any) {
if (x !is String) return
println(x.length) // после return здесь только String
}
```
## После проверки на null
```kotlin
fun length(s: String?): Int {
if (s == null) return 0
return s.length // s: String
}
fun length2(s: String?): Int {
if (s != null) {
return s.length // s: String
}
return 0
}
```
## В логических выражениях
Приведение работает внутри `&&` и `||`, так как они ленивые:
```kotlin
fun check(x: Any?): Boolean {
return x is String && x.length > 3 // x приведён во второй части
}
fun check2(x: Any?): Boolean {
return x !is String || x.length > 3 // приведён в правой части ||
}
```
## В elvis и безопасных вызовах
```kotlin
fun process(s: String?) {
val text = s ?: return // после этой строки s... нет, но text: String
println(text.length)
}
```
## Где приведение НЕ работает
Главное условие: компилятор должен гарантировать, что **значение не изменится между проверкой и использованием**.
### Изменяемое свойство класса - var
```kotlin
class Holder {
var value: String? = null
fun broken() {
if (value != null) {
// println(value.length) // ошибка: smart cast to 'String' is impossible
}
}
fun fixed() {
val local = value // копия в локальную val
if (local != null) {
println(local.length) // работает
}
}
}
```
Почему: между проверкой и обращением другой поток (или вызванная функция) может присвоить `value = null`.
### Свойство с кастомным геттером
```kotlin
class Random2 {
val value: String?
get() = if (Math.random() > 0.5) "x" else null
fun broken() {
if (value != null) {
// println(value.length) // ошибка: геттер вернёт разное
}
}
}
```
### open-свойство
```kotlin
open class Base {
open val value: String? = "x"
}
fun check(b: Base) {
if (b.value != null) {
// println(b.value.length) // ошибка: наследник мог переопределить геттер
}
}
```
### Свойство из другого модуля
Даже `val` из другой единицы компиляции не приводится: компилятор не может доказать, что у него нет кастомного геттера.
### Локальная var, захваченная лямбдой
```kotlin
fun broken() {
var s: String? = "text"
val modify = { s = null }
if (s != null) {
// println(s.length) // ошибка: лямбда может изменить s
}
}
```
## Сводка условий
| Что проверяем | Умное приведение |
|---|---|
| локальная `val` | ✅ работает |
| локальная `var` без захвата лямбдой | ✅ работает |
| локальная `var`, захваченная лямбдой | ❌ |
| `val`-свойство в том же модуле, без геттера | ✅ работает |
| `var`-свойство | ❌ |
| `open val` | ❌ |
| `val` с кастомным геттером | ❌ |
| `val` из другого модуля | ❌ |
| параметр функции (всегда `val`) | ✅ работает |
Универсальное решение во всех ❌-случаях: скопировать значение в локальную `val`.
## Явное приведение - as
Когда умное не применимо:
```kotlin
val x: Any = "text"
val s = x as String // ClassCastException при несовпадении
val n = x as? Int // null при несовпадении
```
Подробнее: [Приведение типов](../Работа%20с%20данными/Приведение%20типов.md)
## Умное приведение в Kotlin 2.x
Компилятор K2 (по умолчанию с Kotlin 2.0) расширил анализ. Теперь работает то, что раньше требовало ручного приведения:
```kotlin
fun test(x: Any?) {
val isString = x is String
if (isString) {
println(x.length) // K2 понимает связь через переменную-флаг
}
}
fun orElse(x: Any?) {
if (x !is String) return
println(x.length) // работало и раньше
}
```
Подробнее: [Компилятор K2](../../Инструментарий/Компилятор%20K2.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `if (x instanceof String) { String s = (String) x; ... }` | `if (x is String) { x.length }` |
> | pattern matching `x instanceof String s` (Java 16+) | есть с первой версии, без объявления переменной |
> | приведение только по типу | ещё и по null |
> | компилятор не отслеживает изменяемость | отказывается приводить, если значение может измениться |
>
> Java 16 добавила pattern matching: `if (x instanceof String s)`. Это ближе к Kotlin, но Java всё равно требует явно назвать переменную, и на nullability это не распространяется.
>
> Сравнить: [Приведение ссылочных типов в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Приведение%20ссылочных%20типов.md)
> [!warning] Подводные камни
> **Сообщение компилятора объясняет причину** - его стоит читать целиком:
> ```
> smart cast to 'String' is impossible, because 'value' is a mutable property
> that could be mutated concurrently
>
> smart cast to 'String' is impossible, because 'value' is a property
> that has an open or custom getter
>
> smart cast to 'String' is impossible, because 's' is a local variable
> that is mutated in a capturing closure
> ```
> Там прямо сказано и что мешает, и почему.
>
> **`!!` вместо локальной `val` - плохой обмен.** Оба компилируются, но второй вариант предсказуем:
> ```kotlin
> // println(value!!.length) // падение при гонке
> val v = value ?: return // гонки нет
> ```
>
> **Приведение дженериков неполное из-за стирания типов:**
> ```kotlin
> fun check(x: Any) {
> if (x is List<*>) {
> // элементы остаются Any?, тип параметра неизвестен
> val first = x.firstOrNull() // Any?
> }
> // if (x is List<String>) // ошибка: cannot check for instance of erased type
> }
> ```
> Обход - [reified](../../Дженерики/Reified%20и%20inline.md).
>
> **Приведение теряется после вызова функции, меняющей состояние** - если это свойство, а не локальная переменная:
> ```kotlin
> class C {
> var s: String? = "x"
> fun reset() { s = null }
> }
> ```
>
> **В `when` без аргумента приведение работает только для проверенной переменной:**
> ```kotlin
> fun combine(a: Any?, b: Any?): Int = when {
> a is String && b is Int -> a.length + b // оба приведены
> else -> 0
> }
> ```
Далее: [Платформенные типы](Платформенные%20типы.md)
@@ -0,0 +1,53 @@
---
title: Hello World!
description: Первая программа на Kotlin, функция main и запуск через kotlinc
tags:
- kotlin
- syntax
---
```kotlin
fun main() {
println("Hello World")
}
```
Файл `Hello.kt`. Имя файла **не связано** с содержимым - можно назвать как угодно.
- `fun` - объявление функции
- `main` - точка входа, может быть без параметров
- `println` - функция из стандартной библиотеки, не метод класса
- Точки с запятой не нужны
## Точка входа
Два допустимых варианта:
```kotlin
fun main() { } // обычно используют этот
fun main(args: Array<String>) { } // если нужны аргументы командной строки
```
Функция `main` должна быть на верхнем уровне файла (top-level), внутри класса её быть не может.
## Запуск
```bash
kotlinc Hello.kt -include-runtime -d hello.jar
java -jar hello.jar
```
Или через скриптовый режим, без создания jar:
```bash
kotlin Hello.kt
```
Подробнее: [kotlinc](../../Инструментарий/kotlinc.md)
> [!info] Отличия от Java
> В Java `main` обязан лежать внутри класса, быть `public static void` и принимать `String[]`, а имя файла должно совпадать с именем публичного класса ([Hello World! в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Hello%20World!.md)).
> В Kotlin всё это не требуется: функции живут прямо в файле.
>
> На уровне байт-кода класс всё равно появляется - компилятор создаёт `HelloKt` с методом `public static void main(String[])`. Поэтому JVM запускает Kotlin-код без каких-либо адаптеров.
> Подробнее: [Kotlin в байт-коде](../../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
> [!warning] Подводные камни
> - `fun Main()` или `fun start()` скомпилируется, но JVM точку входа не найдёт - имя строго `main`
> - Если в файле есть `package`, запускать надо с полным именем: `java -cp . com.example.HelloKt`
> - Две функции `main()` в одном пакете в разных файлах - это нормально, классы-обёртки будут разными (`HelloKt`, `AppKt`)
Далее: [Типы данных](Типы%20данных.md)
@@ -0,0 +1,182 @@
---
title: Операторы и выражения
description: Арифметика, равенство == и ===, побитовые операции словами, приоритет операторов
tags:
- kotlin
- syntax
---
В Kotlin почти все операторы - это вызовы функций. `a + b` компилируется в `a.plus(b)`, поэтому операторы можно определять для своих типов ([Операторные функции](../../Функциональный%20Kotlin/Инфиксные%20и%20операторные%20функции.md)).
## Арифметические
| Оператор | Функция | Пример |
|---|---|---|
| `+` | `plus` | `2 + 3` → 5 |
| `-` | `minus` | `5 - 3` → 2 |
| `*` | `times` | `2 * 3` → 6 |
| `/` | `div` | `7 / 2` → 3 |
| `%` | `rem` | `7 % 2` → 1 |
Целочисленное деление отбрасывает дробную часть:
```kotlin
println(7 / 2) // 3
println(7.0 / 2) // 3.5
println(7 / 2.0) // 3.5
println(-7 / 2) // -3 — округление к нулю
println(-7 % 2) // -1 — знак остатка совпадает со знаком делимого
```
### Инкремент и декремент
```kotlin
var i = 0
println(i++) // 0, потом i = 1
println(++i) // 2
```
Работают только с `var`. Присваивания-операторы (`+=`, `-=`, `*=`, `/=`, `%=`) - тоже.
## Сравнение
`<`, `>`, `<=`, `>=` транслируются в `compareTo`:
```kotlin
println(2 < 3) // true
println("abc" < "abd") // true — лексикографически
```
Работает для любого типа, реализующего `Comparable`.
## Равенство: == и ===
Это главное место, где Kotlin расходится с Java.
| Оператор | Что делает |
|---|---|
| `==` | структурное равенство, вызывает `equals()` |
| `===` | ссылочное равенство, сравнивает адреса |
| `!=`, `!==` | их отрицания |
```kotlin
val a = "Kotlin"
val b = StringBuilder("Kotlin").toString()
println(a == b) // true — содержимое совпадает
println(a === b) // false — разные объекты
```
`==` безопасен для null: `a == b` разворачивается в `a?.equals(b) ?: (b === null)`.
```kotlin
val s: String? = null
println(s == "text") // false, без NPE
println(s == null) // true
```
## Логические
```kotlin
val r1 = true && false // ленивый И
val r2 = true || false // ленивый ИЛИ
val r3 = !true // НЕ
val r4 = true and false // НЕ ленивый — оба операнда вычисляются
val r5 = true or false // НЕ ленивый
val r6 = true xor true // false
```
В обычном коде используют `&&` и `||`: они не вычисляют правую часть, если результат уже известен.
## Побитовые операции
Символов `&`, `|`, `^`, `<<`, `>>` для чисел в Kotlin **нет** - только инфиксные функции:
```kotlin
val a = 0b1100
val b = 0b1010
println(a and b) // 8 = 0b1000
println(a or b) // 14 = 0b1110
println(a xor b) // 6 = 0b0110
println(a.inv()) // -13 — побитовое НЕ
println(1 shl 4) // 16 — сдвиг влево
println(16 shr 2) // 4 — сдвиг вправо со знаком
println(-16 ushr 2) // беззнаковый сдвиг вправо
```
Доступны для `Int` и `Long`.
## Операторы диапазонов и коллекций
```kotlin
println(5 in 1..10) // true
println(5 !in 1..10) // false
println("a" in listOf("a","b")) // true — вызывает contains()
val list = listOf(1, 2)
println(list + 3) // [1, 2, 3] — plus для коллекций
```
Подробнее: [Циклы и диапазоны](Циклы%20и%20диапазоны.md)
## Приоритет
От высшего к низшему:
| Уровень | Операторы |
|---|---|
| постфиксные | `++`, `--`, `.`, `?.`, `!!` |
| префиксные | `-`, `+`, `++`, `--`, `!` |
| приведение | `as`, `as?` |
| умножение | `*`, `/`, `%` |
| сложение | `+`, `-` |
| диапазон | `..`, `..<` |
| инфиксные функции | `and`, `shl`, своя функция `infix` |
| проверка принадлежности | `in`, `!in` |
| проверка типа | `is`, `!is` |
| сравнение | `<`, `>`, `<=`, `>=` |
| равенство | `==`, `!=`, `===`, `!==` |
| конъюнкция | `&&` |
| дизъюнкция | `\|\|` |
| elvis | `?:` |
| присваивание | `=`, `+=`, … |
На практике: сомневаетесь - ставьте скобки.
## Всё является выражением
`if`, `when`, `try` возвращают значение:
```kotlin
val max = if (a > b) a else b
val name = when (code) {
200 -> "ok"
else -> "unknown"
}
val n = try { text.toInt() } catch (e: NumberFormatException) { 0 }
```
Подробнее: [Условные конструкции](Условные%20конструкции.md)
Циклы (`for`, `while`) - **не** выражения, они возвращают `Unit`.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `==` для объектов - ссылки | `==` - вызов `equals()` |
> | `.equals()` - содержимое | `===` - ссылки |
> | `a & b` побитовое И | `a and b` |
> | `a >>> b` | `a ushr b` |
> | `~a` | `a.inv()` |
> | тернарник `c ? a : b` | `if (c) a else b` |
> | операторы нельзя перегружать | можно, через `operator fun` |
> | `s.equals(t)` падает, если `s` null | `s == t` безопасен |
>
> Главная ловушка при переходе: в Java `==` на строках сравнивает ссылки и работает «случайно» благодаря [интернированию](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Строки.md). В Kotlin `==` всегда сравнивает содержимое - это и нужно в 99% случаев.
>
> Сравнить: [Операторы в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Операторы%20и%20выражения.md)
> [!warning] Подводные камни
> **`and` и `or` не ленивые.** Для `Boolean` это рабочий код, но правая часть вычислится всегда:
> ```kotlin
> fun risky(): Boolean { println("вызвана"); return true }
> val a = false && risky() // risky() не вызовется
> val b = false and risky() // "вызвана" напечатается
> ```
>
> **Деление на целочисленный ноль - исключение, на вещественный - нет:**
> ```kotlin
> // println(1 / 0) // ArithmeticException
> println(1.0 / 0) // Infinity
> println(0.0 / 0) // NaN
> println(Double.NaN == Double.NaN) // false! NaN не равен себе
> ```
>
> **Приоритет инфиксных функций ниже арифметики:**
> ```kotlin
> println(1 shl 2 + 3) // 32, а не 7: это 1 shl (2+3)
> println((1 shl 2) + 3) // 7
> ```
>
> **`++` на упакованном значении создаёт новый объект** - в горячем цикле с `Int?` это аллокации. Используйте не-nullable типы.
Далее: [Условные конструкции](Условные%20конструкции.md)
@@ -0,0 +1,129 @@
---
title: Пакеты и импорты
description: Объявление пакетов, импорты, псевдонимы и автоматически импортируемые пакеты
tags:
- kotlin
- syntax
---
## Пакет
Объявляется первой строкой файла:
```kotlin
package com.example.app
fun helper() { }
class Service
```
Полное имя функции - `com.example.app.helper`.
Отличие от Java: **структура каталогов не обязана совпадать с пакетом**, и имя файла не связано с содержимым. Компилятор не возражает. На практике соглашение соблюдают: иначе навигация по проекту ломается.
Файл без `package` попадает в корневой пакет (default package).
## Содержимое файла
В одном файле может быть сколько угодно объявлений верхнего уровня:
```kotlin
package com.example
const val VERSION = "1.0"
fun parse(text: String): Int = text.toInt()
class User(val name: String)
class Admin(val name: String)
private fun internalHelper() { } // видна только в этом файле
```
Никаких `public class` на файл - ограничения «один публичный класс на файл» в Kotlin нет.
## Импорты
```kotlin
import com.example.app.Service // конкретный класс
import com.example.app.helper // функция верхнего уровня
import com.example.app.* // всё из пакета
import com.example.app.User.Companion.create // член companion object
```
Импортировать можно не только типы, но и функции, свойства, enum-константы - всё, что объявлено на верхнем уровне или как статический член.
```kotlin
import kotlin.math.PI
import kotlin.math.sqrt
fun main() {
println(sqrt(PI)) // 1.7724538509055159
}
```
## Псевдонимы - as
Когда имена конфликтуют или слишком длинные:
```kotlin
import com.example.data.User as DataUser
import com.example.api.User as ApiUser
fun convert(u: DataUser): ApiUser = ApiUser(u.name)
```
Псевдоним действует только в своём файле. Если нужно имя на весь модуль - это [typealias](../Базовые%20типы%20ООП/Typealias.md).
## Импортируются автоматически
Эти пакеты доступны без `import`:
| Пакет | Что содержит |
|---|---|
| `kotlin.*` | `Int`, `String`, `Any`, базовые функции |
| `kotlin.annotation.*` | аннотации |
| `kotlin.collections.*` | `List`, `Map`, `listOf`, операции |
| `kotlin.comparisons.*` | `compareBy`, `maxOf` |
| `kotlin.io.*` | `println`, `readln` |
| `kotlin.ranges.*` | диапазоны, `step`, `downTo` |
| `kotlin.sequences.*` | `Sequence`, `sequenceOf` |
| `kotlin.text.*` | операции со строками |
| `java.lang.*` | только на JVM |
| `kotlin.jvm.*` | только на JVM |
Поэтому `println`, `listOf`, `Int` работают из коробки. А `kotlin.math.sqrt` - нет, его импортируют явно.
## Видимость на верхнем уровне
```kotlin
public fun api() { } // видна везде (public — по умолчанию)
internal fun moduleOnly() { } // видна в своём модуле
private fun fileOnly() { } // видна только в этом файле
```
`protected` на верхнем уровне запрещён - нет класса, от которого наследоваться.
Подробнее: [Модификаторы видимости](../../ООП/Модификаторы%20видимости.md)
## Имя класса-обёртки
Функции верхнего уровня попадают в класс, названный по файлу: `Utils.kt` → класс `UtilsKt`. Имя можно задать:
```kotlin
@file:JvmName("Utils")
package com.example
fun helper() { } // из Java: Utils.helper()
```
Аннотации уровня файла ставятся **до** `package`. Подробнее: [Kotlin из Java](../../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | каталоги обязаны совпадать с пакетом | не обязаны (но так принято) |
> | имя файла = имя публичного класса | имя файла произвольно |
> | один публичный класс на файл | сколько угодно объявлений |
> | `import static` для статики | обычный `import` для всего |
> | псевдонимов нет | `import ... as Name` |
> | автоимпорт только `java.lang` | десять пакетов `kotlin.*` |
> | `package-private` по умолчанию | `public` по умолчанию |
>
> Последняя строка важна: в Java класс без модификатора виден только своему пакету, в Kotlin - всем. Для сокрытия внутренностей библиотеки используют `internal`.
> [!warning] Подводные камни
> **Нет аналога package-private.** `internal` ограничивает *модулем* (единицей компиляции Gradle), а не пакетом. Разделить код на пакеты и спрятать их друг от друга внутри одного модуля нельзя - только разносить по модулям.
>
> **`import *` не затягивает вложенные пакеты.** `import com.example.*` не даёт доступа к `com.example.data.User`.
>
> **Конфликт импортов - ошибка компиляции, а не предупреждение.** Два `User` из разных пакетов требуют `as` или полного имени.
>
> **Функции с одинаковым именем в разных файлах одного пакета конфликтуют.** Два `private fun helper()` в разных файлах - нормально, а два `fun helper()` с одной сигнатурой - ошибка: оба попадают в один пакет.
>
> **`@file:JvmName` не действует, если имя файла совпадает с именем класса в нём.** Коллизия имён вызовет ошибку компиляции.
Дополнительно: [Структура проекта](../../Инструментарий/Структура%20проекта.md)
Далее: [Null-безопасность](../Null-безопасность/Null-безопасность.md)
@@ -0,0 +1,154 @@
---
title: Переменные
description: val и var, вывод типов, const val, lateinit и области видимости
tags:
- kotlin
- syntax
---
Два ключевых слова:
- `val` - **value**, значение присваивается один раз (read-only)
- `var` - **variable**, значение можно менять
```kotlin
val name = "Kotlin"
var count = 0
count++ // ok
// name = "Java" // ошибка компиляции: val cannot be reassigned
```
Правило стиля: по умолчанию `val`, `var` - только когда переменная действительно меняется. Это не формальность: с `val` работают [умные приведения](../Null-безопасность/Умные%20приведения.md), а код становится предсказуемее.
## Объявление
```kotlin
val a: Int = 1 // тип указан явно
val b = 1 // тип выведен: Int
val c: Int // без инициализации — тип обязателен
c = 2 // присвоить можно позже, но только один раз
```
Тип ставится **после** имени через двоеточие.
## Вывод типов
Компилятор выводит тип из значения:
```kotlin
val n = 42 // Int
val l = 42L // Long
val s = "text" // String
val list = listOf(1, 2) // List<Int>
val mixed = listOf(1, "a") // List<Any>
```
Вывод работает только там, где есть что выводить. У свойств класса и возвращаемых значений публичных функций тип лучше писать явно - это часть контракта.
## val - не значит immutable
`val` запрещает переприсваивание ссылки, но не меняет сам объект:
```kotlin
val list = mutableListOf(1, 2)
list.add(3) // ok — меняем объект
println(list) // [1, 2, 3]
// list = mutableListOf() // ошибка — меняем ссылку
```
Неизменяемость данных - это выбор типа (`List` вместо `MutableList`), а не `val`. Подробнее: [Изменяемость коллекций](../../Коллекции/Изменяемость.md)
## val с геттером
`val` может вычисляться при каждом обращении:
```kotlin
val now: Long
get() = System.currentTimeMillis()
```
Это уже свойство, а не поле. Подробнее: [Свойства](../../ООП/Свойства.md)
## Константы - const val
```kotlin
const val MAX = 100 // верхний уровень файла
const val GREETING = "Hello"
object Config {
const val TIMEOUT = 30 // или внутри object
}
```
Требования к `const val`:
- только `String` или примитивный тип
- значение известно на этапе компиляции
- только на верхнем уровне, в `object` или в `companion object`
- нельзя у локальной переменной и нельзя с кастомным геттером
Значение подставляется в место использования на этапе компиляции (инлайнится), обращения к полю в рантайме нет.
```kotlin
val runtime = computeLimit() // вычисляется при запуске
const val compiled = 100 // подставлено компилятором
```
## lateinit
Для `var`, который точно будет проинициализирован, но не в момент объявления (DI, настройка в тестах):
```kotlin
class Service {
lateinit var client: HttpClient
fun setup() {
client = HttpClient()
}
fun use() {
if (::client.isInitialized) client.send()
}
}
```
Ограничения `lateinit`:
- только `var`
- тип не nullable
- не примитивный тип (`Int`, `Double`, … запрещены)
- нельзя у локальных переменных
- нельзя с кастомным геттером/сеттером
Обращение до инициализации - `UninitializedPropertyAccessException`.
Альтернатива для ленивых вычислений - `by lazy`, он работает с `val`:
```kotlin
val config: Config by lazy { loadConfig() } // вычислится при первом обращении
```
Подробнее: [Свойства](../../ООП/Свойства.md)
## Области видимости
```kotlin
const val TOP = 1 // весь файл и, если public, другие файлы
fun demo() {
val local = 2 // только тело функции
if (local > 1) {
val inner = 3 // только тело if
}
// println(inner) // ошибка: не видна
}
```
Вложенная переменная может скрыть внешнюю - компилятор выдаст предупреждение `name shadowed`.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `final int x = 1;` | `val x = 1` |
> | `int x = 1;` | `var x = 1` |
> | `var x = 1;` (только локально, с Java 10) | вывод типов везде, включая свойства класса |
> | `static final int MAX = 100;` | `const val MAX = 100` |
> | мутабельность по умолчанию | иммутабельность предпочтительна |
> | `int x;` - значение по умолчанию 0 | неинициализированную переменную читать нельзя |
>
> Важное следствие: у свойств класса в Kotlin **нет значений по умолчанию**. В Java поле `int count;` автоматически 0, в Kotlin `var count: Int` без инициализатора не скомпилируется.
>
> Сравнить: [Переменные в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Переменные.md)
> [!warning] Подводные камни
> **`val` не делает объект неизменяемым** - см. пример с `MutableList` выше. Частая ошибка на собеседованиях.
>
> **`lateinit` прячет ошибку до рантайма.** Если забыли вызвать `setup()`, падение случится в момент использования, далеко от причины. Там, где можно, лучше конструктор или `by lazy`.
>
> **Вывод типов может дать не тот тип, что ожидали:**
> ```kotlin
> val x = 1 / 2 // 0, а не 0.5 — оба операнда Int
> val y = 1.0 / 2 // 0.5
> ```
>
> **`const val` зашивается в вызывающий код.** Если константа лежит в библиотеке и её значение изменилось, зависимый модуль нужно перекомпилировать - иначе он продолжит использовать старое значение.
Дополнительно: [Свойства](../../ООП/Свойства.md)
Далее: [Операторы и выражения](Операторы%20и%20выражения.md)
@@ -0,0 +1,191 @@
---
title: Строковые шаблоны и вывод
description: Интерполяция строк, raw strings, trimIndent, println и форматированный вывод
tags:
- kotlin
- syntax
---
## Шаблоны строк
Вместо конкатенации - подстановка значений прямо в строку:
```kotlin
val name = "Kotlin"
val version = 2.4
println("Привет, $name!") // Привет, Kotlin!
println("Версия: $version") // Версия: 2.4
println("Длина имени: ${name.length}") // Длина имени: 6
println("Сумма: ${2 + 3}") // Сумма: 5
```
Правила:
- `$имя` - для простой переменной
- `${выражение}` - для всего остального: вызовов, свойств, арифметики
- вызывается `toString()` подставляемого значения
- `null` подставляется как текст `null`
```kotlin
val x: String? = null
println("значение: $x") // значение: null
```
Фигурные скобки можно ставить всегда - многие так и делают для единообразия.
## Экранирование
```kotlin
println("цена: \$100") // цена: $100
println("перенос\nстроки")
println("табуляция\tтут")
println("кавычка \" внутри")
println("слеш \\ внутри")
println("символ \u00A9") // ©
```
Поддерживаются `\t`, `\b`, `\n`, `\r`, `\'`, `\"`, `\\`, `\$`, `\uXXXX`.
## Raw strings
Три кавычки - никакого экранирования, переносы сохраняются как есть:
```kotlin
val path = """C:\Users\name\file.txt""" // слеши не экранируются
val json = """
{
"name": "Kotlin",
"stable": true
}
"""
```
Удобно для regex, JSON, SQL, HTML.
Шаблоны `$` в raw strings **работают**:
```kotlin
val user = "admin"
val query = """SELECT * FROM users WHERE name = '$user'"""
```
Чтобы вставить настоящий `$`, используют подстановку символа:
```kotlin
val price = """цена: ${'$'}100""" // цена: $100
```
## trimIndent и trimMargin
Raw string сохраняет отступы исходного кода - их убирают явно:
```kotlin
val text = """
Первая строка
Вторая строка
""".trimIndent()
// "Первая строка\nВторая строка" — общий отступ убран,
// пустые первая и последняя строки удалены
val bar = """
|Первая
|Вторая
""".trimMargin() // по умолчанию маркер |
```
`trimIndent()` вычисляет минимальный общий отступ непустых строк и срезает его у всех.
## println и print
```kotlin
print("без переноса")
println("с переносом")
println() // пустая строка
println(listOf(1, 2, 3)) // [1, 2, 3]
```
Это функции стандартной библиотеки, доступны без импорта. Под капотом - `System.out`.
## Форматированный вывод
Kotlin использует механизм форматирования Java:
```kotlin
import java.util.Locale
fun main() {
println("%05d".format(42)) // 00042
println("%-10s|".format("left")) // left |
println(String.format("%s = %d", "x", 5)) // x = 5
// вещественные числа и разряды зависят от локали — её лучше задавать явно
println("%.2f".format(Locale.US, 3.14159)) // 3.14
println("%,d".format(Locale.US, 1234567)) // 1,234,567
}
```
> [!warning] Локаль по умолчанию
> `format` без явной локали берёт системную. На русской локали получится `3,14` и `1 234 567` - с запятой и неразрывными пробелами. Если строка идёт в JSON, SQL или лог, это ломает парсинг. Для машинных форматов всегда передавайте `Locale.US` (или `Locale.ROOT`).
Основные спецификаторы:
| Спецификатор | Значение |
|---|---|
| `%d` | целое число |
| `%f` | вещественное |
| `%.Nf` | вещественное с N знаками |
| `%s` | строка (через `toString()`) |
| `%b` | boolean |
| `%x` | шестнадцатеричное |
| `%e` | экспоненциальная запись |
| `%n` | перенос строки платформы |
| `%%` | символ процента |
Флаги ширины: `%10d` - по правому краю, `%-10d` - по левому, `%010d` - с ведущими нулями.
## Чтение ввода
```kotlin
val line = readLine() // String? — null при конце потока
val text = readln() // String — бросает исключение при EOF
val n = readln().toInt() // падает на некорректном вводе
val safe = readln().toIntOrNull() ?: 0 // безопасный разбор
```
`readln()` появился в Kotlin 1.6 и удобнее, когда ввод гарантирован.
## Конкатенация
Работает, но в шаблонах почти не нужна:
```kotlin
println("Hello" + " " + "world") // Hello world
println("Hello" + 5 + 10) // Hello510 — слева направо
println("Hello" + (5 + 10)) // Hello15
```
Важно: складывать можно только когда **слева** строка. У `Int` нет `plus(String)`, поэтому в Kotlin не скомпилируется то, что в Java работает:
```kotlin
// println(5 + 10 + "Hello") // ошибка: Int.plus ожидает Int, а не String
println("${5 + 10}Hello") // 15Hello — через шаблон
```
Для сборки строки в цикле - `StringBuilder` или `buildString`:
```kotlin
val result = buildString {
repeat(3) { append("ха") }
}
println(result) // хахаха
```
Подробнее: [Строки](../Работа%20с%20данными/Строки.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `System.out.println(x)` | `println(x)` |
> | `"a" + x + "b"` | `"a${x}b"` |
> | `String.format("%d", n)` | `"%d".format(n)` |
> | text blocks `"""` (Java 15+) | raw strings `"""` + шаблоны |
> | `.stripIndent()` | `.trimIndent()` |
> | `new Scanner(System.in)` | `readln()` |
>
> Шаблоны - не просто синтаксический сахар: компилятор собирает их через `StringBuilder` или `invokedynamic`, то есть без промежуточных строк. Писать `"a" + b + "c"` ради производительности не нужно.
>
> Сравнить: [Форматированный вывод в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Форматированный%20вывод.md)
> [!warning] Подводные камни
> **`$` перед словом всегда считается шаблоном.** Строка `"цена: $100"` скомпилируется (цифра не может начинать имя), а `"$name_suffix"` - нет: Kotlin ищет переменную `name_suffix`. Нужны скобки: `"${name}_suffix"`.
>
> **`trimIndent()` чувствителен к смешению табов и пробелов** - отступ считается по символам, таб и четыре пробела для него разные вещи.
>
> **`$` в regex внутри raw string.** `"""^\d+$"""` работает, потому что после `$` идёт конец строки. Но `"""$\d"""` компилятор попытается разобрать как шаблон.
>
> **Шаблон вызывает `toString()` у массива, а не его содержимое:**
> ```kotlin
> val arr = arrayOf(1, 2, 3)
> println("$arr") // [Ljava.lang.Object;@1b6d3586
> println(arr.contentToString()) // [1, 2, 3]
> println("${arr.joinToString()}") // 1, 2, 3
> ```
>
> **`readLine()` возвращает `String?`.** Вызов `readLine().toInt()` не скомпилируется - нужен `readln()` или обработка null.
Дополнительно: [Строки](../Работа%20с%20данными/Строки.md)
Далее: [Пакеты и импорты](Пакеты%20и%20импорты.md)
@@ -0,0 +1,154 @@
---
title: Типы данных
description: Система типов Kotlin, числа, boolean, Char и отсутствие примитивов на уровне языка
tags:
- kotlin
- types
---
В Kotlin: статическая типизация, сильная, с выводом типов.
Главное отличие от Java: **на уровне языка примитивов нет**. Всё - объект, у всего есть методы.
```kotlin
val x: Int = 42
println(x.toString()) // у числа есть методы
println(42.coerceIn(0, 10)) // 10
```
Но в байт-коде `Int` компилируется в `int`, если это возможно - производительность не теряется. Подробнее в [Kotlin в байт-коде](../../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md).
## Иерархия типов
```mermaid
graph TD
Any["Any — корень всех non-null типов"]
Any --> Number
Any --> Char
Any --> Boolean
Any --> String
Any --> Other["классы, интерфейсы, ..."]
Number --> Byte
Number --> Short
Number --> Int
Number --> Long
Number --> Float
Number --> Double
Nothing["Nothing — наследник всех типов"]
```
`Any`, `Unit` и `Nothing` - особые типы, им посвящена отдельная заметка: [Any, Unit, Nothing](../Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md)
## Числовые типы
| Тип | Размер, бит | Диапазон |
|---|---|---|
| `Byte` | 8 | −128 … 127 |
| `Short` | 16 | −32 768 … 32 767 |
| `Int` | 32 | ≈ ±2 млрд |
| `Long` | 64 | ≈ ±9,2 · 10¹⁸ |
| `Float` | 32 | ≈ 6-7 значащих цифр |
| `Double` | 64 | ≈ 15-16 значащих цифр |
Все наследуются от `Number`, но **не** друг от друга: `Int` не является подтипом `Long`.
### Литералы
```kotlin
val i = 42 // Int по умолчанию
val l = 42L // Long — суффикс L
val d = 42.0 // Double по умолчанию
val f = 42.0f // Float — суффикс f
val hex = 0xFF // 255
val bin = 0b1010 // 10
val big = 1_000_000 // подчёркивания для читаемости
```
Восьмеричных литералов (`017` в Java) в Kotlin **нет**.
## Беззнаковые типы
`UByte`, `UShort`, `UInt`, `ULong` - то, чего в Java нет совсем.
```kotlin
val u: UInt = 4_000_000_000u // суффикс u
println(UInt.MAX_VALUE) // 4294967295
```
Нужны редко: работа с бинарными форматами, протоколами, хешами.
## Boolean
```kotlin
val yes: Boolean = true
val both = yes && other() // ленивые операторы, как в Java
val eager = yes and other() // не ленивый: other() вызовется всегда
```
## Char
```kotlin
val c: Char = 'A'
println(c.code) // 65 — числовой код
println(c + 1) // B — Char + Int даёт Char
println(c.isLetter()) // true
```
`Char` **не является числом**: `val n: Int = 'A'` не скомпилируется, нужен `'A'.code`.
Подробнее: [Char](../Работа%20с%20данными/Char.md)
## Преобразования
Неявных расширяющих преобразований нет вообще - только явные вызовы:
```kotlin
val i = 42
val l: Long = i.toLong() // обязательно явно
val b = i.toByte()
val d = i.toDouble()
```
Доступны `toByte()`, `toShort()`, `toInt()`, `toLong()`, `toFloat()`, `toDouble()`, `toChar()`.
## Строки
`String` - неизменяемый тип, элементы доступны по индексу:
```kotlin
val s = "Kotlin"
println(s[0]) // K
println(s.length) // 6
```
Подробнее: [Строки](../Работа%20с%20данными/Строки.md)
## Nullable-типы
Любой тип имеет nullable-вариант с `?`:
```kotlin
// val a: Int = null // ошибка компиляции
val b: Int? = null // ok
```
Это ядро языка, ему посвящён отдельный раздел: [Null-безопасность](../Null-безопасность/Null-безопасность.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `int` и `Integer` - разные типы | только `Int`, компилятор сам выбирает представление |
> | автоупаковка неявная | упаковка скрыта, в коде не видна |
> | `long x = intValue;` работает | нужен `intValue.toLong()` |
> | `char` - числовой тип | `Char` - отдельный тип, не число |
> | нет беззнаковых типов | `UInt`, `ULong`, … |
> | `017` - восьмеричный литерал | восьмеричных литералов нет |
>
> Сравнить: [Типы данных в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Типы%20данных.md)
> [!warning] Подводные камни
> **Переполнение не проверяется**, как и в Java:
> ```kotlin
> println(Int.MAX_VALUE + 1) // -2147483648
> ```
>
> **Равенство упакованных значений.** Если тип nullable, значение упаковано, и `===` сравнивает ссылки:
> ```kotlin
> val a: Int = 127
> val b: Int? = a
> val c: Int? = a
> println(b == c) // true — сравнение значений
> println(b === c) // true — кеш Integer для -128..127
>
> val x: Int? = 1000
> val y: Int? = 1000
> println(x == y) // true
> println(x === y) // false — за пределами кеша это разные объекты
> ```
> Вывод: для чисел всегда `==`, никогда `===`.
>
> **`Float` и `Double` не точны**, это стандарт IEEE 754:
> ```kotlin
> println(0.1 + 0.2) // 0.30000000000000004
> ```
> Для денег - `java.math.BigDecimal`.
Дополнительно: [Числа и переполнение](../Работа%20с%20данными/Числа%20и%20переполнение.md)
Далее: [Переменные](Переменные.md)
@@ -0,0 +1,175 @@
---
title: Условные конструкции
description: if и when как выражения, исчерпывающий when, отсутствие тернарного оператора и switch
tags:
- kotlin
- syntax
---
В Kotlin две конструкции ветвления: `if` и `when`. Обе могут работать и как инструкция, и как **выражение**, возвращающее значение.
## if как инструкция
```kotlin
if (x > 0) {
println("положительное")
} else if (x < 0) {
println("отрицательное")
} else {
println("ноль")
}
```
Условие обязано быть `Boolean`. Число или null туда подставить нельзя - компилятор не пропустит.
## if как выражение
```kotlin
val max = if (a > b) a else b
val label = if (x > 0) {
log("positive")
"плюс" // последнее выражение блока — результат
} else {
"минус"
}
```
Правила:
- в режиме выражения ветка `else` **обязательна** - иначе значение не определено
- результат блока - его последнее выражение
- тип результата - общий супертип всех ветвей
```kotlin
val r = if (flag) 1 else "text" // тип Any — общий супертип
```
Тернарного оператора `?:` в смысле Java в Kotlin нет: его роль играет `if`. Оператор `?:` в Kotlin - это **elvis**, у него другая задача ([Операторы null-безопасности](../Null-безопасность/Операторы%20null-безопасности.md)).
## when - замена switch
Две формы. С аргументом:
```kotlin
when (code) {
200 -> println("ok")
404 -> println("not found")
500, 502, 503 -> println("ошибка сервера") // несколько значений в одной ветке
else -> println("неизвестно")
}
```
Без аргумента - вместо цепочки `if/else if`:
```kotlin
when {
x < 0 -> println("отрицательное")
x == 0 -> println("ноль")
else -> println("положительное")
}
```
Ветки проверяются **сверху вниз**, выполняется первая подошедшая. Никакого `break` не нужно - провала в следующую ветку (fall-through) в Kotlin не бывает.
## when как выражение
```kotlin
val name = when (code) {
200 -> "ok"
404 -> "not found"
else -> "unknown"
}
```
В режиме выражения `when` обязан быть **исчерпывающим**: либо есть `else`, либо компилятор видит, что перечислены все варианты.
## Условия в ветках when
Не только константы:
```kotlin
when (x) {
in 1..9 -> "одна цифра" // диапазон
in listOf(10, 20, 30) -> "круглое" // коллекция
!in 0..100 -> "вне диапазона"
else -> "прочее"
}
when (value) {
is String -> value.length // проверка типа + умное приведение
is Int -> value + 1
is List<*> -> value.size
else -> 0
}
when (x) {
computeLimit() -> "предел" // произвольное выражение
else -> "нет"
}
```
После `is` переменная автоматически приводится к проверенному типу - см. [Умные приведения](../Null-безопасность/Умные%20приведения.md).
## Исчерпывающий when без else
С `enum` и `sealed` компилятор сам проверяет полноту:
```kotlin
enum class State { LOADING, READY, ERROR }
fun describe(s: State) = when (s) {
State.LOADING -> "загрузка"
State.READY -> "готово"
State.ERROR -> "ошибка"
} // else не нужен — все варианты перечислены
```
Добавили `State.EMPTY` - код **перестанет компилироваться** во всех таких `when`. Это главная причина предпочитать `sealed` и `enum` строкам и числам: компилятор показывает каждое место, которое нужно обновить.
Подробнее: [Enum class](../Базовые%20типы%20ООП/Enum%20class.md), [Sealed class и interface](../Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
## Переменная в when
Аргумент можно объявить прямо в заголовке - он будет виден только внутри:
```kotlin
when (val response = request()) {
is Success -> response.data
is Failure -> response.error
}
```
Удобно, когда `request()` дорогой: вызов происходит один раз.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `switch` только по числам, строкам, enum | `when` по любым значениям и условиям |
> | нужен `break`, иначе fall-through | провала нет, `break` не нужен |
> | `c ? a : b` | `if (c) a else b` |
> | `if` - только инструкция | `if` - ещё и выражение |
> | `switch` не проверяет полноту (до pattern matching в Java 21) | `when` по enum/sealed проверяется компилятором |
>
> `when` ближе всего к `switch`-выражению из Java 14+ и pattern matching из Java 21, но появился в Kotlin задолго до них и покрывает больше случаев.
>
> Сравнить: [Условные конструкции в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Условные%20конструкции%20и%20операторы.md)
> [!warning] Подводные камни
> **Полнота проверяется и у `when`-инструкции - но только для `enum` и `sealed`.** С Kotlin 2.0 пропуск ветки - ошибка компиляции даже когда результат не используется:
> ```kotlin
> fun handle(s: State) {
> when (s) {
> State.LOADING -> show()
> State.READY -> done()
> } // error: 'when' expression must be exhaustive.
> } // Add the 'ERROR' branch or an 'else' branch.
> ```
> А вот для `Int`, `String` и прочих открытых типов полноту проверить невозможно - там `when`-инструкция без `else` компилируется молча и просто ничего не делает:
> ```kotlin
> fun handle(code: Int) {
> when (code) {
> 200 -> show()
> 404 -> fail()
> } // для code = 500 тихо ничего не произойдёт
> }
> ```
> Это ещё одна причина моделировать состояния через `enum` и `sealed`, а не через числа и строки: только тогда компилятор на вашей стороне.
>
> **Порядок ветвей важен.** Первая подошедшая выигрывает, более общая ветка выше «съест» частные:
> ```kotlin
> when (x) {
> in 0..100 -> "диапазон" // сработает и для 42
> 42 -> "ответ" // недостижимо
> }
> ```
>
> **`if` без `else` не выражение:**
> ```kotlin
> // val x = if (flag) 1 // ошибка компиляции
> val x = if (flag) 1 else 0 // ok
> ```
>
> **`is List<*>` не проверяет тип элементов** - дженерики стираются в рантайме. `is List<String>` написать нельзя. Подробнее: [Reified и inline](../../Дженерики/Reified%20и%20inline.md)
Далее: [Циклы и диапазоны](Циклы%20и%20диапазоны.md)
@@ -0,0 +1,159 @@
---
title: Циклы и диапазоны
description: for-in, диапазоны и прогрессии, while, метки и отсутствие классического for
tags:
- kotlin
- syntax
---
В Kotlin один цикл обхода - `for (x in source)`. Классического `for (int i = 0; i < n; i++)` нет: его роль выполняют диапазоны.
## Диапазоны
```kotlin
for (i in 1..5) print(i) // 12345 — включая 5
for (i in 1..<5) print(i) // 1234 — исключая 5
for (i in 5 downTo 1) print(i) // 54321
for (i in 1..10 step 3) print(i) // 1 4 7 10
for (c in 'a'..'e') print(c) // abcde
```
| Конструкция | Смысл |
|---|---|
| `a..b` | от `a` до `b` включительно |
| `a..<b` | от `a` до `b`, не включая `b` |
| `a until b` | то же, что `a..<b` (старая форма) |
| `a downTo b` | по убыванию, включительно |
| `step n` | с шагом `n` |
Шаг всегда положительный, даже в `downTo`: направление задаёт `downTo`, а не знак шага.
Диапазон - это объект, его можно хранить и проверять:
```kotlin
val valid = 1..100
println(42 in valid) // true
println(valid.first) // 1
println(valid.last) // 100
println((1..10 step 3).last) // 10
```
## Обход по индексам
```kotlin
val list = listOf("a", "b", "c")
for (i in list.indices) print(i) // 012
for (i in 0..<list.size) print(i) // 012 — то же самое
for (item in list) print(item) // abc — предпочтительно
for ((i, item) in list.withIndex()) { // 0:a 1:b 2:c
print("$i:$item ")
}
```
`withIndex()` возвращает пары, которые тут же разбираются деструктуризацией ([Деструктуризация](../Работа%20с%20данными/Деструктуризация.md)).
## Обход коллекций и Map
```kotlin
val map = mapOf("a" to 1, "b" to 2)
for ((key, value) in map) {
println("$key=$value")
}
```
`for` работает с любым типом, у которого есть функция `iterator()` - включая свои классы.
## while и do-while
```kotlin
var i = 0
while (i < 3) {
print(i) // 012
i++
}
do {
val line = readLine()
} while (line != null) // тело выполнится минимум один раз
```
Переменная, объявленная в `do`, видна в условии `while` - такого в Java нет.
## break и continue
```kotlin
for (i in 1..10) {
if (i == 3) continue // пропустить итерацию
if (i == 5) break // выйти из цикла
print(i) // 124
}
```
## Метки
Метка - идентификатор с `@`. Нужна, чтобы выйти из внешнего цикла:
```kotlin
outer@ for (i in 1..3) {
for (j in 1..3) {
if (j == 2) continue@outer // к следующей i
if (i == 3) break@outer // выход из обоих циклов
print("$i$j ") // 11 21
}
}
```
## Циклы - не выражения
`for` и `while` возвращают `Unit`, присвоить их результат нельзя. Если нужен результат обхода - это операции над коллекциями:
```kotlin
val doubled = (1..5).map { it * 2 } // [2, 4, 6, 8, 10]
val evens = (1..10).filter { it % 2 == 0 } // [2, 4, 6, 8, 10]
val sum = (1..100).sum() // 5050
```
В идиоматичном Kotlin явные циклы встречаются редко. Подробнее: [Операции над коллекциями](../../Коллекции/Операции.md)
## repeat
Когда индекс не нужен:
```kotlin
repeat(3) { println("повтор") }
repeat(3) { i -> println("итерация $i") } // индекс доступен, с нуля
```
Это не оператор, а [inline-функция](../../Функциональный%20Kotlin/Inline-функции.md) стандартной библиотеки.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `for (int i=0; i<n; i++)` | `for (i in 0..<n)` |
> | `for (T x : collection)` | `for (x in collection)` |
> | `label: for (...)` | `outer@ for (...)` |
> | `break label;` | `break@outer` |
> | три части в заголовке `for` | только `in` |
> | нет диапазонов | `1..10`, `step`, `downTo` |
>
> Что потеряли: заголовок `for` с несколькими переменными и произвольным условием. Если нужна такая логика - пишется `while`.
>
> Сравнить: [Циклы в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Циклы.md)
> [!warning] Подводные камни
> **`1..5` включает 5.** Самая частая ошибка при переходе с Java: индексы массива - это `0..<size`, а не `0..size`:
> ```kotlin
> val list = listOf("a", "b", "c")
> // for (i in 0..list.size) println(list[i]) // IndexOutOfBounds на i=3
> for (i in 0..<list.size) println(list[i]) // верно
> for (i in list.indices) println(list[i]) // ещё лучше
> ```
>
> **`downTo` со `step` требует положительного шага:**
> ```kotlin
> for (i in 5 downTo 1 step 2) print(i) // 531
> // for (i in 5 downTo 1 step -2) // IllegalArgumentException в рантайме
> ```
>
> **Пустой диапазон не ошибка, просто ноль итераций:**
> ```kotlin
> for (i in 5..1) print(i) // ничего не напечатает
> ```
> Молчаливый баг, если границы перепутаны. `(5..1).isEmpty()` - `true`.
>
> **Коллекцию нельзя изменять во время обхода:**
> ```kotlin
> val list = mutableListOf(1, 2, 3)
> // for (x in list) if (x == 2) list.remove(x) // ConcurrentModificationException
> list.removeAll { it == 2 } // правильный способ
> ```
>
> **Диапазон `Long` и `Char` есть, а `Double` - нет.** `1.0..2.0` создаст `ClosedFloatingPointRange`: проверять `in` можно, но обойти циклом - нельзя (нет `iterator()`).
Дополнительно: [Операции над коллекциями](../../Коллекции/Операции.md)
Далее: [Строковые шаблоны и вывод](Строковые%20шаблоны%20и%20вывод.md)