diff --git a/Kotlin/Kotlin.md b/Kotlin/Kotlin.md new file mode 100644 index 0000000..055655e --- /dev/null +++ b/Kotlin/Kotlin.md @@ -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) diff --git a/Kotlin/Синтаксис и типы/Null-безопасность/Null-безопасность.md b/Kotlin/Синтаксис и типы/Null-безопасность/Null-безопасность.md new file mode 100644 index 0000000..2d54e80 --- /dev/null +++ b/Kotlin/Синтаксис и типы/Null-безопасность/Null-безопасность.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 = listOf("x") // список есть, элементы не null +val b: List = listOf(null) // список есть, элементы могут быть null +val c: List? = null // списка может не быть +val d: List? = null // и то, и другое +``` +Для отсеивания null есть готовые функции: +```kotlin +val items: List = listOf("a", null, "b") +println(items.filterNotNull()) // [a, b] +``` + +## Nullable и дженерики +Параметр типа по умолчанию допускает null, его верхняя граница - `Any?`: +```kotlin +fun process(item: T) { } // T может быть nullable +fun 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?` на уровне типов, без аллокации | +> | `@Nullable`/`@NotNull` - подсказки для IDE | часть системы типов, проверяется компилятором | +> | `Objects.requireNonNull(x)` | `requireNotNull(x)` или просто non-null тип | +> +> `Optional` в 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) diff --git a/Kotlin/Синтаксис и типы/Null-безопасность/Операторы null-безопасности.md b/Kotlin/Синтаксис и типы/Null-безопасность/Операторы null-безопасности.md new file mode 100644 index 0000000..091e0fb --- /dev/null +++ b/Kotlin/Синтаксис и типы/Null-безопасность/Операторы null-безопасности.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? = 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? = 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) diff --git a/Kotlin/Синтаксис и типы/Null-безопасность/Платформенные типы.md b/Kotlin/Синтаксис и типы/Null-безопасность/Платформенные типы.md new file mode 100644 index 0000000..979505f --- /dev/null +++ b/Kotlin/Синтаксис и типы/Null-безопасность/Платформенные типы.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 getItems() { return null; } +``` +Тип результата - `(Mutable)List!`. Неизвестно ни то, может ли быть null сам список, ни его элементы: +```kotlin +val items: List? = repo.items // самое честное предположение +items?.filterNotNull()?.forEach(::println) +``` + +Отдельная проблема - изменяемость. `java.util.List` отображается в `(Mutable)List`, то есть Kotlin позволит передать его туда, где ожидается `MutableList`, и Java-код может изменить «неизменяемый» список: +```kotlin +val list: List = 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 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) diff --git a/Kotlin/Синтаксис и типы/Null-безопасность/Умные приведения.md b/Kotlin/Синтаксис и типы/Null-безопасность/Умные приведения.md new file mode 100644 index 0000000..aff5a8b --- /dev/null +++ b/Kotlin/Синтаксис и типы/Null-безопасность/Умные приведения.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) // ошибка: 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) diff --git a/Kotlin/Синтаксис и типы/Базовый синтаксис/Hello World!.md b/Kotlin/Синтаксис и типы/Базовый синтаксис/Hello World!.md new file mode 100644 index 0000000..8c82e26 --- /dev/null +++ b/Kotlin/Синтаксис и типы/Базовый синтаксис/Hello World!.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) { } // если нужны аргументы командной строки +``` +Функция `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) diff --git a/Kotlin/Синтаксис и типы/Базовый синтаксис/Операторы и выражения.md b/Kotlin/Синтаксис и типы/Базовый синтаксис/Операторы и выражения.md new file mode 100644 index 0000000..24a1267 --- /dev/null +++ b/Kotlin/Синтаксис и типы/Базовый синтаксис/Операторы и выражения.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) diff --git a/Kotlin/Синтаксис и типы/Базовый синтаксис/Пакеты и импорты.md b/Kotlin/Синтаксис и типы/Базовый синтаксис/Пакеты и импорты.md new file mode 100644 index 0000000..54d87da --- /dev/null +++ b/Kotlin/Синтаксис и типы/Базовый синтаксис/Пакеты и импорты.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) diff --git a/Kotlin/Синтаксис и типы/Базовый синтаксис/Переменные.md b/Kotlin/Синтаксис и типы/Базовый синтаксис/Переменные.md new file mode 100644 index 0000000..b559ff6 --- /dev/null +++ b/Kotlin/Синтаксис и типы/Базовый синтаксис/Переменные.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 +val mixed = listOf(1, "a") // List +``` +Вывод работает только там, где есть что выводить. У свойств класса и возвращаемых значений публичных функций тип лучше писать явно - это часть контракта. + +## 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) diff --git a/Kotlin/Синтаксис и типы/Базовый синтаксис/Строковые шаблоны и вывод.md b/Kotlin/Синтаксис и типы/Базовый синтаксис/Строковые шаблоны и вывод.md new file mode 100644 index 0000000..9af13cb --- /dev/null +++ b/Kotlin/Синтаксис и типы/Базовый синтаксис/Строковые шаблоны и вывод.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) diff --git a/Kotlin/Синтаксис и типы/Базовый синтаксис/Типы данных.md b/Kotlin/Синтаксис и типы/Базовый синтаксис/Типы данных.md new file mode 100644 index 0000000..7a082af --- /dev/null +++ b/Kotlin/Синтаксис и типы/Базовый синтаксис/Типы данных.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) diff --git a/Kotlin/Синтаксис и типы/Базовый синтаксис/Условные конструкции.md b/Kotlin/Синтаксис и типы/Базовый синтаксис/Условные конструкции.md new file mode 100644 index 0000000..46e3a9a --- /dev/null +++ b/Kotlin/Синтаксис и типы/Базовый синтаксис/Условные конструкции.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` написать нельзя. Подробнее: [Reified и inline](../../Дженерики/Reified%20и%20inline.md) + +Далее: [Циклы и диапазоны](Циклы%20и%20диапазоны.md) diff --git a/Kotlin/Синтаксис и типы/Базовый синтаксис/Циклы и диапазоны.md b/Kotlin/Синтаксис и типы/Базовый синтаксис/Циклы и диапазоны.md new file mode 100644 index 0000000..24e41a9 --- /dev/null +++ b/Kotlin/Синтаксис и типы/Базовый синтаксис/Циклы и диапазоны.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.. println("итерация $i") } // индекс доступен, с нуля +``` +Это не оператор, а [inline-функция](../../Функциональный%20Kotlin/Inline-функции.md) стандартной библиотеки. + +> [!info] Отличия от Java +> | Java | Kotlin | +> |---|---| +> | `for (int i=0; i | `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.. ```kotlin +> val list = listOf("a", "b", "c") +> // for (i in 0..list.size) println(list[i]) // IndexOutOfBounds на i=3 +> for (i in 0.. 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)