diff --git a/Kotlin/Kotlin.md b/Kotlin/Kotlin.md deleted file mode 100644 index 055655e..0000000 --- a/Kotlin/Kotlin.md +++ /dev/null @@ -1,145 +0,0 @@ ---- -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/Взаимодействие с Java/Java из Kotlin.md b/Kotlin/Взаимодействие с Java/Java из Kotlin.md deleted file mode 100644 index 2d8e1e9..0000000 --- a/Kotlin/Взаимодействие с Java/Java из Kotlin.md +++ /dev/null @@ -1,385 +0,0 @@ ---- -title: Java из Kotlin -description: Платформенные типы, синтетические свойства, SAM-конверсия, массивы, статические члены и escape-хатчи -tags: - - kotlin - - java-interop ---- - -Вызов Java из Kotlin почти не требует усилий: Java-классы видны как обычные Kotlin-классы. Трение возникает в четырёх местах - null-безопасность, свойства, массивы и ключевые слова. - -```kotlin -import java.io.File -import java.util.ArrayList - -fun main() { - val list = ArrayList() - list.add("первый") - list.add("второй") - println(list.size) // 2 - println(list.joinToString()) // первый, второй - - val tmp = File.createTempFile("demo", ".txt") - tmp.writeText("содержимое") - println(tmp.readText()) // содержимое - tmp.delete() -} -``` -Обратите внимание: `joinToString` и `writeText` - это Kotlin-расширения, работающие на Java-типах. Stdlib добавляет сотни таких функций к `java.util` и `java.io`. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md) - -## Платформенные типы -Главная особенность: тип из Java записывается как `String!` - компилятор не знает, может ли там быть `null`. - -```kotlin -fun main() { - // System.getProperty возвращает String! — может быть null - val asNonNull: String = System.getProperty("java.version") // компилируется - val asNullable: String? = System.getProperty("нет.такого") // тоже компилируется - - println(asNonNull.isNotEmpty()) // true - println(asNullable?.length) // null -} -``` -Компилятор разрешает оба варианта, поэтому ответственность на вас. Если Java вернёт `null` там, где объявлен ненулевой тип, проверка сработает в точке присваивания: -```kotlin -fun main() { - val error = runCatching { - val value: String = System.getProperty("отсутствует") - value.length - }.exceptionOrNull() - println(error?.javaClass?.simpleName) // NullPointerException -} -``` -Сообщение будет вида «Parameter specified as non-null is null» или «null cannot be cast to non-null type» - проверка вставляется компилятором. Подробнее: [Платформенные типы](../Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md) - -**Аннотации в Java решают проблему.** Если Java-код аннотирован, платформенных типов не возникает: -```java -// Java -@Nullable public String findName() { return null; } // в Kotlin: String? -@NotNull public String getId() { return "1"; } // в Kotlin: String -public String plain() { return null; } // в Kotlin: String! -``` -Поддерживаются `org.jetbrains.annotations`, JSR-305 (`javax.annotation`), `androidx.annotation`, Lombok, Eclipse и Checker Framework. Это единственный способ получить настоящую null-безопасность на границе с Java, поэтому в смешанных проектах аннотировать Java-код стоит. - -## Синтетические свойства -Java-геттеры и сеттеры видны как свойства Kotlin: -```kotlin -import java.util.Calendar - -fun main() { - val cal = Calendar.getInstance() - - cal.timeInMillis = 0L // вместо setTimeInMillis(0) - println(cal.timeInMillis) // 0 - - val sb = StringBuilder("текст") - println(sb.length) // 5 — вместо length() -} -``` -Правила распознавания: - -| Java-метод | Свойство в Kotlin | -|---|---| -| `getValue()` | `value` (только чтение) | -| `getValue()` + `setValue(T)` | `value` (чтение и запись) | -| `isReady()` | `isReady` (имя сохраняется) | -| `setOnly(T)` без геттера | свойства нет, только метод | -| `getValue(int index)` | свойства нет - есть параметр | - -```kotlin -fun main() { - val thread = Thread.currentThread() - println(thread.name.isNotEmpty()) // true — getName() - println(thread.isDaemon) // false — isDaemon() сохраняет префикс - println(thread.priority) // 5 — getPriority() -} -``` -Синтетические свойства доступны только для Java-методов. Kotlin-функция с именем `getFoo()` свойством не становится. - -## SAM-конверсия -Java-интерфейс с одним абстрактным методом принимает лямбду: -```kotlin -import java.util.concurrent.Executors - -fun main() { - val executor = Executors.newSingleThreadExecutor() - - executor.submit { println("как Runnable") } // лямбда -> Runnable - val future = executor.submit { "как Callable" } // лямбда -> Callable - println(future.get()) - - val items = mutableListOf("гамма", "альфа", "бета") - items.sortWith { a, b -> a.compareTo(b) } // Comparator - println(items) - executor.shutdown() - // как Runnable - // как Callable - // [альфа, бета, гамма] -} -``` -Когда перегрузок несколько и тип неоднозначен, интерфейс указывают явно: -```kotlin -import java.util.concurrent.Executors - -fun main() { - val executor = Executors.newSingleThreadExecutor() - val task = Runnable { println("явная SAM-конструкция") } - executor.submit(task) - executor.shutdown() - Thread.sleep(50) - // явная SAM-конструкция -} -``` -Важная тонкость: SAM-конверсия работает для **Java**-интерфейсов автоматически. Для Kotlin-интерфейса нужно объявить его как `fun interface`: -```kotlin -fun interface Validator { - fun validate(input: String): Boolean -} - -fun check(v: Validator, s: String) = v.validate(s) - -fun main() { - println(check({ it.isNotEmpty() }, "текст")) // true -} -``` -Подробнее: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md) - -## Массивы -Java-массивы отображаются на типизированные классы Kotlin, причём примитивные - на специальные: -```kotlin -fun main() { - val ints: IntArray = intArrayOf(1, 2, 3) // int[] - val strings: Array = arrayOf("a", "b") // String[] - val boxed: Array = arrayOf(1, 2) // Integer[] - - println(ints::class.simpleName) // IntArray - println(strings::class.simpleName) // Array - println(ints.sum()) // 6 - println(strings.joinToString()) // a, b - println(boxed.size) // 2 -} -``` -`IntArray` компилируется в `int[]` без упаковки, а `Array` - в `Integer[]`. Это важно для производительности и для совместимости: Java-метод, принимающий `int[]`, не примет `Array`. - -Передача массива в varargs требует spread-оператора: -```kotlin -fun main() { - val parts = arrayOf("a", "b", "c") - println(String.format("%s-%s-%s", *parts)) // a-b-c - - val numbers = intArrayOf(3, 1, 2) - println(listOf(*numbers.toTypedArray()).sorted()) // [1, 2, 3] -} -``` -`*` распаковывает массив в отдельные аргументы - без него массив передался бы как один элемент. - -Статические методы класса `java.lang.String` через Kotlin-овский `String` недоступны - `String` в Kotlin это `kotlin.String`, у него нет `join`: -```kotlin -fun main() { - val parts = listOf("x", "y") - println(java.lang.String.join("/", parts)) // x/y - println(parts.joinToString("/")) // x/y — идиоматично -} -``` - -## Статические члены -```kotlin -fun main() { - println(Integer.MAX_VALUE) // 2147483647 - println(Integer.parseInt("42")) // 42 - println(Math.max(3, 7)) // 7 - - val list = java.util.Collections.emptyList() - println(list.size) // 0 -} -``` -Статические методы и поля вызываются через имя класса, как в Java. Для часто используемых есть Kotlin-аналоги: `"42".toInt()` вместо `Integer.parseInt`, `maxOf(3, 7)` вместо `Math.max`, `emptyList()` вместо `Collections.emptyList()`. - -## Java-ключевые слова как идентификаторы -Если Java-метод называется как ключевое слово Kotlin, его имя берётся в обратные кавычки: -```kotlin -class JavaLike { - fun `is`(value: Int) = value > 0 // в Kotlin `is` — ключевое слово - fun `in`(value: Int) = value - fun `object`() = "объект" -} - -fun main() { - val j = JavaLike() - println(j.`is`(5)) // true - println(j.`in`(1)) // 1 - println(j.`object`()) // объект -} -``` -Чаще всего это встречается с методами `is`, `in`, `object`, `when`, `val`, `var`, `fun`, `typealias`. Backticks работают и для имён с пробелами - этим пользуются в названиях тестов. Подробнее: [kotlin.test и JUnit 5](../Тестирование/kotlin.test%20и%20JUnit%205.md) - -## Checked exceptions исчезают -```kotlin -import java.io.File - -fun main() { - // Java-метод объявляет throws IOException, но Kotlin этого не требует - val tmp = File.createTempFile("demo", ".txt") - tmp.writeText("данные") - println(tmp.readText()) - tmp.delete() - // данные -} -``` -В Kotlin нет проверяемых исключений: `try/catch` вокруг `createTempFile` не обязателен. Это упрощает код, но требует дисциплины - компилятор больше не напоминает об обработке ошибок. Подробнее: [Система исключений](../Исключения/Система%20исключений.md) - -## Наследование от Java-класса -```kotlin -import java.io.IOException - -class CustomException(message: String, cause: Throwable? = null) : - IOException(message, cause) - -abstract class BaseTask : Runnable { - abstract fun execute() - override fun run() { - try { - execute() - } catch (e: Exception) { - println("ошибка: ${e.message}") - } - } -} - -class FailingTask : BaseTask() { - override fun execute() = throw CustomException("задача упала") -} - -fun main() { - FailingTask().run() // ошибка: задача упала -} -``` -Наследование работает в обе стороны без ограничений: Kotlin-класс может расширять Java-класс, реализовывать Java-интерфейс и быть предком Java-класса. - -## Java-типы, которые заменяют на Kotlin-аналоги -| Java | Предпочтительно в Kotlin | Почему | -|---|---|---| -| `java.util.ArrayList` | `mutableListOf()` | короче, тот же класс внутри | -| `Integer.parseInt(s)` | `s.toInt()` | расширение, читается слева направо | -| `String.valueOf(x)` | `x.toString()` | то же | -| `Math.max(a, b)` | `maxOf(a, b)` | работает с любым `Comparable` | -| `Collections.sort(list)` | `list.sort()` | расширение | -| `Optional` | `T?` | nullable-тип нативен | -| `new String[]{...}` | `arrayOf(...)` | короче | -| `StringBuilder` | интерполяция или `buildString` | читаемее | - -```kotlin -fun main() { - val fromJava = java.util.ArrayList().apply { add("a") } - val idiomatic = mutableListOf("a") - println("тот же класс: ${fromJava::class == idiomatic::class}") // тот же класс: true - - println(buildString { - append("собрано ") - append("через buildString") - }) - // собрано через buildString -} -``` -`mutableListOf()` возвращает именно `java.util.ArrayList` - разница только в синтаксисе. - -## Optional -```kotlin -import java.util.Optional - -fun main() { - val present: Optional = Optional.of("значение") - val empty: Optional = Optional.empty() - - println(present.orElse("по умолчанию")) // значение - println(empty.orElse("по умолчанию")) // по умолчанию - - // перевод в идиоматичный Kotlin - val asNullable: String? = present.orElse(null) - println(asNullable?.length) // 8 - println(empty.getOrNull()?.length) // null -} - -fun Optional.getOrNull(): T? = orElse(null) -``` -`Optional` в Kotlin-коде избыточен: его роль выполняет `T?`, который не создаёт обёртку и поддержан синтаксисом (`?.`, `?:`, `let`). При получении `Optional` из Java-API его сразу переводят в nullable. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `getName()` | `name` (синтетическое свойство) | -> | `isReady()` | `isReady` | -> | `new Runnable() { ... }` | `{ ... }` (SAM) | -> | `int[]` | `IntArray` | -> | `Integer[]` | `Array` | -> | `method(array)` для varargs | `method(*array)` | -> | `throws IOException` обязателен | не требуется | -> | `Optional` | `T?` | -> -> **Платформенные типы - компромисс, которого в Java нет.** В чистом Kotlin компилятор гарантирует отсутствие NPE. На границе с Java гарантия исчезает: тип `String!` означает «компилятор не знает». Альтернативой было бы считать все Java-типы nullable - тогда Java-код требовал бы `?.` и `!!` на каждом шагу и стал бы неудобен. Авторы Kotlin выбрали практичность: ответственность за null на границе лежит на программисте. -> -> **Синтетические свойства работают только для Java.** Это важная асимметрия: Java-метод `getName()` в Kotlin доступен и как `getName()`, и как `.name`, а Kotlin-функция `getName()` - только как функция. Причина в том, что в Kotlin для этого есть настоящие свойства, и превращать функции в свойства означало бы двусмысленность. -> -> **SAM-конверсия автоматическая только для Java-интерфейсов.** В Java любой интерфейс с одним методом принимает лямбду. В Kotlin - нет: нужен `fun interface`, иначе требуется `object : Interface { }`. Причина - в Kotlin есть функциональные типы (`(String) -> Boolean`), которые покрывают большинство случаев, и SAM-интерфейсы нужны реже. -> -> **Массивы различаются сильнее, чем кажется.** В Java `int[]` и `Integer[]` - разные типы, но оба «массивы». В Kotlin это `IntArray` и `Array`, причём `Array` ковариантен по-другому: `Array` не является `Array`, тогда как в Java `String[]` является `Object[]` - что и породило `ArrayStoreException`. Kotlin эту дыру закрыл, сделав `Array` инвариантным. Подробнее: [Вариантность](../Дженерики/Вариантность.md) -> -> **Отсутствие проверяемых исключений - сознательное решение.** Опыт Java показал, что `throws` чаще приводит к `catch (Exception e) { }` и обёрткам в `RuntimeException`, чем к осмысленной обработке, а в сочетании с лямбдами и дженериками создаёт неразрешимые проблемы (функциональный интерфейс не может объявить произвольный `throws`). Цена - потеря документирующей роли и необходимость `@Throws` при вызове из Java. -> -> Сравнить: [Массивы в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Массивы.md), [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md) - -> [!warning] Подводные камни -> **Платформенный тип даёт NPE в неожиданном месте:** -> ```kotlin -> fun main() { -> val props = System.getProperties() -> val error = runCatching { -> val missing: String = props.getProperty("нет.такого") -> missing.uppercase() -> }.exceptionOrNull() -> println("NPE при присваивании: ${error is NullPointerException}") -> // NPE при присваивании: true -> } -> ``` -> Исключение возникает не там, где значение используется, а там, где оно присваивается ненулевому типу. Стектрейс указывает на строку присваивания, а не на Java-метод. -> -> **`!!` на платформенном типе бессмысленен.** `javaMethod()!!` не добавляет безопасности - проверка и так вставляется. Правильно - `?:` с осмысленным значением по умолчанию или явный тип `String?`. -> -> **Java-аннотации nullability обязательны для настоящей безопасности.** Без них компилятор Kotlin не знает ничего, и весь Java-API остаётся зоной риска. Это основная причина добавлять `@Nullable`/`@NotNull` в унаследованный Java-код при миграции. -> -> **Неизменяемая Kotlin-коллекция, переданная в Java, может быть изменена:** -> ```kotlin -> fun main() { -> val readOnly = listOf("a") -> val error = runCatching { (readOnly as MutableList).add("b") } -> .exceptionOrNull() -> println(error?.javaClass?.simpleName) // UnsupportedOperationException -> } -> ``` -> Java-код увидит `java.util.List` с методом `add()` и может его вызвать. Приведение компилируется, падение - в рантайме. Передавайте копию через `toMutableList()`, если Java-код изменяет коллекцию. -> -> **`Array` и `IntArray` не взаимозаменяемы.** Java-метод с параметром `int[]` требует `IntArray`; `arrayOf(1, 2)` даёт `Array` (то есть `Integer[]`) и не подойдёт. Конвертация - `toIntArray()`, и она копирует данные. -> -> **Забытый spread-оператор меняет смысл:** -> ```kotlin -> fun main() { -> val parts = arrayOf("a", "b") -> println(listOf(parts).size) // 1 — массив как один элемент -> println(listOf(*parts).size) // 2 — распакованный -> } -> ``` -> Компилятор не всегда предупреждает - `listOf(parts)` корректен, просто создаёт `List>`. -> -> **`getClass()` вызывается как `javaClass`.** Метод `Object.getClass()` в Kotlin недоступен напрямую: он конфликтует с `::class`. Используйте `javaClass` для `java.lang.Class` и `::class` для `KClass`. -> -> **Статические вложенные Java-классы требуют полного пути.** `Map.Entry` в Kotlin - это `Map.Entry`, но `java.util.AbstractMap.SimpleEntry` нужно импортировать явно; обращение через внешний класс (`AbstractMap.SimpleEntry`) работает только после импорта внешнего. -> -> **Lombok-геттеры видны, но не всегда.** Lombok генерирует методы на этапе аннотационной обработки Java, и Kotlin-компилятор, работающий первым, их не видит. Нужен плагин `kotlin("plugin.lombok")` и `lombok.config` с `lombok.addLombokGeneratedAnnotation = true`. -> -> **Java-дженерики с wildcards переводятся в проекции.** `List` становится `List`, `List` - `List`. Raw-тип `List` без параметра становится `List<*>`, и элементы из него имеют тип `Any?`. Подробнее: [Проекции типов](../Дженерики/Проекции%20типов.md) -> -> **Вызов Java-метода с `Object`-параметром принимает всё.** `method(Object o)` в Kotlin имеет параметр `Any!`, поэтому туда пройдёт и `null`, и любое значение - компилятор не поможет. Для таких API особенно важны аннотации. -> -> **SAM-конверсия не работает для абстрактных классов.** `Thread(Runnable { })` - да, но `object : Thread() { }` для переопределения `run()` - только через наследование. SAM применяется только к интерфейсам с единственным абстрактным методом. - -Далее: [Kotlin из Java](Kotlin%20из%20Java.md) diff --git a/Kotlin/Взаимодействие с Java/Kotlin в байт-коде.md b/Kotlin/Взаимодействие с Java/Kotlin в байт-коде.md deleted file mode 100644 index 85a3bf0..0000000 --- a/Kotlin/Взаимодействие с Java/Kotlin в байт-коде.md +++ /dev/null @@ -1,434 +0,0 @@ ---- -title: Kotlin в байт-коде -description: Во что компилируются свойства, лямбды, data class, inline, suspend, value class и null-проверки -tags: - - kotlin - - java-interop - - jvm ---- - -Понимание представления Kotlin в байт-коде отвечает на три практических вопроса: почему API выглядит из Java именно так, откуда берутся накладные расходы и что происходит при `NullPointerException` внутри сгенерированного кода. - -Инструмент один - `javap`: -``` -kotlinc Demo.kt -d out # компиляция в .class -javap -p -cp out DemoKt # сигнатуры, включая приватные -javap -c -p -cp out DemoKt # с байт-кодом -javap -v -p -cp out DemoKt # с аннотациями и константным пулом -``` - -```kotlin -fun add(a: Int, b: Int) = a + b - -fun main() { - println(add(2, 3)) // 5 -} -``` -Эта функция компилируется в `public static final int add(int, int)` внутри класса `DemoKt`. Никакой обёртки нет - сигнатура такая же, как у Java-метода. - -## Где живёт код -| Конструкция Kotlin | Класс в байт-коде | -|---|---| -| `class Foo` | `Foo` | -| функции верхнего уровня в `Demo.kt` | `DemoKt` | -| `object Bar` | `Bar` с полем `INSTANCE` | -| `companion object` в `Foo` | `Foo$Companion` + поле `Foo.Companion` | -| вложенный класс | `Foo$Inner` | -| анонимный объект | `Foo$method$1` | -| лямбда | `invokedynamic`, без отдельного класса | -| `enum class Color` | `Color extends java.lang.Enum` | - -```kotlin -object Registry { - private val items = mutableListOf() - fun add(item: String) { items += item } - fun size() = items.size -} - -fun main() { - Registry.add("a") - println(Registry.size()) // 1 -} -``` -`object` становится классом с приватным конструктором, статическим полем `INSTANCE` и статическим инициализатором. Это классический singleton, и его потокобезопасность обеспечена гарантиями JVM на загрузку класса. - -## Свойства -Свойство - это всегда приватное поле плюс аксессоры: -```kotlin -class Holder { - var mutable: String = "a" - val immutable: Int = 1 - private var hidden = 2 - lateinit var late: String - - fun touch() = hidden++ -} - -fun main() { - val h = Holder() - h.mutable = "b" - println("${h.mutable} ${h.immutable} ${h.touch()}") // b 1 2 -} -``` -В байт-коде: -``` -private java.lang.String mutable; -private final int immutable; -private int hidden; -public java.lang.String late; // lateinit — поле публично! -public final java.lang.String getMutable(); -public final void setMutable(java.lang.String); -public final int getImmutable(); -public final java.lang.String getLate(); -public final void setLate(java.lang.String); -``` -Три наблюдения: - -**`val` даёт `final` поле и только геттер.** Это не константа - значение может вычисляться в конструкторе. - -**`lateinit var` делает поле публичным.** Компилятор не может сделать его приватным, потому что геттер должен различать «не инициализировано» и «значение `null`» - для этого нужна прямая работа с полем, а `lateinit` исключает nullable-тип. Побочный эффект: поле видно из Java напрямую. - -**Приватное свойство аксессоров не получает** - доступ идёт к полю, если нет кастомного геттера. - -Свойство с кастомным геттером поля не создаёт вообще: -```kotlin -class Rect(val w: Int, val h: Int) { - val area: Int get() = w * h -} - -fun main() { - println(Rect(3, 4).area) // 12 -} -``` -`area` - только метод `getArea()`, считающий значение каждый раз. Подробнее: [Свойства](../ООП/Свойства.md) - -## Null-проверки -Для каждого ненулевого параметра публичной функции компилятор вставляет проверку: -```kotlin -fun nn(s: String) = s.length -fun nullable(s: String?) = s?.length - -fun main() { - println(nn("текст")) // 5 - println(nullable(null)) // null -} -``` -Байт-код `nn`: -``` -0: aload_0 -1: ldc #9 // String s -3: invokestatic #15 // Intrinsics.checkNotNullParameter:(Object;String)V -6: aload_0 -7: invokevirtual #21 // String.length:()I -10: ireturn -``` -`checkNotNullParameter` бросает `NullPointerException` с сообщением «Parameter specified as non-null is null» - это и есть защита границы от Java-кода. Для `nullable` проверки нет, зато есть ветвление и упаковка в `Integer`: -``` -public static final java.lang.Integer nullable(java.lang.String); -``` -Возвращаемый тип `Int?` стал `Integer` - nullable-примитив всегда упакован. - -Информация о nullability хранится в аннотациях: -``` -public static final int nn(java.lang.String); @NotNull на параметре -public static final java.lang.Integer nullable(...); @Nullable на методе и параметре -``` -Это `org.jetbrains.annotations.@NotNull`/`@Nullable` с `RuntimeInvisible`-удержанием. Их читают Java-IDE и другие JVM-языки. Подробнее: [Null-безопасность](../Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md) - -```mermaid -graph LR - A["String в Kotlin"] --> B["java.lang.String
+ @NotNull
+ checkNotNullParameter"] - C["String? в Kotlin"] --> D["java.lang.String
+ @Nullable
без проверки"] - E["Int в Kotlin"] --> F["int"] - G["Int? в Kotlin"] --> H["java.lang.Integer"] -``` - -## data class -```kotlin -data class Point(val x: Int, val y: Int) - -fun main() { - val p = Point(1, 2) - println(p) // Point(x=1, y=2) - println(p.copy(y = 5)) // Point(x=1, y=5) - println(p == Point(1, 2)) // true -} -``` -Генерируется: -``` -private final int x; -private final int y; -public Point(int, int); -public final int getX(); -public final int getY(); -public final int component1(); -public final int component2(); -public final Point copy(int, int); -public static Point copy$default(Point, int, int, int, java.lang.Object); -public java.lang.String toString(); -public int hashCode(); -public boolean equals(java.lang.Object); -``` -`component1()`/`component2()` нужны для деструктуризации - компилятор вызывает их по позиции. `copy$default` реализует значения по умолчанию: четвёртый параметр `int` - битовая маска переданных аргументов, пятый `Object` - технический. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md) - -## Значения по умолчанию -```kotlin -class Service { - fun call(url: String, retries: Int = 3, timeout: Long = 1000L) = - "$url r=$retries t=$timeout" -} - -fun main() { - println(Service().call("/api")) // /api r=3 t=1000 -} -``` -Два метода вместо одного: -``` -public final java.lang.String call(java.lang.String, int, long); -public static java.lang.String call$default(Service, java.lang.String, int, long, int, java.lang.Object); -``` -`$default` получает битовую маску: бит `i` установлен, если аргумент `i` **не** передан. Внутри он подставляет значение по умолчанию для каждого отсутствующего аргумента и вызывает основной метод. Отсюда два следствия: вызов со значениями по умолчанию стоит на один вызов дороже, а из Java `$default` недоступен (он синтетический и требует знания маски). - -## Лямбды и функциональные типы -```kotlin -fun noCapture(): () -> Int = { 42 } -fun withCapture(n: Int): () -> Int = { n } - -fun main() { - println(noCapture()()) // 42 - println(withCapture(7)()) // 7 -} -``` -Функциональный тип `() -> Int` - это интерфейс `kotlin.jvm.functions.Function0`. Их 23: `Function0` ... `Function22`. -``` -public static final kotlin.jvm.functions.Function0 noCapture(); - 0: invokedynamic #81, 0 // InvokeDynamic #0:invoke:()Lkotlin/jvm/functions/Function0; -``` -Начиная с K2 лямбды компилируются через `invokedynamic` (как в Java), а не в отдельные классы `Foo$lambda$1`. Это уменьшает размер артефакта и ускоряет загрузку, но означает, что имя класса лямбды генерируется в рантайме - в стектрейсах появляются `$$Lambda$...`. - -Важная деталь: `Function0` работает с **упакованным** `Integer`. Лямбда, возвращающая `Int`, упаковывает результат на каждом вызове - если это горячий цикл, нужна `inline`-функция. Подробнее: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md) - -## inline -```kotlin -inline fun measure(block: () -> Unit) { - block() -} - -fun useInline() { - measure { println("тело") } -} - -fun main() { - useInline() // тело -} -``` -Байт-код `useInline` не содержит вызова `measure` вообще: -``` -public static final void useInline(); - 0: iconst_0 - 1: istore_0 - 2: iconst_0 - 3: istore_1 - 4: ldc #49 // String тело - 6: getstatic #55 // System.out - 9: swap - 10: invokevirtual #61 // println - 13: nop - 14: nop - 15: return -``` -Тело лямбды подставлено прямо в метод: ни объекта `Function0`, ни вызова по ссылке. Это основа `forEach`, `let`, `map` и остальных операций stdlib - они не создают объектов. Цена - код дублируется в каждой точке вызова. Подробнее: [Inline-функции](../Функциональный%20Kotlin/Inline-функции.md) - -Сама `measure` всё равно существует как метод (для Java-клиентов и рефлексии): -``` -public static final void measure(kotlin.jvm.functions.Function0); -``` - -## suspend -```kotlin -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking - -suspend fun fetch(id: Int): String { - delay(10) - return "ответ $id" -} - -fun main() = runBlocking { - println(fetch(1)) // ответ 1 -} -``` -Сигнатура меняется радикально: -``` -public static final java.lang.Object fetch(int, kotlin.coroutines.Continuation); -``` -Три изменения: добавлен параметр `Continuation`, тип возврата стал `Object` (вместо `String`), и само тело превратилось в конечный автомат. Функция может вернуть либо результат, либо специальный маркер `COROUTINE_SUSPENDED`, означающий «я приостановилась, результат придёт через `Continuation`». - -Конечный автомат - это анонимный класс с полем `label`, хранящим номер точки приостановки, и полями для всех локальных переменных, живущих через приостановку. При возобновлении функция вызывается снова, читает `label` и прыгает в нужную ветку `switch`. Это CPS-преобразование (continuation-passing style), выполняемое компилятором. - -Отсюда свойства корутин: приостановка не блокирует поток (нет никакого потока - есть объект состояния), стектрейсы короткие, а Java-код вызвать `suspend`-функцию практически не может. Подробнее: [Suspend-функции](../Корутины/Suspend-функции.md) - -## value class -```kotlin -@JvmInline -value class UserId(val value: Int) - -fun process(id: UserId) = "обработан ${id.value}" - -fun main() { - println(process(UserId(42))) // обработан 42 -} -``` -Класс `UserId` в байт-коде существует, но в сигнатурах заменяется на обёрнутый тип: -``` -public static final java.lang.String process-4ZD5Yi0(int); -``` -Параметр стал `int` - обёртки в рантайме нет. Суффикс `-4ZD5Yi0` (хеш от типов) нужен, чтобы `process(UserId)` и `process(OrderId)` не конфликтовали: после стирания у обеих сигнатура `process(int)`. - -Обёртка всё-таки создаётся в трёх случаях: когда value-класс используется как дженерик-параметр (`List`), как nullable (`UserId?`) и при обращении через супертип. Подробнее: [Value class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Value%20class.md) - -## Делегированные свойства -```kotlin -class Config { - val expensive: String by lazy { "вычислено" } -} - -fun main() { - println(Config().expensive) // вычислено -} -``` -``` -private final kotlin.Lazy v$delegate; -public final java.lang.String getV(); -``` -Поле хранит не значение, а делегат. Геттер вызывает `delegate.getValue()`. Поэтому каждое делегированное свойство - это дополнительный объект и один лишний вызов на чтение. Для `by lazy` с дефолтным режимом внутри ещё и `synchronized`-блок при первом доступе. Подробнее: [Делегирование](../ООП/Делегирование.md) - -## Дженерики и вариантность -```kotlin -class Box>(val value: T) { - fun max(other: T) = if (value > other) value else other -} - -fun sizes(l: List, m: MutableList) = l.size + m.size - -fun main() { - println(Box(3).max(7)) // 7 - println(sizes(listOf(1, 2), mutableListOf(3))) // 3 -} -``` -``` -public final class Box> -public static final int sizes(java.util.List, java.util.List); -``` -Проекции Kotlin становятся wildcards Java: `out T` → `? extends T`, `in T` → `? super T`. Дженерики стираются так же, как в Java - в рантайме `Box` и `Box` неразличимы. Обойти это позволяет только `reified` в `inline`-функциях, где тип подставляется при компиляции. Подробнее: [Вариантность](../Дженерики/Вариантность.md), [Reified и inline](../Дженерики/Reified%20и%20inline.md) - -## Nothing и Unit -```kotlin -fun fail(): Nothing = throw IllegalStateException("всегда падает") -fun act(): Unit = println("действие") - -fun main() { - act() // действие - println(runCatching { fail() }.isFailure) // true -} -``` -`Unit` как возвращаемый тип становится `void`. `Nothing` - `java.lang.Void` (значение которого всегда `null`, потому что функция никогда не возвращается нормально). В обоих случаях Kotlin-компилятор знает больше, чем байт-код: `Nothing` делает код после вызова недостижимым, но Java-компилятор этого не увидит. Подробнее: [Any, Unit, Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md) - -## Метаданные -Каждый Kotlin-класс несёт аннотацию `kotlin.Metadata` с закодированной информацией о nullable-типах, значениях по умолчанию, свойствах, `suspend`, вариантности, `inline`, `sealed` и прочем. Именно её читает Kotlin-компилятор, подключая скомпилированную библиотеку: `.class`-файла недостаточно, чтобы восстановить Kotlin-API. - -Практические следствия: - -**Обфускация ломает Kotlin-interop.** ProGuard/R8 должны сохранять `kotlin.Metadata`, иначе библиотека перестанет быть пригодной для Kotlin-клиентов (но останется пригодной для Java). - -**Рефлексия Kotlin требует `kotlin-reflect`.** `KClass.memberProperties` читает метаданные, и для этого нужна отдельная зависимость весом ~2 МБ. Java-рефлексия (`javaClass.declaredFields`) работает без неё, но видит только байт-код. Подробнее: [Рефлексия](../Рефлексия/Рефлексия.md) - -**Java-библиотека не знает про Kotlin-типы.** Jackson, Hibernate и подобные видят только `.class`: конструктор со значениями по умолчанию, nullable-поле как обычное. Для корректной работы нужны модули-адаптеры (`jackson-module-kotlin`), которые читают `kotlin.Metadata`. - -## Сводная таблица -| Kotlin | Байт-код | -|---|---| -| `fun f()` на верхнем уровне | `static final` в `ФайлKt` | -| `object O` | класс + `static final O INSTANCE` | -| `companion object` | класс `Cls$Companion` + поле | -| `val x: T` | `private final T x` + `getX()` | -| `var x: T` | `private T x` + `getX()`/`setX()` | -| `lateinit var x` | `public T x` (поле публично) | -| `val x get() = ...` | только `getX()`, поля нет | -| `x: String` (параметр) | `String` + `@NotNull` + `checkNotNullParameter` | -| `x: String?` | `String` + `@Nullable` | -| `Int` | `int` | -| `Int?` | `java.lang.Integer` | -| `() -> Int` | `Function0` | -| лямбда | `invokedynamic` | -| `inline fun` | тело подставлено + метод для Java | -| `suspend fun f(): T` | `Object f(Continuation)` + автомат | -| `@JvmInline value class` | обёрнутый тип + суффикс в имени | -| `by lazy` | поле `x$delegate: Lazy` | -| `out T` / `in T` | `? extends T` / `? super T` | -| `Unit` | `void` | -| `Nothing` | `java.lang.Void` | -| `internal fun f()` | `public final f$module()` | - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | исходник ≈ байт-код | есть зазор, есть метаданные | -> | поля публичны, если так написано | поля всегда приватны (кроме `lateinit`) | -> | геттер пишется руками | генерируется | -> | лямбда → `invokedynamic` | то же (с K2) | -> | нет значений по умолчанию | синтетический `$default` | -> | нет `inline` | тело подставляется | -> | `async` через библиотеку | `suspend` → конечный автомат | -> | `int` и `Integer` в коде | `Int` и `Int?` | -> -> **Главное отличие - наличие метаданных.** В Java `.class` полностью описывает API: что видно в сигнатурах, то и есть. В Kotlin часть системы типов (nullability, значения по умолчанию, `suspend`, `inline`, `sealed`, вариантность) существует только в аннотации `kotlin.Metadata`. Это значит, что Kotlin-компилятор и Java-компилятор видят **разные** API одной библиотеки: первый богаче. Отсюда и необходимость сохранять метаданные при обфускации, и зависимость `kotlin-reflect`, и адаптеры для Jackson. -> -> **Проверки на null стоят вызова метода на каждый параметр.** В Java такой проверки нет вообще - `NullPointerException` возникает при первом использовании. Kotlin переносит сбой на границу функции, что делает диагностику проще (понятно, какой параметр виноват), но добавляет `invokestatic` на каждый ненулевой параметр публичной функции. В приватных и `inline`-функциях проверок нет: там компилятор уже гарантировал тип. -> -> **`inline` - возможность, которой в Java нет на уровне языка.** JIT в Java тоже встраивает вызовы, но это решение рантайма, непредсказуемое и не применимое к лямбдам с захватом. Kotlin встраивает на этапе компиляции, гарантированно и вместе с телом лямбды. Это и позволяет `collection.map { }` не создавать объектов, и даёт `reified`, и делает возможными `return` из лямбды. Цена - рост байт-кода и невозможность изменить тело без перекомпиляции клиентов. -> -> **`suspend` реализован компилятором, а не рантаймом.** В Java асинхронность требует либо потоков, либо `CompletableFuture` с цепочками колбэков, либо виртуальных потоков (Loom), которые меняют JVM. Kotlin делает CPS-преобразование на этапе компиляции и работает на любой JVM 8+. Следствие: корутины не требуют поддержки платформы, но и не интегрируются с Java-кодом. -> -> **Value-классы ближе к будущим Project Valhalla, чем к Java сегодня.** В Java любая обёртка над `int` - это объект в куче. Kotlin убирает обёртку на этапе компиляции, заменяя тип в сигнатурах. Цена - манглирование имён и обёртка в дженериках, где стирание не позволяет обойтись примитивом. -> -> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md), [Байт-код](../../Программирование%20на%20Java/JVM/Байт-код.md) - -> [!warning] Подводные камни -> **`lateinit var` создаёт публичное поле.** Это нарушает инкапсуляцию неожиданным образом: Java-код и рефлексия получают прямой доступ в обход геттера, минуя проверку инициализации. Если это важно, используйте `var x: T? = null` с приватным сеттером. -> -> **Nullable-примитив всегда упакован:** -> ```kotlin -> fun main() { -> val plain: Int = 1 -> val nullable: Int? = 1 -> println(plain == nullable) // true -> println(nullable!!::class.java.name) // java.lang.Integer -> val a: Int? = 1000 -> val b: Int? = 1000 -> println("identity: ${a === b}") // identity: false -> } -> ``` -> `Int?` это `Integer`, поэтому `===` на значениях вне кеша `-128..127` даёт `false`. В горячем коде `Int?` в коллекциях и лямбдах создаёт объекты. Подробнее: [Числа и переполнение](../Синтаксис%20и%20типы/Работа%20с%20данными/Числа%20и%20переполнение.md) -> -> **Значения по умолчанию стоят лишнего вызова.** `call("/api")` идёт через `call$default`, который разбирает битовую маску и вызывает `call`. Для горячего пути с миллионами вызовов разница измерима - там пишут явные перегрузки. -> -> **`inline` раздувает байт-код.** Большая `inline`-функция, вызванная в сотне мест, копируется сто раз. Компилятор предупреждает про «expected performance impact... insignificant» для функций без лямбда-параметров - это намёк, что `inline` там не нужен. -> -> **Изменение тела `inline`-функции требует перекомпиляции клиентов.** Старое тело уже вкомпилировано в их байт-код. Для публичных библиотек это серьёзное ABI-ограничение: `inline` в публичном API фиксирует реализацию. -> -> **Делегированное свойство - это объект и вызов.** `by lazy` на каждом из сотни свойств даёт сотню объектов `Lazy` и сотню косвенных вызовов. Для простых случаев дешевле `private var x: T? = null` с проверкой в геттере. -> -> **Стектрейс корутины не показывает путь вызова.** Конечный автомат возобновляется из диспетчера, поэтому в стектрейсе видна только текущая точка приостановки. Для отладки включают `-Dkotlinx.coroutines.debug` или зависимость `kotlinx-coroutines-debug`. Подробнее: [Корутины](../Корутины/Корутины.md) -> -> **Обфускация без правил для `kotlin.Metadata` ломает библиотеку для Kotlin-клиентов.** Код продолжит работать, но исчезнет nullability, пропадут значения по умолчанию, `kotlin-reflect` перестанет видеть свойства. В правилах R8 нужно `-keep class kotlin.Metadata`. -> -> **`javap` не показывает Kotlin-API.** Он читает байт-код, не метаданные: `suspend`-функция выглядит как метод с `Continuation`, а `data class` - как обычный класс с кучей методов. Чтобы увидеть Kotlin-вид скомпилированного кода, нужен `kotlinp` или IDE («Show Kotlin Bytecode» → «Decompile»). -> -> **Синтетические методы видны рефлексии.** `component1()`, `copy$default`, `access$...`, `Foo$Companion` - всё это попадёт в `getDeclaredMethods()`. Код, перебирающий методы класса (сериализаторы, валидаторы), должен фильтровать по `isSynthetic()`. -> -> **`internal` не защищает.** Манглированное имя `helper$module` публично. Java-код в том же артефакте вызовет его без препятствий, как и любая рефлексия. -> -> **Проверки `checkNotNullParameter` можно отключить, но не стоит.** Флаг `-Xno-param-assertions` убирает их, давая небольшой выигрыш. Цена - `NullPointerException` будет возникать глубоко внутри вместо границы функции, и диагностика станет заметно хуже. - -Далее: [Инструментарий](../Инструментарий/Инструментарий.md) diff --git a/Kotlin/Взаимодействие с Java/Kotlin из Java.md b/Kotlin/Взаимодействие с Java/Kotlin из Java.md deleted file mode 100644 index 6b8fd16..0000000 --- a/Kotlin/Взаимодействие с Java/Kotlin из Java.md +++ /dev/null @@ -1,525 +0,0 @@ ---- -title: Kotlin из Java -description: Аннотации @JvmStatic, @JvmOverloads, @JvmName, @JvmField, @Throws и то, как конструкции Kotlin выглядят из Java -tags: - - kotlin - - java-interop ---- - -Обратное направление сложнее: в Kotlin есть конструкции, которых нет в JVM, и компилятор представляет их по-своему. Без аннотаций ваш API окажется неудобным для Java-кода. - -```kotlin -class Greeter(val name: String) { - fun greet(greeting: String = "Привет") = "$greeting, $name!" -} - -fun main() { - println(Greeter("Мир").greet()) // Привет, Мир! -} -``` -Из Java этот же класс выглядит так: -```java -// Java -Greeter g = new Greeter("Мир"); -System.out.println(g.greet("Привет")); // параметр обязателен! -// g.greet(); // не скомпилируется -``` -Значение по умолчанию существует только для Kotlin: в байт-коде есть `greet(String)` и синтетический `greet$default(Greeter, String, int, Object)`, вызывать который вручную нельзя. - -## Карта проблем и аннотаций -| Конструкция Kotlin | Из Java без аннотации | Аннотация | -|---|---|---| -| значение по умолчанию | один метод со всеми параметрами | `@JvmOverloads` | -| `companion object` | `Cls.Companion.method()` | `@JvmStatic` | -| `object` | `Singleton.INSTANCE.method()` | `@JvmStatic` | -| `val` в `companion object` | `Cls.Companion.getX()` | `@JvmField` | -| `const val` | уже поле - ничего не нужно | - | -| функция верхнего уровня | `FileNameKt.fn()` | `@JvmName` на файле | -| свойство | `getX()` / `setX()` | `@get:JvmName` | -| `suspend fun` | лишний параметр `Continuation` | не решается | -| `value class` | манглированное имя | не решается | -| исключение | не видно в `throws` | `@Throws` | -| перегрузки с дженериками | конфликт стирания | `@JvmName` | - -```mermaid -graph TD - A["Kotlin API"] --> B{"Вызывается из Java?"} - B -->|"нет"| C["аннотации не нужны"] - B -->|"да"| D["проверить каждый пункт"] - D --> E["значения по умолчанию
@JvmOverloads"] - D --> F["companion / object
@JvmStatic"] - D --> G["константы
@JvmField или const"] - D --> H["исключения
@Throws"] - D --> I["имена
@JvmName"] -``` - -## @JvmOverloads -Генерирует перегрузку для каждого значения по умолчанию: -```kotlin -class Greeter(val name: String) { - @JvmOverloads - fun greet(greeting: String = "Привет", punct: String = "!") = "$greeting, $name$punct" -} - -fun main() { - val g = Greeter("Мир") - println(g.greet()) // Привет, Мир! - println(g.greet("Здравствуй")) // Здравствуй, Мир! - println(g.greet("Hi", ".")) // Hi, Мир. -} -``` -В байт-коде появляются три метода: -``` -public final String greet(String, String) // полный -public final String greet(String) // punct по умолчанию -public final String greet() // оба по умолчанию -``` -Перегрузки создаются **справа налево**: параметры отбрасываются с конца. Поэтому `greet(punct = ".")` из Java невозможно - такой перегрузки нет. Это ограничение влияет на порядок параметров: самые часто переопределяемые ставьте первыми. - -Для конструктора аннотация работает так же: -```kotlin -class Config @JvmOverloads constructor( - val host: String = "localhost", - val port: Int = 8080, -) { - override fun toString() = "$host:$port" -} - -fun main() { - println(Config()) // localhost:8080 - println(Config("example.com")) // example.com:8080 - println(Config("example.com", 443)) // example.com:443 -} -``` - -## @JvmStatic -Без неё члены `companion object` доступны только через `.Companion`: -```kotlin -class Repository private constructor(val id: String) { - companion object { - @JvmStatic - fun create(id: String) = Repository(id) - - fun createNoStatic(id: String) = Repository(id) - } -} - -fun main() { - println(Repository.create("1").id) // 1 - println(Repository.createNoStatic("2").id) // 2 -} -``` -Из Kotlin обе формы одинаковы. Из Java - нет: -```java -// Java -Repository.create("1"); // работает: статический метод -Repository.Companion.createNoStatic("2"); // без @JvmStatic только так -``` -То же для `object`: -```kotlin -object Registry { - private val items = mutableListOf() - - @JvmStatic - fun register(item: String) { items += item } - - fun size() = items.size -} - -fun main() { - Registry.register("a") - println(Registry.size()) // 1 -} -``` -Из Java: `Registry.register("a")` против `Registry.INSTANCE.size()`. - -## @JvmField и const -Свойство в `companion object` компилируется в приватное поле с геттером на `Companion`. Три варианта: -```kotlin -class Limits { - companion object { - const val MAX = 100 // примитив/String, compile-time - @JvmField val DEFAULT = "значение" // поле, доступно напрямую - val COMPUTED = "вычисляется".uppercase() // только через геттер - } -} - -fun main() { - println(Limits.MAX) // 100 - println(Limits.DEFAULT) // значение - println(Limits.COMPUTED) // ВЫЧИСЛЯЕТСЯ -} -``` -Из Java: -```java -// Java -Limits.MAX; // public static final int -Limits.DEFAULT; // public static final String -Limits.Companion.getCOMPUTED(); // только геттер -``` -`const val` требует примитив или `String` и инициализацию константным выражением - зато встраивается в место вызова (и потому меняет ABI при изменении значения). `@JvmField` работает с любым типом, но несовместим с `private`, `open`, `override`, `lateinit` и делегированными свойствами. - -## @JvmName -Задаёт имя в байт-коде. Три точки применения. - -**Файл.** Функции верхнего уровня попадают в класс `ИмяФайлаKt`: -```kotlin -@file:JvmName("StringUtils") - -fun String.slugify() = lowercase().replace(" ", "-") - -fun main() { - println("Привет Мир".slugify()) // привет-мир -} -``` -Без аннотации Java-код обращался бы к `SlugifyKt.slugify(s)`, с ней - `StringUtils.slugify(s)`. Расширения становятся статическими методами с получателем первым параметром. - -**Аксессор свойства:** -```kotlin -class Account(private val raw: String) { - @get:JvmName("fetchBalance") - val balance: String get() = raw -} - -fun main() { - println(Account("1000").balance) // 1000 -} -``` -Из Java метод называется `fetchBalance()`, а не `getBalance()`. - -**Обход конфликта стирания.** Две функции, различающиеся только параметром дженерика, в байт-коде имеют одинаковую сигнатуру: -```kotlin -@JvmName("sumInts") -fun List.sumAll() = sum() - -@JvmName("sumDoubles") -fun List.sumAll() = sum() - -fun main() { - println(listOf(1, 2, 3).sumAll()) // 6 - println(listOf(1.5, 2.5).sumAll()) // 4.0 -} -``` -Без `@JvmName` компилятор откажется: «platform declaration clash». Это одна из немногих ситуаций, когда аннотация нужна даже для чисто Kotlin-кода. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md) - -## @Throws -В Kotlin нет проверяемых исключений, поэтому в байт-коде нет `throws`: -```kotlin -import java.io.IOException - -class Loader { - @Throws(IOException::class) - fun load(path: String): String { - if (path.isEmpty()) throw IOException("пустой путь") - return "данные из $path" - } - - fun loadNoThrows(path: String): String { - if (path.isEmpty()) throw IOException("пустой путь") - return "данные из $path" - } -} - -fun main() { - val loader = Loader() - println(loader.load("/tmp/file")) // данные из /tmp/file - println(runCatching { loader.loadNoThrows("") }.isFailure) // true -} -``` -Из Java разница принципиальна: -```java -// Java -try { loader.load(""); } -catch (IOException e) { } // компилируется - -try { loader.loadNoThrows(""); } -catch (IOException e) { } // ОШИБКА: exception is never thrown -``` -Java-компилятор запрещает ловить проверяемое исключение, которое метод не объявляет. Исключение всё равно вылетит в рантайме - просто обработать его можно только через `catch (Exception e)`. - -## Как выглядят остальные конструкции -### Nullability -```kotlin -class Validator { - fun check(input: String): Boolean = input.isNotEmpty() - fun find(key: String): String? = if (key == "a") "найдено" else null -} - -fun main() { - val v = Validator() - println(v.check("текст")) // true - println(v.find("b")) // null -} -``` -`check(String)` получает аннотацию `@NotNull` на параметре и проверку `Intrinsics.checkNotNullParameter` в начале тела. Java-код, передавший `null`, получит `NullPointerException` с сообщением «Parameter specified as non-null is null» - сразу, а не при первом использовании. `find` возвращает `@Nullable String`, и IDE предупредит Java-код. - -### Свойства -```kotlin -class User { - var name: String = "аноним" - val id: Int = 1 - var internalState: String = "скрыто" - private set -} - -fun main() { - val u = User() - u.name = "Иван" - println("${u.name} ${u.id} ${u.internalState}") // Иван 1 скрыто -} -``` -Из Java: `getName()`/`setName()`, `getId()` без сеттера, `getInternalState()` без сеттера (приватный сеттер в байт-коде приватен). Само поле всегда приватно - прямого доступа из Java нет. - -### Функции-расширения -```kotlin -fun List.firstOrDefault(default: String) = firstOrNull() ?: default - -fun main() { - println(listOf("a").firstOrDefault("нет")) // a - println(emptyList().firstOrDefault("нет")) // нет -} -``` -Из Java это статический метод с получателем первым аргументом: `FileNameKt.firstOrDefault(list, "нет")`. Никакого синтаксического сахара - расширение существует только в Kotlin. - -### suspend-функции -```kotlin -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking - -suspend fun fetch(id: Int): String { - delay(10) - return "ответ для $id" -} - -fun main() = runBlocking { - println(fetch(1)) // ответ для 1 -} -``` -В байт-коде сигнатура становится `Object fetch(int, Continuation)`, возвращая либо результат, либо маркер `COROUTINE_SUSPENDED`. Вызвать такое из Java практически невозможно. Для Java-клиентов делают обёртку: -```kotlin -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.future.future -import java.util.concurrent.CompletableFuture - -class Api(private val scope: CoroutineScope) { - suspend fun fetch(id: Int): String { - delay(10) - return "ответ для $id" - } - - // для Java: CompletableFuture вместо suspend - fun fetchAsync(id: Int): CompletableFuture = - scope.future { fetch(id) } -} - -fun main() { - val scope = CoroutineScope(Dispatchers.Default) - println(Api(scope).fetchAsync(7).get()) // ответ для 7 -} -``` -Нужна зависимость `kotlinx-coroutines-jdk8`. Подробнее: [Suspend-функции](../Корутины/Suspend-функции.md) - -### value class -```kotlin -@JvmInline -value class UserId(val value: Int) - -fun process(id: UserId) = "обработан ${id.value}" - -fun main() { - println(process(UserId(42))) // обработан 42 -} -``` -В байт-коде `process` получает имя `process-4ZD5Yi0(int)` - суффикс нужен, чтобы отличать перегрузки с разными value-классами над одним примитивом. Вызвать такое имя из Java нельзя: дефис недопустим в идентификаторе. Для Java-API добавляют обычную перегрузку: -```kotlin -@JvmInline -value class Money(val cents: Long) - -fun charge(amount: Money) = "списано ${amount.cents}" - -@JvmName("chargeCents") -fun charge(cents: Long) = charge(Money(cents)) - -fun main() { - println(charge(Money(500))) // списано 500 - println(charge(500L)) // списано 500 -} -``` -Подробнее: [Value class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Value%20class.md) - -### internal -```kotlin -class Service { - internal fun helper() = "вспомогательное" - fun public() = helper() -} - -fun main() { - println(Service().public()) // вспомогательное -} -``` -`internal` компилируется в `public` с манглированным именем вида `helper$main` (суффикс - имя модуля). Java-код технически может вызвать `helper$main()`. Это не изоляция, а соглашение, которое соблюдает только Kotlin-компилятор. Подробнее: [Модификаторы видимости](../ООП/Модификаторы%20видимости.md) - -### data class -```kotlin -data class Point(val x: Int, val y: Int) - -fun main() { - val p = Point(1, 2) - println(p) // Point(x=1, y=2) - println(p.copy(y = 5)) // Point(x=1, y=5) - val (x, y) = p - println("$x $y") // 1 2 -} -``` -Из Java доступны `getX()`, `getY()`, `equals`, `hashCode`, `toString`, `component1()`, `component2()` и `copy(int, int)`. Но `copy` с одним аргументом недоступна - это те же значения по умолчанию, и `@JvmOverloads` на сгенерированном `copy` поставить нельзя. Деструктуризации в Java нет. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md) - -### sealed -```kotlin -sealed interface Result -data class Ok(val value: String) : Result -data class Err(val message: String) : Result - -fun describe(r: Result) = when (r) { - is Ok -> "успех: ${r.value}" - is Err -> "ошибка: ${r.message}" -} - -fun main() { - println(describe(Ok("данные"))) // успех: данные - println(describe(Err("сбой"))) // ошибка: сбой -} -``` -Из Java `sealed interface` Kotlin - обычный интерфейс (при JVM-таргете 17+ может компилироваться в настоящий `sealed`). Исчерпывающего `switch` Java не получит - гарантия полноты обеспечивается только Kotlin-компилятором. Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md) - -## Чеклист публичного API для Java-клиентов -1. `@JvmOverloads` на всех функциях со значениями по умолчанию. -2. `@JvmStatic` на членах `companion object` и `object`. -3. `const val` или `@JvmField` для констант. -4. `@Throws` там, где Java-код должен ловить проверяемое исключение. -5. `@file:JvmName` для файлов с функциями верхнего уровня. -6. Нет `suspend` в публичном API - только обёртки (`CompletableFuture`, колбэки). -7. Нет `value class` в сигнатурах - или перегрузка с `@JvmName`. -8. Возвращать `List`, а не `MutableList`, и отдавать копию, если Java может изменить. -9. Nullable-типы объявлять явно - Java-IDE прочитает аннотации. -10. Проверить результат через `javap` на реальном классе. - -```kotlin -// пример API, готового к вызову из Java -@file:JvmName("UserApi") - -class UserService @JvmOverloads constructor( - private val baseUrl: String = "https://api.example.com", -) { - @Throws(IllegalArgumentException::class) - @JvmOverloads - fun find(id: String, includeDeleted: Boolean = false): String { - require(id.isNotBlank()) { "id не может быть пустым" } - return "$baseUrl/users/$id?deleted=$includeDeleted" - } - - companion object { - const val DEFAULT_TIMEOUT_MS = 5_000L - - @JvmStatic - fun default() = UserService() - } -} - -fun main() { - println(UserService.default().find("42")) - println(UserService.DEFAULT_TIMEOUT_MS) - // https://api.example.com/users/42?deleted=false - // 5000 -} -``` - -## Проверка результата -Единственный надёжный способ увидеть реальный API - посмотреть байт-код: -``` -kotlinc Api.kt -d out -javap -cp out com.example.UserApi -javap -cp out 'com.example.UserService$Companion' -``` -`javap` покажет точные сигнатуры, включая синтетические методы `$default` и манглированные имена. Это стоит делать перед релизом библиотеки. Подробнее: [Kotlin в байт-коде](Kotlin%20в%20байт-коде.md) - -> [!info] Отличия от Java -> | Java | Kotlin из Java | -> |---|---| -> | `static` метод | `@JvmStatic` в `companion object` | -> | `public static final` поле | `const val` или `@JvmField` | -> | перегрузки пишутся руками | `@JvmOverloads` генерирует | -> | `throws` в сигнатуре | `@Throws` | -> | имя класса = имя файла | `@file:JvmName` | -> | утилитный класс со `static` | функции верхнего уровня | -> | геттер пишется руками | генерируется из свойства | -> -> **Аннотации `@Jvm*` - это не костыли, а явное управление ABI.** В Java представление в байт-коде однозначно: что написано, то и скомпилируется. В Kotlin между исходником и байт-кодом есть зазор, и аннотации дают над ним контроль. Плата за выразительность языка: `@JvmOverloads` существует потому, что в JVM нет значений по умолчанию, а не из-за недоработки. -> -> **Значения по умолчанию в Kotlin устроены иначе, чем перегрузки в Java.** В Java N необязательных параметров требуют N перегрузок, каждую с делегированием. В Kotlin - один метод и синтетический `$default`, принимающий битовую маску переданных аргументов. Отсюда и ограничение `@JvmOverloads`: перегрузки генерируются только отбрасыванием с конца, потому что произвольные комбинации дали бы экспоненциальное число методов. -> -> **`companion object` - настоящий объект, а не пространство имён.** В Java `static` - модификатор: метод принадлежит классу. В Kotlin companion - отдельный экземпляр класса `Cls$Companion`, у него есть тип, он может реализовывать интерфейсы и быть получателем расширений. Поэтому без `@JvmStatic` вызов идёт через `Cls.Companion`. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md) -> -> **Отсутствие проверяемых исключений нельзя «включить» обратно.** `@Throws` только добавляет запись в атрибут `Exceptions` метода - сам Kotlin-компилятор её не проверяет. Фактически это документация для Java-компилятора: без неё Java не разрешит `catch`, с ней - потребует обработки. -> -> **`suspend` принципиально несовместим с Java.** Корутины реализованы через CPS-преобразование на этапе компиляции Kotlin: функция превращается в конечный автомат с параметром `Continuation`. Java-компилятор этого преобразования не делает, поэтому написать Java-вызывающий код для `suspend`-функции нельзя - только обёртки. Это самая жёсткая граница interop. Подробнее: [Корутины против потоков](../Корутины/Корутины%20против%20потоков.md) -> -> Сравнить: [Инкапсуляция в Java](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md), [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md) - -> [!warning] Подводные камни -> **`@JvmOverloads` генерирует перегрузки только справа налево.** Для `fun f(a: Int = 1, b: Int = 2, c: Int = 3)` появятся `f(a,b,c)`, `f(a,b)`, `f(a)`, `f()` - но не `f(c)` и не `f(a,c)`. Из Java нельзя передать только третий параметр. Ставьте чаще переопределяемые параметры раньше. -> -> **Добавление `@JvmStatic` или `@JvmOverloads` позже ломает совместимость.** `@JvmStatic` не удаляет метод с `Companion`, но меняет рекомендуемую точку входа; снятие аннотации удаляет статический метод и роняет скомпилированный Java-код с `NoSuchMethodError`. Решайте до релиза. -> -> **Изменение `const val` требует перекомпиляции клиентов.** Значение встраивается в место вызова, поэтому Java-код, собранный против старой версии, продолжит использовать старое число. Для значений, которые могут меняться, лучше `@JvmField val`. -> -> **`@JvmField` несовместим с половиной модификаторов:** -> ```kotlin -> class Limits { -> companion object { -> @JvmField val OK = "работает" -> // @JvmField private val BAD = "" // ошибка: не может быть private -> // @JvmField lateinit var LATE: String // ошибка: не для lateinit -> } -> } -> -> fun main() { -> println(Limits.OK) // работает -> } -> ``` -> Запрещено для `private`, `open`, `override`, `const`, `lateinit` и делегированных свойств. -> -> **`copy()` у `data class` из Java требует все аргументы.** `@JvmOverloads` на сгенерированный `copy` поставить нельзя. Если Java-коду нужна частичная копия, пишите свой метод или билдер. -> -> **Манглированные имена value-классов недоступны из Java.** `fun takeId(id: UserId)` компилируется в `takeId-4ZD5Yi0(int)`; дефис делает имя невызываемым в Java. Это проявится только при попытке интеграции - Kotlin-компилятор не предупредит. -> -> **`internal` виден из Java.** `internalFun$main()` - публичный метод в байт-коде. Если нужна настоящая закрытость для Java-кода в том же проекте, используйте `private` или выносите в отдельный модуль. -> -> **`List` из Kotlin в Java имеет `add()`.** Тип стирается до `java.util.List`, и Java-код может вызвать мутирующий метод. Падение будет в рантайме с `UnsupportedOperationException`. Для публичного API возвращайте `Collections.unmodifiableList(...)` или копию. -> -> **Платформенные объявления конфликтуют при стирании:** -> ```kotlin -> @JvmName("countStrings") -> fun List.count2() = size -> -> @JvmName("countNumbers") -> fun List.count2() = size -> -> fun main() { -> println(listOf("a").count2()) // 1 -> println(listOf(1, 2).count2()) // 2 -> } -> ``` -> Без `@JvmName` будет ошибка компиляции «platform declaration clash» - обе функции дают `count2(List)`. Это проявляется и в чисто Kotlin-проекте. -> -> **`@file:JvmName` должна стоять до `package`.** Файловые аннотации идут самой первой строкой файла, иначе компилятор их не примет. -> -> **`@get:JvmName` на свойстве в интерфейсе или `open` классе запрещена.** Переименование аксессора сломало бы переопределение, поэтому компилятор его не допускает для `open`/`override` членов. -> -> **Делегированные свойства из Java выглядят как обычные.** `by lazy` даёт `getX()`, но поле хранит `Lazy` под именем `x$delegate`. Прямого доступа к значению нет, и `@JvmField` не применить. Подробнее: [Делегирование](../ООП/Делегирование.md) -> -> **Расширения не переопределяются и не видны как методы.** `fun Foo.bar()` из Java - это `FileKt.bar(foo)`. Java-код не найдёт `foo.bar()`, а IDE не предложит. Если метод нужен Java-клиентам, делайте его членом класса. - -Далее: [Kotlin в байт-коде](Kotlin%20в%20байт-коде.md) diff --git a/Kotlin/Взаимодействие с Java/Взаимодействие с Java.md b/Kotlin/Взаимодействие с Java/Взаимодействие с Java.md deleted file mode 100644 index 037d12d..0000000 --- a/Kotlin/Взаимодействие с Java/Взаимодействие с Java.md +++ /dev/null @@ -1,202 +0,0 @@ ---- -title: Взаимодействие с Java -description: Зачем нужен interop, что работает без усилий, где возникает трение и как устроен раздел -tags: - - kotlin - - java-interop ---- - -Kotlin проектировался как язык, который должен без потерь жить в существующей Java-экосистеме. Это не побочная возможность, а требование: без доступа к Spring, Jackson, JDBC и остальной инфраструктуре JVM язык был бы бесполезен. - -```kotlin -import java.time.LocalDate -import java.util.UUID - -fun main() { - val id = UUID.randomUUID() - val today = LocalDate.of(2026, 1, 15) - - println(id.toString().length) // 36 - println(today.plusDays(30)) // 2026-02-14 - println(today.dayOfWeek) // THURSDAY -} -``` -Это обычный Java API, вызванный как родной Kotlin-код: никаких обёрток, конвертеров и объявлений внешних типов. - -## Что происходит без усилий -| Что | Как работает | -|---|---| -| вызов Java-метода из Kotlin | как обычный метод | -| геттеры/сеттеры Java | как свойства Kotlin | -| Java-коллекции | как `MutableList`, `MutableMap` и т.д. | -| наследование от Java-класса | как от Kotlin-класса | -| реализация Java-интерфейса | SAM-конверсия из лямбды | -| Java-аннотации | применяются к Kotlin-коду | -| смешанная компиляция в одном модуле | поддерживается Gradle-плагином | - -```kotlin -import java.util.concurrent.Executors - -fun main() { - val executor = Executors.newSingleThreadExecutor() - executor.submit { println("Runnable из лямбды") } // SAM-конверсия - executor.shutdown() - - val list = java.util.ArrayList() - list.add("элемент") // как MutableList - println(list.size) // 1 - - val builder = StringBuilder() - builder.append("a").append("b") - println(builder.length) // 2 - // Runnable из лямбды -} -``` - -## Где возникает трение -Три системы типов Kotlin не имеют соответствия в Java, и именно на этих границах появляются сложности. - -**1. Null-безопасность.** Java не различает `String` и `String?`, поэтому любое значение из Java - платформенный тип `String!`: -```kotlin -fun main() { - // Java-метод вернул null, а Kotlin считал тип ненулевым - val value: String = System.getProperty("несуществующее.свойство") - ?: "значение по умолчанию" - println(value) // значение по умолчанию -} -``` -Без `?:` здесь был бы `NullPointerException` в момент присваивания. Подробнее: [Платформенные типы](../Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md) - -**2. Изменяемость коллекций.** В Kotlin `List` и `MutableList` - разные типы, в Java это один `java.util.List`: -```kotlin -fun main() { - val immutable: List = listOf("a", "b") - // передача в Java-метод, который может вызвать add() - val asJava: java.util.List = immutable as java.util.List - val error = runCatching { asJava.add("c") }.exceptionOrNull() - println("неизменяемый список отверг изменение: ${error != null}") // неизменяемый список отверг изменение: true -} -``` -Java-код не видит разницы и может попытаться изменить список, который Kotlin считает неизменяемым. Подробнее: [Изменяемость](../Коллекции/Изменяемость.md) - -**3. Возможности Kotlin, которых нет в Java.** Значения по умолчанию, функции-расширения, `suspend`, value-классы, `object` - всё это компилируется в конструкции JVM, но из Java выглядит иначе, чем задумано. Для управления этим есть аннотации `@JvmStatic`, `@JvmOverloads`, `@JvmName` и другие. Подробнее: [Kotlin из Java](Kotlin%20из%20Java.md) - -## Асимметрия направлений -Направления не равноценны - и это главное, что нужно понять про interop. - -```mermaid -graph LR - A["Java-код"] -->|"почти без трения:
только null-безопасность"| B["Kotlin-код"] - B -->|"требует аннотаций:
@JvmStatic, @JvmOverloads,
@JvmName, @JvmField"| A -``` - -**Java из Kotlin** работает почти прозрачно: язык создавался именно для этого. Единственная серьёзная проблема - платформенные типы, то есть риск `NullPointerException` там, где компилятор уверен в ненулевом значении. - -**Kotlin из Java** требует внимания: без аннотаций ваш API окажется неудобным - значения по умолчанию превратятся в один метод с лишними параметрами, `companion object` потребует обращения через `.Companion`, а `suspend`-функция получит параметр `Continuation`. - -Практический вывод: если ваш Kotlin-код вызывается из Java, его публичный API нужно проектировать осознанно. Если Kotlin-код вызывается только из Kotlin, аннотации не нужны вообще. - -## Карта раздела -| Заметка | О чём | -|---|---| -| [Java из Kotlin](Java%20из%20Kotlin.md) | платформенные типы, SAM, массивы, статика, синтетические свойства | -| [Kotlin из Java](Kotlin%20из%20Java.md) | `@JvmStatic`, `@JvmOverloads`, `@JvmName`, `@JvmField`, `@Throws` | -| [Kotlin в байт-коде](Kotlin%20в%20байт-коде.md) | во что компилируются свойства, лямбды, `data class`, `suspend` | - -## Смешанный проект -Gradle-плагин Kotlin компилирует Java и Kotlin вместе, разрешая циклические зависимости между языками: -```kotlin -// build.gradle.kts -plugins { - kotlin("jvm") version "2.4.0" - java -} - -kotlin { - jvmToolchain(21) -} - -// Kotlin и Java в одном модуле: -// src/main/kotlin/ и src/main/java/ — компилируются совместно -``` -Порядок компиляции: сначала Kotlin видит Java-исходники (через стабы), затем `javac` компилирует Java, уже видя готовые Kotlin-классы. Поэтому Java-класс может наследоваться от Kotlin-класса, который реализует Java-интерфейс. Подробнее: [Структура проекта](../Инструментарий/Структура%20проекта.md) - -## Стратегия миграции -Типичный путь для существующего Java-проекта: - -1. **Новый код на Kotlin, старый не трогать.** Interop позволяет это без прослоек. -2. **Конвертировать по файлу**, начиная с листовых классов (DTO, утилиты) - у них меньше зависимостей. -3. **Проверять результат автоконвертера.** IDE конвертирует Java в Kotlin, но результат получается «Java, написанная на Kotlin»: платформенные типы вместо nullable, `!!` вместо `?.`, классы вместо `data class`. -4. **Публичный API, вызываемый из Java, аннотировать** сразу - иначе придётся менять его потом. -5. **Тесты конвертировать последними или не конвертировать** - они и так работают. - -```kotlin -// типичный результат автоконвертера — работает, но это не идиоматичный Kotlin -class UserDtoConverted { - var name: String? = null - var age: Int? = null - fun getDisplayName(): String { - return if (name != null) name!! else "аноним" - } -} - -// идиоматичная версия -data class UserDto(val name: String?, val age: Int?) { - val displayName: String get() = name ?: "аноним" -} - -fun main() { - println(UserDtoConverted().getDisplayName()) // аноним - println(UserDto(null, null).displayName) // аноним -} -``` - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | один язык, одна система типов | две системы, граница между ними | -> | `String` может быть `null` | `String` и `String?` различаются | -> | `List` один тип | `List` и `MutableList` | -> | `int` и `Integer` различаются | `Int` и `Int?` | -> | статические методы | `companion object` + `@JvmStatic` | -> | нет аналога | платформенные типы `String!` | -> | checked exceptions обязательны | проверяемых исключений нет | -> -> **Interop работает, потому что Kotlin компилируется в тот же байт-код.** Kotlin-класс - это обычный класс JVM: у него те же модификаторы доступа, то же наследование, тот же формат `.class`. Никакой прослойки, маршалинга или рефлексии в runtime нет, поэтому вызов Kotlin-метода из Java стоит ровно столько же, сколько вызов Java-метода. Это принципиально отличает ситуацию от interop между языками разных платформ (JNI, JNA), где границу приходится пересекать с накладными расходами. -> -> **Цена - необходимость представлять конструкции Kotlin средствами JVM.** В байт-коде нет ни nullable-типов, ни значений по умолчанию, ни функций-расширений. Kotlin эмулирует их: nullable-информацию хранит в аннотациях и метаданных, значения по умолчанию - в синтетическом методе `greet$default`, расширения - в статических методах. Из Kotlin это видно как родные конструкции (компилятор читает метаданные), из Java - как реализация. Подробнее: [Kotlin в байт-коде](Kotlin%20в%20байт-коде.md) -> -> **Проверяемые исключения - односторонняя потеря.** В Kotlin их нет: `throws IOException` из Java-метода не обязывает ни к `try/catch`, ни к объявлению. Это упрощает Kotlin-код, но Java-код, вызывающий Kotlin-метод, не узнает о возможном исключении без `@Throws`. Подробнее: [Система исключений](../Исключения/Система%20исключений.md) -> -> **Null-безопасность Kotlin не распространяется на Java-код.** Компилятор не может проверить то, чего нет в типах Java. Поэтому аннотации `@Nullable`/`@NotNull` в Java-коде - не документация, а реальная информация для компилятора Kotlin: с ними платформенные типы становятся обычными nullable/non-null. -> -> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md), [Байт-код](../../Программирование%20на%20Java/JVM/Байт-код.md) - -> [!warning] Подводные камни -> **Платформенные типы отключают проверки компилятора.** Значение из Java имеет тип `String!`, который компилятор позволяет использовать и как `String`, и как `String?`. Ошибка проявится как `NullPointerException` в рантайме, в точке присваивания: -> ```kotlin -> fun main() { -> val error = runCatching { -> val value: String = System.getProperty("нет.такого") // вернёт null -> value.length -> }.exceptionOrNull() -> println("NPE от платформенного типа: ${error is NullPointerException}") -> // NPE от платформенного типа: true -> } -> ``` -> -> **Автоконвертер даёт работающий, но плохой код.** Типичные следы: `!!` вместо `?.`, `var x: T? = null` вместо конструктора, геттеры вместо свойств, обычные классы вместо `data class`. После конвертации файл нужно переписывать вручную, иначе Kotlin-проект унаследует все недостатки Java-кода и не получит преимуществ. -> -> **Java-код может нарушить неизменяемость Kotlin-коллекции.** `List` из Kotlin приходит в Java как `java.util.List`, у которого есть `add()`. Вызов упадёт с `UnsupportedOperationException` в рантайме, а компилятор об этом не предупредит. Передавайте в Java копию (`toList()`) или `Collections.unmodifiableList`. -> -> **Kotlin-код без аннотаций неудобен из Java.** Значения по умолчанию, `companion object`, `object`, `suspend`-функции и функции верхнего уровня выглядят из Java не так, как задумано. Если API публичный, это нужно решать до релиза - добавление `@JvmStatic` позже меняет сигнатуры и ломает совместимость. -> -> **`internal` в Kotlin из Java доступен.** Он компилируется в `public` с манглированным именем (`internalFun$main`), поэтому Java-код в том же проекте технически может его вызвать. Это не защита, а соглашение. -> -> **Проверяемые исключения из Kotlin не видны Java.** Kotlin-метод, бросающий `IOException`, из Java выглядит как не бросающий ничего - `catch (IOException e)` вокруг него не скомпилируется («exception is never thrown»). Нужна аннотация `@Throws`. -> -> **Смешанная компиляция требует правильного плагина.** В Gradle нужен `kotlin("jvm")` и, при наличии Java-исходников, корректно настроенный `sourceSets`. Без этого Java-код не увидит Kotlin-классы, а ошибка выглядит как «cannot find symbol». -> -> **Generics Java стираются и в Kotlin.** `List` и `List` в рантайме неразличимы по обе стороны границы, поэтому проверка типа элементов коллекции из Java невозможна. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md) - -Далее: [Java из Kotlin](Java%20из%20Kotlin.md) diff --git a/Kotlin/Дженерики/Reified и inline.md b/Kotlin/Дженерики/Reified и inline.md deleted file mode 100644 index b462914..0000000 --- a/Kotlin/Дженерики/Reified и inline.md +++ /dev/null @@ -1,359 +0,0 @@ ---- -title: Reified и inline -description: Как reified обходит стирание типов, что компилятор делает с inline-функциями и чего reified не умеет -tags: - - kotlin - - generics ---- - -На JVM параметры типа стираются: в рантайме `List` и `List` - один класс. Но если функция объявлена `inline`, компилятор подставляет её тело в место вызова, где фактический тип **известен**. Это и позволяет `reified`. - -```kotlin -inline fun isOfType(value: Any): Boolean = value is T - -fun main() { - println(isOfType("текст")) // true - println(isOfType("текст")) // false - println(isOfType>(listOf(1))) // true -} -``` -Без `reified` строка `value is T` не компилируется - проверить стёртый тип невозможно. - -## Что делает компилятор -```kotlin -inline fun nameOf(): String = T::class.simpleName ?: "?" - -fun main() { - println(nameOf()) // String - println(nameOf()) // Int -} -``` -В байт-коде вызова `nameOf()` не остаётся: компилятор подставляет тело и заменяет `T` на `String`, получая буквально `String::class.simpleName`. Поэтому `reified` и требует `inline` - без подстановки подставлять нечего. - -Следствие: `reified`-функцию нельзя вызвать из Java и нельзя передать `T` динамически. - -## Главный случай: парсинг и десериализация -Задача, ради которой `reified` чаще всего и используют: -```kotlin -inline fun parseAs(raw: String): T? = when (T::class) { - Int::class -> raw.toIntOrNull() as T? - Long::class -> raw.toLongOrNull() as T? - Double::class -> raw.toDoubleOrNull() as T? - Boolean::class -> raw.toBooleanStrictOrNull() as T? - String::class -> raw as T - else -> null -} - -fun main() { - println(parseAs("42")) // 42 - println(parseAs("abc")) // null - println(parseAs("1.5")) // 1.5 - println(parseAs("true")) // true - println(parseAs("текст")) // текст -} -``` -В Java такая функция потребовала бы параметр `Class type` в сигнатуре. Именно это делают Jackson (`readValue(json, User.class)`) и Gson (`fromJson(json, User.class)`); Kotlin-обёртки над ними объявляют `inline fun readValue(json: String): T`, и тип выводится из контекста: -```kotlin -// С kotlinx.serialization или Jackson Kotlin module: -// val user: User = mapper.readValue(json) // тип берётся из объявления переменной -``` - -## filterIsInstance и прочее в stdlib -```kotlin -fun main() { - val mixed: List = listOf(1, "a", 2.0, 3, "b") - - println(mixed.filterIsInstance()) // [1, 3] - println(mixed.filterIsInstance()) // [a, b] - println(mixed.filterIsInstance()) // [1, 2.0, 3] - - val target = mutableListOf() - mixed.filterIsInstanceTo(target) - println(target) // [1, 3] -} -``` -`filterIsInstance` объявлена как `inline fun Iterable<*>.filterIsInstance(): List` - без `reified` поэлементная проверка типа была бы невозможна. - -Другие `reified`-функции stdlib: -```kotlin -fun main() { - val array = arrayOf(1, 2, 3) // arrayOf — reified - println(listOf("a", "b").toTypedArray().size) // 2 — тоже reified - println(array.javaClass.simpleName) // Integer[] - - val typed: Array = arrayOfNulls(2) - println(typed.size) // 2 - - println(emptyArray().size) // 0 -} -``` -`toTypedArray` без `reified` не смогла бы создать массив правильного типа - у `Array` в рантайме есть настоящий тип элементов. - -## Безопасное приведение -```kotlin -inline fun Any?.asOrNull(): T? = this as? T - -fun main() { - val value: Any = "текст" - println(value.asOrNull()?.length) // 5 - println(value.asOrNull()) // null — без исключения -} -``` -Это удобнее, чем `as? String` в каждом месте, когда тип приходит из параметра обобщённой функции. - -Разбор `Map` - типичная задача при работе с конфигами и JSON: -```kotlin -inline fun Map.getTyped(key: String): T? = this[key] as? T - -fun main() { - val config = mapOf("port" to 8080, "host" to "localhost", "tls" to true) - - println(config.getTyped("port")) // 8080 - println(config.getTyped("host")) // localhost - println(config.getTyped("host")) // null — тип не тот - println(config.getTyped("нет")) // null — ключа нет -} -``` - -## reified и Java-рефлексия -```kotlin -inline fun newInstance(): T = T::class.java.getDeclaredConstructor().newInstance() - -class Service2 { - override fun toString() = "сервис создан" -} - -fun main() { - val service: Service2 = newInstance() - println(service) // сервис создан -} -``` -`T::class` даёт `KClass`, `T::class.java` - `Class`. Это мост к любому Java API, требующему `Class`: -```kotlin -inline fun typeName(): String = T::class.java.name - -fun main() { - println(typeName()) // java.lang.String - println(typeName()) // java.lang.Integer — не int! - println(typeName>()) // java.util.List - println(Int::class.java.name) // int — а так примитив -} -``` -Две детали из этого примера: -- `T::class` знает только стёртый тип: `List` и `List` дадут одинаковый результат. -- Параметр типа всегда представлен ссылочным типом, поэтому `T = Int` даёт `java.lang.Integer`, хотя написанный напрямую `Int::class.java` возвращает примитивный `int`. - -## Что происходит с inline -`reified` - побочный эффект `inline`, а у `inline` есть своя основная задача: убрать объект лямбды. -```kotlin -inline fun measure(block: () -> Unit): Long { - val start = System.nanoTime() - block() - return System.nanoTime() - start -} - -fun main() { - val time = measure { (1..1000).sum() } - println(time > 0) // true -} -``` -Тело `block` подставляется прямо в `main` - объекта `Function0` не создаётся. Подробнее об этом и о `noinline`/`crossinline`: [Inline-функции](../Функциональный%20Kotlin/Inline-функции.md) - -Для `reified` важно только одно следствие inline: **функция перестаёт существовать как метод**, поэтому её нельзя вызвать не из Kotlin и нельзя взять на неё ссылку. - -## Ограничения reified -```kotlin -inline fun demo(): String { - val cls = T::class // можно - val arr = arrayOfNulls(1) // можно - // val instance = T() // нельзя: нет конструктора в типе - // val companion = T.Companion // нельзя - return "${cls.simpleName} ${arr.size}" -} - -fun main() { - println(demo()) // String 1 -} -``` - -| Можно с `reified T` | Нельзя | -|---|---| -| `value is T`, `value as? T` | `T()` - создать экземпляр | -| `T::class`, `T::class.java` | обращаться к статическим членам `T` | -| `arrayOfNulls()`, `arrayOf()` | различать `List` и `List` | -| передать `T` в другую `reified`-функцию | вызвать из Java | -| вывести `T` из типа переменной | передать тип как значение в рантайме | - -Создание экземпляра требует рефлексии: -```kotlin -inline fun createOrNull(): T? = runCatching { - T::class.java.getDeclaredConstructor().newInstance() -}.getOrNull() - -class WithDefault { override fun toString() = "создан" } -class WithoutDefault(val x: Int) - -fun main() { - println(createOrNull()) // создан - println(createOrNull()) // null — нет пустого конструктора -} -``` - -## Проброс reified дальше -```kotlin -inline fun logType(value: Any) { - println("${T::class.simpleName}: ${check2(value)}") -} - -inline fun check2(value: Any): Boolean = value is T - -fun main() { - logType("текст") // String: true - logType("текст") // Int: false -} -``` -`reified` передаётся в другие `inline reified`-функции - каждая подстановка сохраняет фактический тип. А вот в обычную обобщённую функцию - нет: -```kotlin -fun notReified(value: Any): Boolean = value != null // T здесь стёрт - -inline fun caller(value: Any): Boolean = notReified(value) - -fun main() { - println(caller("текст")) // true — но проверки типа внутри нет -} -``` - -## Альтернатива без reified -Когда inline невозможен (рекурсия, большая функция, вызов из Java), тип передают явно: -```kotlin -fun parseWith(raw: String, type: kotlin.reflect.KClass): T? = when (type) { - Int::class -> raw.toIntOrNull() as T? - String::class -> raw as T - else -> null -} - -fun main() { - println(parseWith("42", Int::class)) // 42 - println(parseWith("текст", String::class)) // текст -} -``` -Это ровно Java-подход с `Class`. Он многословнее в вызове, зато функция остаётся обычным методом. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | ` T parse(String s, Class type)` | `inline fun parse(s: String): T` | -> | `parse(json, User.class)` | `parse(json)` или `val u: User = parse(json)` | -> | `x instanceof T` - невозможно | `x is T` с `reified` | -> | `new T[]` - запрещён | `arrayOf()` с `reified` | -> | `T.class` - невозможно | `T::class` с `reified` | -> | нет inline-функций | `inline` + подстановка на этапе компиляции | -> -> **`reified` устраняет параметр-токен типа.** Это самая заметная разница в API. Jackson в Java: -> ```java -> User user = mapper.readValue(json, User.class); -> List users = mapper.readValue(json, new TypeReference>() {}); -> ``` -> Kotlin-модуль того же Jackson: -> ```kotlin -> // val user: User = mapper.readValue(json) -> // val users: List = mapper.readValue(json) -> ``` -> Второй случай особенно показателен: в Java для обобщённого типа приходится создавать анонимный класс `TypeReference`, чтобы тип сохранился в сигнатуре суперкласса - единственное место, где Java не стирает дженерики. Kotlin обходится выводом типа из переменной. -> -> **Стирание никуда не делось.** `reified` работает только потому, что тип подставляется при компиляции. Поэтому `reified T` **не** даёт настоящей информации об аргументах типа: -> ```kotlin -> inline fun describeType(): String = T::class.simpleName ?: "?" -> -> fun main() { -> println(describeType>()) // List — параметр потерян -> println(describeType>()) // List — то же самое -> } -> ``` -> Для полного типа с аргументами нужен `typeOf()` из `kotlin.reflect`, и он требует зависимости `kotlin-reflect`. -> -> **Inline-функций в Java нет.** JVM инлайнит методы на уровне JIT в рантайме, но это оптимизация, невидимая системе типов. Kotlin инлайнит на этапе компиляции, что даёт две вещи сразу: отсутствие объекта лямбды и `reified`. Цена - рост размера байт-кода и невозможность вызвать такую функцию из Java. -> -> **`filterIsInstance` аналога в Java не имеет.** Эквивалент - `stream().filter(X.class::isInstance).map(X.class::cast)`, где класс указывается дважды. -> -> Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md), [Reflection API в Java](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md) - -> [!warning] Подводные камни -> **`reified` без `inline` не компилируется** - это не выбор, а требование. Если функция не может быть inline (рекурсивная, очень большая, публичное API для Java), придётся передавать `KClass` вручную. -> -> **Тип всё равно стёрт внутри аргументов:** -> ```kotlin -> inline fun isList(value: Any): Boolean = value is T -> -> fun main() { -> println(isList>(listOf(1, 2))) // true — хотя это List! -> } -> ``` -> `is List` в подставленном коде превращается в `is List<*>`. Это молчаливая ошибка: проверка выглядит строгой, но не различает аргументы типа. -> -> **`as T` внутри reified-функции не всегда проверяется:** -> ```kotlin -> inline fun forceCast(value: Any): T = value as T -> -> fun main() { -> val list: List = forceCast(listOf(1, 2)) // прошло! -> println(list.size) // 2 -> try { -> println(list[0].length) -> } catch (e: ClassCastException) { -> println("упало только при обращении к элементу") -> } -> } -> ``` -> Каст к `List` сводится к касту к `List`, который успешен. Ошибка всплывает позже. -> -> **Inline раздувает байт-код.** Тело функции копируется в каждое место вызова. Для большой функции, вызываемой из сотни мест, это заметный рост размера класса - и потеря выигрыша от кеша инструкций процессора. `reified` оправдывает inline только для коротких функций. -> -> **Inline-функцию нельзя вызвать из Java:** -> ```kotlin -> inline fun kotlinOnly(): String = T::class.simpleName ?: "?" -> -> fun main() { println(kotlinOnly()) } // Int -> ``` -> Java-код не умеет подставлять тело и не знает про `reified`. Для API, доступного из Java, нужна обычная функция с `Class`. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md) -> -> **`T()` не работает** - у параметра типа нет конструктора. Варианты: передать фабрику `() -> T` параметром или использовать рефлексию, которая упадёт при отсутствии пустого конструктора. -> -> **Ссылку на inline-функцию взять нельзя:** -> ```kotlin -> inline fun typeNameOf(): String = T::class.simpleName ?: "?" -> -> fun main() { -> // val ref = ::typeNameOf // ошибка: нельзя взять ссылку -> println(typeNameOf()) // Int -> } -> ``` -> -> **`reified` требует, чтобы тип был известен в месте вызова.** Передать тип из переменной нельзя: -> ```kotlin -> inline fun show2(value: Any) = println(value is T) -> -> fun wrapper(value: Any) { -> // show2(value) // ошибка: U не reified -> show2(value) -> } -> -> fun main() { wrapper("x") } // true -> ``` -> Цепочка `reified` обрывается на первой обычной обобщённой функции. -> -> **`T::class.java` и `Int::class.java` дают разные классы:** -> ```kotlin -> inline fun jvmName(): String = T::class.java.name -> -> fun main() { -> println(jvmName()) // java.lang.Integer -> println(Int::class.java.name) // int -> println(Int::class.javaObjectType.name) // java.lang.Integer -> } -> ``` -> Через параметр типа всегда получается обёртка - примитив не может быть аргументом типа. Написанный напрямую `Int::class.java` даёт примитивный `int`, и если результат передаётся в Java-API, различие имеет значение: `Integer.class` и `int.class` - разные объекты, и, например, `getDeclaredMethod` по ним найдёт разные методы. Явные формы - `javaPrimitiveType` и `javaObjectType`. Подробнее: [Платформенные типы](../Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md) -> -> **Изменение тела inline-функции требует перекомпиляции всех вызывающих.** Если функция опубликована в библиотеке, клиенты, собранные со старой версией, продолжат исполнять старое тело - оно физически скопировано в их байт-код. Для публичных API это аргумент против inline. - -Далее: [Функции высшего порядка](../Функциональный%20Kotlin/Функции%20высшего%20порядка.md) diff --git a/Kotlin/Дженерики/Вариантность.md b/Kotlin/Дженерики/Вариантность.md deleted file mode 100644 index 6a087a3..0000000 --- a/Kotlin/Дженерики/Вариантность.md +++ /dev/null @@ -1,351 +0,0 @@ ---- -title: Вариантность -description: out и in в объявлении класса, ковариантность, контравариантность и правило PECS -tags: - - kotlin - - generics ---- - -Вариантность отвечает на один вопрос: если `String` - подтип `Any`, то является ли `Box` подтипом `Box`? - -По умолчанию - **нет**. Дженерики инвариантны: -```kotlin -class Box6(val value: T) - -fun main() { - val str: Box6 = Box6("текст") - // val any: Box6 = str // ошибка: несовместимые типы - println(str.value) // текст -} -``` - -Причина видна, если допустить обратное на изменяемой коллекции: -```kotlin -fun main() { - val strings = mutableListOf("a", "b") - // val anys: MutableList = strings // если бы компилировалось... - // anys.add(42) // ...сюда положили бы Int - // val s: String = strings[0] // ...и здесь получили бы ClassCastException - println(strings.size) // 2 -} -``` -Запрет инвариантности спасает от этой дыры. Но он же мешает там, где изменения нет. - -## out - ковариантность -`out T` означает, что `T` встречается только **на выходе**: в возвращаемых значениях, но не в параметрах. - -```kotlin -class Producer(private val value: T) { - fun get(): T = value // T на выходе — разрешено - // fun set(v: T) { } // ошибка: T на входе запрещён -} - -fun main() { - val strings: Producer = Producer("текст") - val anys: Producer = strings // работает! - println(anys.get()) // текст -} -``` -Это безопасно: из `Producer` можно только читать, а всякая прочитанная `String` является `Any`. - -Именно так объявлен `List`: -```kotlin -fun printSize(items: List) = items.size - -fun main() { - println(printSize(listOf("a", "b"))) // 2 - println(printSize(listOf(1, 2, 3))) // 3 - - val numbers: List = listOf(1, 2.0, 3L) - println(numbers.sumOf { it.toDouble() }) // 6.0 -} -``` -В stdlib: `List`, `Set`, `Map`, `Iterable`, `Sequence`, `Pair`. - -## in - контравариантность -`in T` означает, что `T` встречается только **на входе**: в параметрах, но не в возвращаемых типах. - -```kotlin -class Consumer { - fun consume(value: T) { println("принял $value") } - // fun produce(): T = TODO() // ошибка: T на выходе запрещён -} - -fun main() { - val anyConsumer: Consumer = Consumer() - val stringConsumer: Consumer = anyConsumer // работает! - stringConsumer.consume("текст") // принял текст -} -``` -Направление перевёрнуто: `Consumer` является подтипом `Consumer`. Логика простая - тот, кто умеет принимать любой `Any`, справится и со `String`. - -Классический пример - `Comparator`: -```kotlin -fun main() { - val anyComparator = Comparator { a, b -> a.hashCode() - b.hashCode() } - val strings = mutableListOf("bb", "a") - strings.sortWith(anyComparator) // Comparator для List - println(strings.size) // 2 - - val byLength: Comparator = compareBy { it.length } - println(listOf("ccc", "a", "bb").sortedWith(byLength)) // [a, bb, ccc] -} -``` -`sortedWith` принимает `Comparator`, поэтому компаратор по `CharSequence` годится для `List`. - -В stdlib: `Comparator`, `Comparable`, функциональные типы `(in P) -> out R`. - -## Сводка -| Объявление | Называется | `T` может быть | Направление подтипов | -|---|---|---|---| -| `Box` | инвариантность | везде | нет связи | -| `Box` | ковариантность | только возврат | `Box` -> `Box` | -| `Box` | контравариантность | только параметр | `Box` -> `Box` | - -```mermaid -graph TD - subgraph co["out T: ковариантность"] - CS["Producer<String>"] --> CA["Producer<Any>"] - end - subgraph contra["in T: контравариантность"] - KA["Consumer<Any>"] --> KS["Consumer<String>"] - end - subgraph inv["T: инвариантность"] - IS["Box<String>"] - IA["Box<Any>"] - end -``` - -## Функциональные типы -Вариантность функциональных типов - самый частый случай, с которым работают, не замечая этого: -```kotlin -fun main() { - val produceString: () -> String = { "текст" } - val produceAny: () -> Any = produceString // возврат ковариантен - println(produceAny()) // текст - - val acceptAny: (Any) -> Unit = { println("принял $it") } - val acceptString: (String) -> Unit = acceptAny // параметр контравариантен - acceptString("строка") // принял строка -} -``` -Тип `(P) -> R` объявлен как `Function1`: функция, принимающая больше и возвращающая меньше, подходит везде, где ожидается более узкая. - -Практическое следствие для API: -```kotlin -class EventBus { - private val handlers = mutableListOf<(Any) -> Unit>() - - fun subscribe(handler: (Any) -> Unit) { handlers.add(handler) } - fun publish(event: Any) = handlers.forEach { it(event) } -} - -fun main() { - val bus = EventBus() - bus.subscribe { println("получено: $it") } - bus.publish("событие") // получено: событие - bus.publish(42) // получено: 42 -} -``` - -## Когда помечать свой класс -Правило простое: если параметр типа используется **только** на выходе - ставьте `out`, только на входе - `in`. Компилятор проверит и подскажет. - -```kotlin -interface Reader { - fun read(): T - fun readAll(count: Int): List // List — тоже выход, ок -} - -interface Writer { - fun write(value: T) - fun writeAll(values: List) // List на входе, ок -} - -interface Storage : Reader, Writer // и то и другое — инвариантен - -class MemoryStorage(private var value: T) : Storage { - override fun read(): T = value - override fun readAll(count: Int) = List(count) { value } - override fun write(value: T) { this.value = value } - override fun writeAll(values: List) { values.lastOrNull()?.let { value = it } } -} - -fun main() { - val storage = MemoryStorage("начальное") - val reader: Reader = storage // ковариантность работает - storage.write("новое") - println(reader.read()) // новое - println(reader.readAll(2)) // [новое, новое] -} -``` -Разделение на `Reader` и `Writer` - типичный приём: инвариантный класс реализует оба интерфейса, а клиентам отдаётся вариантный. - -## Nothing и вариантность -`Nothing` - подтип всех типов, поэтому `List` подходит под любой `List`: -```kotlin -fun main() { - val empty: List = emptyList() - val ints: List = empty // работает благодаря out - val strings: List = empty - println("${ints.size} ${strings.size}") // 0 0 -} -``` -Именно поэтому `emptyList()` один на все типы - это синглтон `EmptyList : List`. Подробнее: [Any, Unit, Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md) - -Тот же приём в sealed-иерархиях: -```kotlin -sealed interface Outcome { - data class Ok(val value: T) : Outcome - data object Empty : Outcome -} - -fun describe4(o: Outcome): String = when (o) { - is Outcome.Ok -> "значение ${o.value}" - Outcome.Empty -> "пусто" -} - -fun main() { - println(describe4(Outcome.Ok(1))) // значение 1 - println(describe4(Outcome.Empty)) // пусто -} -``` -`Empty` может быть `object`, а не обобщённым классом, только потому что `Outcome` ковариантен. Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md) - -## Ограничение out: нельзя принимать T -Иногда ковариантный класс всё-таки должен принять `T`. Для этого параметр объявляют с более широкой границей: -```kotlin -class ImmutableBox(private val value: T) { - fun get(): T = value - - fun withDefault(default: R): R where R : Any = default - - fun plus(other: U): List = listOf(value, other) -} - -fun main() { - val box: ImmutableBox = ImmutableBox("a") - val any: ImmutableBox = box - println(any.get()) // a - println(box.plus(1)) // [a, 1] -} -``` -В stdlib этот приём виден в `List.plus`: -```kotlin -fun main() { - val strings: List = listOf("a") - val mixed: List = strings + 1 // plus объявлен как List.plus(element: T) - println(mixed) // [a, 1] -} -``` -Тип `T` здесь выводится как `Any`, а не как `String`, поэтому ковариантность не нарушается. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `List` в каждой сигнатуре | `List` один раз в объявлении | -> | `List` | `MutableList` или `in` в объявлении | -> | `Collection` | `Collection<*>` | -> | нет declaration-site variance | `out`/`in` при объявлении класса | -> | PECS - правило для программиста | компилятор проверяет сам | -> -> **Главное отличие - где объявляется вариантность.** Java знает только use-site variance: интерфейс `List` инвариантен, и каждый метод, принимающий «список чего-то производного», обязан писать wildcard: -> ```java -> void printAll(Collection items) { ... } -> double sum(List numbers) { ... } -> void addAll(List dest, List src) { ... } -> ``` -> В Kotlin `List` объявлен один раз в stdlib, и дальше всё работает само: -> ```kotlin -> fun sum3(numbers: List) = numbers.sumOf { it.toDouble() } -> -> fun main() { -> println(sum3(listOf(1, 2, 3))) // 6.0 — List подошёл -> } -> ``` -> Это убирает wildcard из подавляющего большинства сигнатур. Use-site variance в Kotlin тоже есть - это [проекции типов](Проекции%20типов.md), но нужны они редко. -> -> **PECS из правила превратился в проверку компилятора.** В Java «Producer Extends, Consumer Super» - мнемоника, которую программист применяет вручную; забыл - и метод неудобен в использовании, но компилируется. В Kotlin вы помечаете класс `out`, и компилятор откажется принять `T` в параметр: ошибка возникает при объявлении, а не у пользователя API. -> -> **`Comparator` в Kotlin контравариантен автоматически.** В Java `Comparator` инвариантен, и методы вроде `Collections.sort` объявляют `Comparator`. В Kotlin stdlib `Comparator` уже `in T`, поэтому `sortedWith(comparator)` принимает компаратор по любому супертипу без дополнительных обозначений. -> -> **Разделение `List`/`MutableList` делает `out` возможным.** В Java `List` содержит `add`, поэтому ковариантным он быть не может в принципе - только через wildcard в месте использования. Kotlin вынес мутаторы в `MutableList` (инвариантный), и `List` стал безопасным по построению. -> -> Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md), [Полиморфизм](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм.md) - -> [!warning] Подводные камни -> **`out` запрещает `T` в параметрах - включая неочевидные места:** -> ```kotlin -> class Holder(private val value: T) { -> fun get(): T = value -> // fun contains(other: T): Boolean = value == other // ошибка -> fun contains(other: Any?): Boolean = value == other // так можно -> } -> -> fun main() { -> println(Holder("a").contains("a")) // true -> } -> ``` -> Расширение границы до `Any?` - стандартный обход, но оно теряет проверку типов: `contains(42)` тоже скомпилируется. -> -> **`private` функции могут нарушать вариантность:** -> ```kotlin -> class Mutable(private var value: T) { -> fun get(): T = value -> private fun set(v: T) { value = v } // private — проверка не применяется -> } -> -> fun main() { println(Mutable("a").get()) } // a -> ``` -> Компилятор разрешает `T` на входе в `private` членах, потому что снаружи они недоступны. Но `private var value: T` - всё ещё изменяемое состояние; если его изменить изнутри класса, ковариантный «снимок» окажется нестабильным. -> -> **`MutableList` инвариантен, и это не ошибка:** -> ```kotlin -> fun main() { -> val strings = mutableListOf("a") -> // val anys: MutableList = strings // не скомпилируется — и правильно -> val readOnly: List = strings // а так можно -> println(readOnly.size) // 1 -> } -> ``` -> Если нужно передать изменяемую коллекцию «как список чего угодно» - передавайте `List`, а не `MutableList`. -> -> **Массивы инвариантны в Kotlin и ковариантны в Java:** -> ```kotlin -> fun main() { -> val strings = arrayOf("a", "b") -> // val anys: Array = strings // ошибка компиляции -> println(strings.size) // 2 -> } -> ``` -> В Java `String[]` является `Object[]`, поэтому `Object[] o = new String[1]; o[0] = 1;` компилируется и бросает `ArrayStoreException` в рантайме. Kotlin закрыл эту дыру ценой того, что `Array` не принимает `Array`. Для чтения используйте `Array`. -> -> **Вариантность не распространяется на вложенные параметры автоматически:** -> ```kotlin -> fun main() { -> val nested: List> = listOf(listOf("a")) -> val widened: List> = nested // работает: оба List ковариантны -> println(widened.size) // 1 -> -> val maps: List> = listOf(mutableListOf("a")) -> // val wide: List> = maps // не работает: MutableList инвариантен -> println(maps.size) // 1 -> } -> ``` -> -> **`in` и `out` легко перепутать направление.** Мнемоника: `out` - значение **выходит** из объекта (producer, можно только читать), `in` - значение **входит** в объект (consumer, можно только писать). Если нужны обе операции, вариантность невозможна. -> -> **`Map` ковариантен только по значению:** -> ```kotlin -> fun main() { -> val m: Map = mapOf("a" to 1) -> val widened: Map = m // ок — V ковариантен -> // val keys: Map = m // ошибка — K инвариантен -> println(widened.size) // 1 -> } -> ``` -> Ключ инвариантен, потому что он используется на входе в `get(key)`. -> -> **Ковариантный класс с `var`-полем типа `T` объявить нельзя** - сеттер принимает `T`. Если нужно изменяемое состояние, либо инвариантность, либо приватный сеттер с осознанием последствий. - -Далее: [Проекции типов](Проекции%20типов.md) diff --git a/Kotlin/Дженерики/Параметрический полиморфизм.md b/Kotlin/Дженерики/Параметрический полиморфизм.md deleted file mode 100644 index 773cc51..0000000 --- a/Kotlin/Дженерики/Параметрический полиморфизм.md +++ /dev/null @@ -1,393 +0,0 @@ ---- -title: Параметрический полиморфизм -description: Обобщённые функции и классы, ограничения параметров типа и вывод типов -tags: - - kotlin - - generics ---- - -Дженерики позволяют написать код один раз для любого типа, сохранив проверку типов при компиляции. - -```kotlin -fun firstOrDefault(items: List, default: T): T = - if (items.isEmpty()) default else items[0] - -fun main() { - println(firstOrDefault(listOf(1, 2), 0)) // 1 - println(firstOrDefault(emptyList(), "нет")) // нет - // println(firstOrDefault(listOf(1), "нет")) // ошибка: T не может быть и Int, и String -} -``` -`` объявляется **перед** именем функции. Компилятор выводит его из аргументов. - -## Обобщённые классы -```kotlin -class Box5(private val value: T) { - fun get(): T = value - fun map(f: (T) -> R): Box5 = Box5(f(value)) - override fun toString() = "Box($value)" -} - -fun main() { - val intBox = Box5(42) - println(intBox.get() + 1) // 43 - println(intBox.map { it.toString() + "!" }) // Box(42!) - - val explicit = Box5("явно") - println(explicit.get().length) // 4 -} -``` -Метод `map` объявляет **свой** параметр типа `R` - он не связан с `T` класса. - -Несколько параметров: -```kotlin -class Pair2(val first: A, val second: B) { - fun swap(): Pair2 = Pair2(second, first) - override fun toString() = "($first, $second)" -} - -fun main() { - val p = Pair2("ключ", 1) - println(p) // (ключ, 1) - println(p.swap()) // (1, ключ) -} -``` - -## Ограничения параметра типа -Без ограничения у `T` доступны только методы `Any?`: -```kotlin -fun describe(value: T): String = "значение: $value" // только toString - -fun > maxOf3(a: T, b: T): T = if (a > b) a else b - -fun sum2(items: List): Double = items.sumOf { it.toDouble() } - -fun main() { - println(describe(listOf(1))) // значение: [1] - println(maxOf3(3, 7)) // 7 - println(maxOf3("abc", "abd")) // abd - println(sum2(listOf(1, 2.5, 3L))) // 6.5 -} -``` -Верхняя граница после двоеточия говорит компилятору, какие методы доступны. - -По умолчанию граница - `Any?`, то есть `T` может быть nullable: -```kotlin -fun firstOrNull2(items: List): T? = items.firstOrNull() - -fun requireFirst(items: List): T = items.first() - -fun main() { - println(firstOrNull2(listOf(null))) // null - println(requireFirst(listOf(1, 2))) // 1 -} -``` -`T : Any` - способ запретить nullable-аргумент типа. - -### Несколько ограничений -```kotlin -interface Named4 { val name: String } -interface Aged { val age: Int } - -fun describe2(value: T): String where T : Named4, T : Aged = - "${value.name}, ${value.age}" - -data class Person2(override val name: String, override val age: Int) : Named4, Aged - -fun main() { - println(describe2(Person2("Иван", 30))) // Иван, 30 -} -``` -Больше одного ограничения задаётся через `where` после сигнатуры. - -## Обобщённые функции-расширения -```kotlin -fun List.secondOrNull(): T? = if (size >= 2) this[1] else null - -fun > List.isSorted(): Boolean = - zipWithNext().all { (a, b) -> a <= b } - -fun main() { - println(listOf(1, 2, 3).secondOrNull()) // 2 - println(listOf(1).secondOrNull()) // null - println(listOf(1, 2, 3).isSorted()) // true - println(listOf(3, 1).isSorted()) // false -} -``` -Это основной способ добавить обобщённую операцию к существующему типу. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md) - -## Вывод типов -```kotlin -fun main() { - val a = listOf(1, 2) // List - val b = listOf(1, "a") // List - val c = listOf(1, 2.0) // List — указали явно - val d = mutableListOf() // без аргументов тип не вывести - val e = emptyMap() - - println("${a::class.simpleName} ${b.size} ${c.size} ${d.size} ${e.size}") - // ArrayList 2 2 0 0 -} -``` -Когда аргументов нет или нужен более широкий тип, параметр указывают явно. - -Вывод работает и через возвращаемое значение: -```kotlin -class Box7(private val value: T?) { - fun get(): T? = value -} - -fun emptyBox(): Box7 = Box7(null) - -fun main() { - val box: Box7 = emptyBox() // T выведен из типа переменной - println(box.get()) // null -} -``` - -## Обобщённые интерфейсы -```kotlin -interface Repository6 { - fun find(id: ID): T? - fun save(item: T): ID - fun all(): List -} - -data class User9(val id: Int, val name: String) - -class InMemoryUserRepository : Repository6 { - private val storage = mutableMapOf() - - override fun find(id: Int) = storage[id] - override fun save(item: User9): Int { storage[item.id] = item; return item.id } - override fun all() = storage.values.toList() -} - -fun main() { - val repo = InMemoryUserRepository() - repo.save(User9(1, "Иван")) - println(repo.find(1)?.name) // Иван - println(repo.find(2)) // null - println(repo.all().size) // 1 -} -``` -Реализация может зафиксировать параметры типа (как здесь) или оставить их обобщёнными: -```kotlin -interface Repository7 { - fun find(id: ID): T? - fun save(item: T): ID - fun all(): List -} - -data class User10(val id: Int, val name: String) - -class InMemoryRepository( - private val idOf: (T) -> ID, -) : Repository7 { - private val storage = mutableMapOf() - - override fun find(id: ID) = storage[id] - override fun save(item: T): ID { - val id = idOf(item) - storage[id] = item - return id - } - override fun all() = storage.values.toList() -} - -fun main() { - val repo = InMemoryRepository { it.id } - repo.save(User10(1, "Иван")) - println(repo.find(1)?.name) // Иван -} -``` - -## Стирание типов -Параметры типа существуют только при компиляции. В рантайме их нет: -```kotlin -fun main() { - val ints = listOf(1, 2) - val strings = listOf("a", "b") - - println(ints.javaClass == strings.javaClass) // true — один класс - // println(ints is List) // ошибка: нельзя проверить - println(ints is List<*>) // true — только «какой-то List» -} -``` -Это наследие JVM: дженерики появились в Java 5 и реализованы стиранием ради совместимости. Следствия: - -```kotlin -fun main() { - // 1. Нельзя создать массив параметра типа напрямую - // fun makeArray(): Array = arrayOf() // ошибка - - // 2. Нельзя перегрузить по параметру типа - // fun show(x: List) = "строки" - // fun show(x: List) = "числа" // та же JVM-сигнатура - - // 3. Непроверяемый каст компилируется с предупреждением - val any: Any = listOf(1, 2) - @Suppress("UNCHECKED_CAST") - val back = any as List // не упадёт здесь - try { - println(back[0].length) // упадёт здесь - } catch (e: ClassCastException) { - println("ClassCastException при обращении") - } -} -``` -Обойти стирание в Kotlin можно для inline-функций через `reified`. Подробнее: [Reified и inline](Reified%20и%20inline.md) - -## Параметры типа и значения по умолчанию -```kotlin -class Cache( - private val maxSize: Int = 100, - private val storage: MutableMap = LinkedHashMap(), -) { - fun put(key: K, value: V) { - if (storage.size >= maxSize) storage.remove(storage.keys.first()) - storage[key] = value - } - - fun get(key: K): V? = storage[key] - val size: Int get() = storage.size -} - -fun main() { - val cache = Cache(maxSize = 2) - cache.put("a", 1) - cache.put("b", 2) - cache.put("c", 3) // "a" вытеснен - println("${cache.get("a")} ${cache.get("c")} ${cache.size}") // null 3 2 -} -``` - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | ` T first(List x)` | `fun first(x: List): T` | -> | `` | `` | -> | `` | `where T : A, T : B` | -> | `new Box<>(1)` (ромб) | `Box(1)` - вывод | -> | `List` | `List<*>` | -> | `List` | `List` | -> | `List` | `List` | -> | стирание типов | стирание типов (то же) | -> | нет аналога | `reified` в inline-функциях | -> -> **Синтаксис почти совпадает** - это сознательное решение, чтобы перенос кода был механическим. Два отличия: двоеточие вместо `extends` и `where` для нескольких границ. -> -> **Вариантность объявляется в месте определения.** В Java `List` пишется каждый раз в сигнатуре метода (use-site variance). В Kotlin `List` объявлено один раз в самом интерфейсе, поэтому `List` **является** `List` автоматически: -> ```kotlin -> fun printAll(items: List) = items.size -> -> fun main() { -> println(printAll(listOf("a", "b"))) // 2, без всяких ? extends -> } -> ``` -> В Java пришлось бы объявить параметр как `List`. Подробнее: [Вариантность](Вариантность.md) -> -> **`reified` решает главную проблему стирания.** В Java чтобы узнать тип в рантайме, передают `Class`: -> ```java -> T parse(String json, Class type) { ... } -> parse(json, User.class); -> ``` -> В Kotlin inline-функция может объявить `reified T` и использовать `T::class` напрямую. Это убирает параметр-костыль из сигнатуры. -> -> **Массивы дженериков одинаково проблемны.** `new T[]` запрещён в Java, `arrayOf()` - в Kotlin. Обход - `Array` с кастом или `ArrayList`. -> -> Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md), [Полиморфизм](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм.md) - -> [!warning] Подводные камни -> **Типы стираются - проверить `is List` нельзя:** -> ```kotlin -> fun describe3(x: Any): String = when (x) { -> is List<*> -> "список из ${x.size}" // только так -> is Map<*, *> -> "карта" -> else -> "другое" -> } -> -> fun main() { -> println(describe3(listOf(1, 2))) // список из 2 -> println(describe3(listOf("a"))) // список из 2? нет — список из 1 -> } -> ``` -> Отличить `List` от `List` в рантайме невозможно. -> -> **Непроверяемый каст откладывает ошибку:** -> ```kotlin -> @Suppress("UNCHECKED_CAST") -> fun castAll(items: List): List = items as List -> -> fun main() { -> val wrong: List = castAll(listOf(1, 2)) -> println(wrong.size) // 2 — каст «прошёл» -> try { -> println(wrong[0].length) -> } catch (e: ClassCastException) { -> println("упало только при использовании") -> } -> } -> ``` -> `ClassCastException` возникает не в месте каста, а там, где значение используется - отлаживать это тяжело. -> -> **Параметр типа без ограничения почти бесполезен внутри функции.** У `T` доступны только `toString`, `equals`, `hashCode`. Если нужны операции - нужна граница. -> -> **`T` по умолчанию nullable:** -> ```kotlin -> fun process3(value: T): String = value.toString() -> -> fun main() { -> println(process3(null)) // null — T выведен как Nothing? -> } -> ``` -> Чтобы запретить это, нужно ``. -> -> **Перегрузка по параметру типа невозможна** - после стирания сигнатуры совпадают. Обход - `@JvmName`: -> ```kotlin -> @JvmName("sumInts") -> fun sumAll(items: List) = items.sum() -> fun sumAll(items: List) = items.sumOf { it.length } -> -> fun main() { println(sumAll(listOf("ab", "c"))) } // 3 -> ``` -> -> **Массив параметра типа создать нельзя:** -> ```kotlin -> class Buffer(size: Int) { -> @Suppress("UNCHECKED_CAST") -> private val items = arrayOfNulls(size) as Array -> operator fun set(i: Int, v: T) { items[i] = v } -> operator fun get(i: Int): T? = items[i] -> } -> -> fun main() { -> val b = Buffer(2) -> b[0] = "a" -> println(b[0]) // a -> } -> ``` -> Это стандартный обход, он безопасен, пока массив не утекает наружу как `Array`. -> -> **Вывод типа может дать неожиданно широкий тип:** -> ```kotlin -> fun main() { -> val mixed = listOf(1, "a") // List -> val nullable = listOf(1, null) // List -> println("${mixed.size} ${nullable.size}") // 2 2 -> // mixed[0] + 1 // ошибка: Any не число -> } -> ``` -> -> **Ограничение `T : Comparable` не покрывает смешанные типы.** `maxOf3(1, 2.0)` не скомпилируется, потому что `Int` и `Double` не сравнимы между собой. -> -> **Обобщённое исключение объявить нельзя:** -> ```kotlin -> // class MyException : Exception() // ошибка: generic class cannot extend Throwable -> class MyException(val payload: Any?) : Exception() -> -> fun main() { println(MyException(1).payload) } // 1 -> ``` -> Причина - стирание: `catch` не смог бы различить типы параметров. - -Далее: [Вариантность](Вариантность.md) diff --git a/Kotlin/Дженерики/Проекции типов.md b/Kotlin/Дженерики/Проекции типов.md deleted file mode 100644 index 1629565..0000000 --- a/Kotlin/Дженерики/Проекции типов.md +++ /dev/null @@ -1,331 +0,0 @@ ---- -title: Проекции типов -description: Вариантность в месте использования, star-проекция и ограничения, которые она накладывает -tags: - - kotlin - - generics ---- - -[Вариантность](Вариантность.md) объявляется при определении класса. Но инвариантный класс изменить нельзя - ни свой `MutableList`, ни чужой из библиотеки. Для этого существуют проекции: вариантность в **месте использования**. - -```kotlin -fun copy(from: MutableList, to: MutableList) { - for (item in from) to.add(item) -} - -fun main() { - val source = mutableListOf("a", "b") - val target = mutableListOf(1) - copy(source, target) - println(target) // [1, a, b] -} -``` -Без `out` в сигнатуре `MutableList` не подошёл бы под `MutableList`. С `out` компилятор принимает любой `MutableList`, где `T` - подтип `Any`, но взамен запрещает использовать `from` на запись. - -## out-проекция -```kotlin -fun sumAll2(numbers: MutableList): Double { - // numbers.add(1) // ошибка: add принимает Nothing - return numbers.sumOf { it.toDouble() } -} - -fun main() { - println(sumAll2(mutableListOf(1, 2, 3))) // 6.0 - println(sumAll2(mutableListOf(1.5, 2.5))) // 4.0 -} -``` -Внутри функции `MutableList` ведёт себя как `List`: читать можно, писать нельзя. Формально тип параметра `add` становится `Nothing` - значения такого типа не существует, поэтому вызвать его невозможно. - -## in-проекция -Зеркальный случай - нужно только писать: -```kotlin -fun fillWithOnes(target: MutableList) { - target.add(1) - target.add(2) - // val x: Int = target[0] // ошибка: читается как Any? - val any: Any? = target[0] - println("первый: $any") -} - -fun main() { - val numbers = mutableListOf(0.5) - fillWithOnes(numbers) - println(numbers) // [0.5, 1, 2] - - val anys = mutableListOf("x") - fillWithOnes(anys) - println(anys) // [x, 1, 2] -} -``` -`MutableList` принимает `MutableList`, `MutableList`, `MutableList` - любой список, в который `Int` поместится. Чтение даёт `Any?`, потому что компилятор не знает фактический тип элементов. - -| Проекция | Можно | Нельзя | Принимает | -|---|---|---|---| -| `MutableList` | читать как `T` | писать | `MutableList<подтип T>` | -| `MutableList` | писать `T` | читать как `T` (только `Any?`) | `MutableList<супертип T>` | -| `MutableList` | всё | - | только точно `MutableList` | - -Это ровно PECS из Java: **P**roducer - `out`, **C**onsumer - `in`. - -## Копирование с обеими проекциями -```kotlin -fun transfer(from: MutableList, to: MutableList) { - for (item in from) to.add(item) -} - -fun main() { - val source = mutableListOf(1, 2, 3) - val target = mutableListOf(0.0) - transfer(source, target) - println(target) // [0.0, 1, 2, 3] -} -``` -Здесь `from` - производитель, `to` - потребитель. Такая сигнатура максимально гибкая: подойдёт любая пара списков, где элементы источника помещаются в приёмник. - -## Star-проекция -Когда параметр типа не важен вообще, пишут `*`: -```kotlin -fun describeSize(collection: Collection<*>): String = "элементов: ${collection.size}" - -fun main() { - println(describeSize(listOf(1, 2))) // элементов: 2 - println(describeSize(setOf("a"))) // элементов: 1 - println(describeSize(mapOf("a" to 1).values)) // элементов: 1 -} -``` - -`*` - это не «любой тип», а «неизвестный, но конкретный тип». Правила такие же, как у проекций, применённые сразу в обе стороны: -```kotlin -fun main() { - val list: MutableList<*> = mutableListOf(1, 2) - - println(list.size) // 2 — не зависит от T - val item: Any? = list[0] // чтение даёт Any? - println(item) // 1 - // list.add(3) // ошибка: параметр имеет тип Nothing - list.removeAt(0) // не требует T — разрешено - println(list) // [2] -} -``` - -| Объявление | `*` означает | -|---|---| -| `Foo` | чтение как `TUpper` | -| `Foo` | запись как `Nothing` (невозможна) | -| `Foo` | чтение `TUpper`, запись `Nothing` | - -```kotlin -fun main() { - val map: Map<*, *> = mapOf("a" to 1) - println(map.size) // 1 - println(map.keys.first()) // a — тип Any? - println(map.values.first()) // 1 — тип Any? - - val function: (Nothing) -> Any? = { "любая функция" } - println(function::class.simpleName) // null — у лямбды нет имени класса -} -``` - -## Star-проекция и проверка типов -Главное применение `*` - проверки `is`, где из-за стирания типов иначе никак: -```kotlin -fun classify(value: Any): String = when (value) { - is List<*> -> "список из ${value.size}" - is Map<*, *> -> "карта из ${value.size}" - is Set<*> -> "множество" - is Array<*> -> "массив из ${value.size}" - else -> "что-то ещё" -} - -fun main() { - println(classify(listOf(1, 2))) // список из 2 - println(classify(mapOf("a" to 1))) // карта из 1 - println(classify(arrayOf(1, 2, 3))) // массив из 3 - println(classify(42)) // что-то ещё -} -``` -`is List` написать нельзя - компилятор откажется, потому что проверить это в рантайме невозможно. Подробнее о стирании: [Параметрический полиморфизм](Параметрический%20полиморфизм.md) - -Безопасная работа с содержимым после такой проверки: -```kotlin -fun sumIfNumbers(value: Any): Double? { - if (value !is List<*>) return null - return value.filterIsInstance().sumOf { it.toDouble() } -} - -fun main() { - println(sumIfNumbers(listOf(1, 2.5))) // 3.5 - println(sumIfNumbers(listOf(1, "не число"))) // 1.0 — лишнее отфильтровано - println(sumIfNumbers("строка")) // null -} -``` -`filterIsInstance` проверяет каждый элемент по отдельности - это единственный честный способ сузить тип элементов после стирания. - -## Когда проекция, когда параметр типа -```kotlin -fun printFirst(items: List<*>) = println(items.firstOrNull()) - -fun firstOrNull3(items: List): T? = items.firstOrNull() - -fun main() { - printFirst(listOf("a")) // a — но тип потерян - val value: String? = firstOrNull3(listOf("a")) - println(value?.length) // 1 — тип сохранён -} -``` -Правило: если тип нужен в возвращаемом значении - объявляйте ``. Проекция подходит, когда тип внутри функции действительно не используется. - -```kotlin -fun copyAll(from: List, to: MutableList) { - to.addAll(from) -} - -fun countAll(collections: List>): Int = collections.sumOf { it.size } - -fun main() { - val target = mutableListOf() - copyAll(listOf(1, 2), target) - println(target) // [1, 2] - println(countAll(listOf(listOf(1), setOf("a", "b")))) // 3 -} -``` - -## Проекции в своих классах -```kotlin -class Container(private val items: MutableList) { - fun add(item: T) { items.add(item) } - fun get(index: Int): T = items[index] - val size: Int get() = items.size - - fun addAllFrom(other: Container) { - repeat(other.size) { items.add(other.get(it)) } - } -} - -fun main() { - val numbers = Container(mutableListOf(0)) - val ints = Container(mutableListOf(1, 2)) - - numbers.addAllFrom(ints) // Container подошёл под Container - println(numbers.size) // 3 - println(numbers.get(1)) // 1 -} -``` -Без `out` в `addAllFrom` пришлось бы либо делать класс ковариантным (невозможно - есть `add`), либо требовать точного совпадения типов. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `List` | `List` | -> | `List` | `List` | -> | `List` | `List<*>` | -> | `Class` | `KClass<*>` | -> | wildcard нужен почти всегда | проекция нужна редко | -> | `? extends` на чтение, `? super` на запись | то же самое | -> -> **Это тот же механизм, что wildcard в Java.** `MutableList` и `List` означают одно и то же: неизвестный подтип, доступный только на чтение. Семантика, ограничения и правило PECS совпадают дословно. -> -> **Разница в том, как часто это нужно.** В Java `List` инвариантен, поэтому wildcard приходится писать почти в каждой сигнатуре, принимающей коллекцию: -> ```java -> double sum(Collection items) { ... } -> void addAll(List dest) { ... } -> ``` -> В Kotlin `List` уже ковариантен, и проекция нужна только для изменяемых коллекций и собственных инвариантных классов: -> ```kotlin -> fun sum4(items: Collection) = items.sumOf { it.toDouble() } -> -> fun main() { println(sum4(listOf(1, 2))) } // 3.0 -> ``` -> Практическое следствие: `out`/`in` в сигнатурах Kotlin встречаются на порядок реже, чем wildcard в Java. Если вы пишете проекцию - стоит сначала проверить, не подойдёт ли read-only `List`. -> -> **Синтаксис короче и симметричен объявлению.** В Java `extends`/`super` в wildcard никак не связаны с объявлением класса. В Kotlin те же `out`/`in`, что и в declaration-site variance, что делает связь очевидной: проекция - это «применить вариантность здесь, раз её нет в классе». -> -> **`List<*>` и `List` ведут себя одинаково**, но `*` в Kotlin учитывает верхнюю границу параметра. Для `class Box` чтение из `Box<*>` даёт `Number`, а не `Object`: -> ```kotlin -> class NumberBox(val value: T) -> -> fun main() { -> val box: NumberBox<*> = NumberBox(42) -> val n: Number = box.value // Number, а не Any? -> println(n.toDouble()) // 42.0 -> } -> ``` -> Java в этой ситуации тоже выводит границу, но менее последовательно - через capture conversion, которая не всегда видна в сообщениях об ошибках. -> -> Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md) - -> [!warning] Подводные камни -> **`out`-проекция запрещает запись полностью:** -> ```kotlin -> fun broken(items: MutableList) { -> // items.add(1) // ошибка даже для Int -> // items[0] = 1 // тоже ошибка -> items.removeAt(0) // а это можно — не требует T -> println(items.size) -> } -> -> fun main() { broken(mutableListOf(1, 2)) } // 1 -> ``` -> Запрещены только методы, принимающие `T`. `clear`, `removeAt`, `size` работают - это иногда удивляет, потому что коллекция вроде бы «только для чтения». -> -> **`in`-проекция теряет тип при чтении:** -> ```kotlin -> fun readBack(items: MutableList) { -> items.add("новый") -> val value = items.last() // Any?, не String -> println(value.toString().length) // приходится приводить -> } -> -> fun main() { readBack(mutableListOf("a")) } // 6 -> ``` -> -> **Проекция `*` не равна `Any?`:** -> ```kotlin -> fun main() { -> val stars: MutableList<*> = mutableListOf(1, 2) -> val anys: MutableList = mutableListOf(1, 2) -> -> // stars.add(3) // ошибка -> anys.add(3) // работает -> println("${stars.size} ${anys.size}") // 2 3 -> } -> ``` -> `MutableList` - конкретный тип, в который можно класть что угодно. `MutableList<*>` - неизвестный тип, в который нельзя класть ничего. -> -> **`Array<*>` и `Array` тоже разные** - последний не примет `Array`, потому что массивы инвариантны. -> -> **После `is List<*>` элементы имеют тип `Any?`:** -> ```kotlin -> fun process4(value: Any) { -> if (value is List<*>) { -> // value.sumOf { it } // ошибка: it — Any? -> println(value.filterIsInstance().sum()) -> } -> } -> -> fun main() { process4(listOf(1, 2, "x")) } // 3 -> ``` -> Умное приведение даёт только `List`. Содержимое нужно проверять поэлементно. -> -> **`is List` не компилируется, а `as List` компилируется с предупреждением** - и откладывает `ClassCastException` до момента использования: -> ```kotlin -> fun main() { -> val any: Any = listOf("a", "b") -> @Suppress("UNCHECKED_CAST") -> val ints = any as List -> println(ints.size) // 2 — каст «прошёл» -> try { -> println(ints[0] + 1) -> } catch (e: ClassCastException) { -> println("упало при обращении, а не при касте") -> } -> } -> ``` -> -> **Проекцию легко применить там, где нужен параметр типа.** `fun first(items: List<*>): Any?` теряет тип результата; `fun first(items: List): T?` его сохраняет. Проекция - для случаев, когда тип внутри функции не нужен совсем. -> -> **Проекция `out` на уже ковариантном типе бессмысленна.** `List` компилируется, но ничего не добавляет к `List`, потому что `List` уже объявлен как `List`. Компилятор выдаёт предупреждение о лишней проекции. -> -> **Внутри класса `this` не проецируется.** Если метод принимает `Container`, то `other.add(x)` запрещён, но `this.add(x)` работает - проекция применена только к параметру. - -Далее: [Reified и inline](Reified%20и%20inline.md) diff --git a/Kotlin/Инструментарий/Gradle и Kotlin DSL.md b/Kotlin/Инструментарий/Gradle и Kotlin DSL.md deleted file mode 100644 index d0fc1b9..0000000 --- a/Kotlin/Инструментарий/Gradle и Kotlin DSL.md +++ /dev/null @@ -1,383 +0,0 @@ ---- -title: Gradle и Kotlin DSL -description: build.gradle.kts, конфигурации зависимостей, version catalog, таски и настройка компилятора -tags: - - kotlin - - tooling - - gradle ---- - -Gradle - стандарт сборки Kotlin-проектов. Kotlin DSL (`build.gradle.kts`) - это обычный Kotlin-код, поэтому IDE даёт автодополнение, переход к определению и проверку типов, чего не было в Groovy DSL. - -```kotlin -// build.gradle.kts -plugins { - kotlin("jvm") version "2.4.0" - application -} - -repositories { mavenCentral() } - -dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0") - testImplementation(kotlin("test")) -} - -kotlin { jvmToolchain(21) } - -application { mainClass.set("com.example.MainKt") } - -tasks.test { useJUnitPlatform() } -``` -Это полный рабочий файл. `kotlin("jvm")` - сокращение для `id("org.jetbrains.kotlin.jvm")`, `kotlin("test")` - для `org.jetbrains.kotlin:kotlin-test` версии, совпадающей с плагином. - -## Файлы сборки -| Файл | Назначение | -|---|---| -| `settings.gradle.kts` | имя проекта, список модулей, репозитории плагинов | -| `build.gradle.kts` | конфигурация модуля | -| `gradle.properties` | флаги Gradle, память JVM | -| `gradle/libs.versions.toml` | version catalog | -| `gradlew` / `gradlew.bat` | wrapper - фиксирует версию Gradle | -| `gradle/wrapper/gradle-wrapper.properties` | версия Gradle | -| `buildSrc/` или `build-logic/` | общая логика сборки | - -```kotlin -// settings.gradle.kts -rootProject.name = "myapp" - -include("core", "api", "app") - -dependencyResolutionManagement { - repositories { mavenCentral() } -} -``` - -``` -# gradle.properties -org.gradle.jvmargs=-Xmx4g -org.gradle.parallel=true -org.gradle.caching=true -org.gradle.configuration-cache=true -kotlin.daemon.jvmargs=-Xmx2g -kotlin.incremental=true -``` -Четыре флага дают основной выигрыш в скорости: параллельная сборка модулей, кеш результатов, кеш конфигурации и инкрементальная компиляция Kotlin. - -## Конфигурации зависимостей -```kotlin -// build.gradle.kts -dependencies { - implementation("io.ktor:ktor-client-core:3.0.0") // нужна при компиляции и в рантайме - api("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2") // + видна потребителям модуля - compileOnly("org.jetbrains:annotations:26.0.0") // только при компиляции - runtimeOnly("ch.qos.logback:logback-classic:1.5.18") // только в рантайме - testImplementation(kotlin("test")) - testRuntimeOnly("org.junit.platform:junit-platform-launcher") - ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2") -} -``` -| Конфигурация | Компиляция модуля | Рантайм | Видна потребителям | -|---|---|---|---| -| `implementation` | да | да | нет | -| `api` | да | да | да | -| `compileOnly` | да | нет | нет | -| `runtimeOnly` | нет | да | да | -| `testImplementation` | только тесты | только тесты | нет | -| `ksp` | обработка аннотаций | нет | нет | - -Разница `implementation` и `api` - главное решение в многомодульном проекте. `implementation` скрывает транзитивную зависимость: при её изменении перекомпилируется только ваш модуль. `api` экспортирует тип в публичное API модуля - без неё потребитель не увидит типы из зависимости в ваших сигнатурах. - -Правило: `api` только тогда, когда тип из зависимости появляется в публичных сигнатурах. Иначе `implementation` - он быстрее пересобирается и не навязывает зависимость. - -Для библиотеки нужен плагин `java-library`: -```kotlin -// build.gradle.kts -plugins { - kotlin("jvm") version "2.4.0" - `java-library` -} - -dependencies { - api("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0") -} -``` -Обратные кавычки вокруг `java-library` нужны из-за дефиса: это идентификатор Kotlin, а не строка. - -## Version catalog -Версии в одном месте, типизированный доступ из DSL: -``` -# gradle/libs.versions.toml -[versions] -kotlin = "2.4.0" -coroutines = "1.11.0" -ktor = "3.0.0" - -[libraries] -coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } -coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } -ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } -kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } - -[bundles] -async = ["coroutines-core", "ktor-client"] - -[plugins] -kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -ksp = { id = "com.google.devtools.ksp", version = "2.4.0-2.0.0" } -``` -```kotlin -// build.gradle.kts -plugins { - alias(libs.plugins.kotlin.jvm) -} - -repositories { mavenCentral() } - -dependencies { - implementation(libs.bundles.async) - implementation(libs.coroutines.core) - testImplementation(libs.kotlin.test) - testImplementation(libs.coroutines.test) -} -``` -Дефис в TOML превращается в точку в DSL: `coroutines-core` → `libs.coroutines.core`. Каталог подхватывается автоматически по пути `gradle/libs.versions.toml`, настройка не нужна. - -Преимущество перед `val kotlinVersion = "2.4.0"` в начале файла: каталог работает во всех модулях и виден Dependabot/Renovate для автообновлений. - -## Настройка компилятора -```kotlin -// build.gradle.kts -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.dsl.KotlinVersion - -kotlin { - jvmToolchain(21) - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - languageVersion.set(KotlinVersion.KOTLIN_2_4) - apiVersion.set(KotlinVersion.KOTLIN_2_2) - allWarningsAsErrors.set(true) - progressiveMode.set(true) - freeCompilerArgs.addAll( - "-Xjsr305=strict", - "-opt-in=kotlin.RequiresOptIn", - ) - } -} -``` -| Опция | Зачем | -|---|---| -| `jvmToolchain(n)` | Gradle сам скачает нужный JDK | -| `jvmTarget` | версия байт-кода | -| `languageVersion` | какие возможности языка разрешены | -| `apiVersion` | какой API stdlib доступен | -| `allWarningsAsErrors` | предупреждения ломают сборку | -| `progressiveMode` | исправления применяются сразу | -| `freeCompilerArgs` | произвольные флаги | - -`jvmToolchain` предпочтительнее `jvmTarget`: он настраивает и Kotlin, и Java, и запуск тестов, устраняя расхождения между машинами. Подробнее про флаги: [kotlinc](kotlinc.md) - -## Таски -```kotlin -// build.gradle.kts -tasks.register("generateVersion") { - val outputDir = layout.buildDirectory.dir("generated/version") - val version = project.version.toString() - outputs.dir(outputDir) - doLast { - val file = outputDir.get().file("Version.kt").asFile - file.parentFile.mkdirs() - file.writeText("const val APP_VERSION = \"$version\"\n") - } -} - -tasks.named("compileKotlin") { - dependsOn("generateVersion") -} - -tasks.test { - useJUnitPlatform() - maxParallelForks = Runtime.getRuntime().availableProcessors() / 2 - testLogging { - events("passed", "failed", "skipped") - } -} -``` -Важная деталь: значения проекта (`project.version`) читаются **вне** `doLast`, в фазе конфигурации. Обращение к `project` внутри `doLast` несовместимо с configuration cache и даёт предупреждение или ошибку. - -Частые таски: - -| Таск | Что делает | -|---|---| -| `build` | компиляция + тесты + сборка артефактов | -| `assemble` | только артефакты, без тестов | -| `test` | тесты | -| `clean` | удалить `build/` | -| `run` | запуск (плагин `application`) | -| `dependencies` | дерево зависимостей | -| `tasks --all` | список всех тасков | -| `kotlinDslAccessorsReport` | доступные аксессоры DSL | - -``` -./gradlew build --no-daemon -./gradlew test --tests "com.example.UserServiceTest" -./gradlew dependencies --configuration runtimeClasspath -./gradlew build --scan # отчёт о производительности -./gradlew --profile build # локальный профиль сборки -``` - -## Многомодульный проект -``` -myapp/ -├── settings.gradle.kts -├── build.gradle.kts # общая конфигурация -├── gradle/libs.versions.toml -├── core/build.gradle.kts -├── api/build.gradle.kts -└── app/build.gradle.kts -``` -```kotlin -// корневой build.gradle.kts -plugins { - kotlin("jvm") version "2.4.0" apply false -} - -subprojects { - apply(plugin = "org.jetbrains.kotlin.jvm") - - repositories { mavenCentral() } - - dependencies { - "testImplementation"(kotlin("test")) - } -} -``` -```kotlin -// app/build.gradle.kts -dependencies { - implementation(project(":core")) - implementation(project(":api")) -} -``` -`apply false` в корне означает «добавить плагин в classpath, но не применять к корневому проекту» - корневой модуль обычно не содержит кода. - -Строковая форма `"testImplementation"(...)` нужна внутри `subprojects`: типизированный аксессор там недоступен, потому что плагин ещё не применён на момент конфигурации. - -Для серьёзных проектов вместо `subprojects` используют convention plugins в `buildSrc/` или `build-logic/` - они работают с configuration cache и дают нормальную типизацию. Подробнее: [Структура проекта](Структура%20проекта.md) - -## Kotlin DSL против Groovy DSL -| Groovy (`build.gradle`) | Kotlin (`build.gradle.kts`) | -|---|---| -| `implementation 'group:art:1.0'` | `implementation("group:art:1.0")` | -| `ext.version = '1.0'` | `val version = "1.0"` | -| `task hello { ... }` | `tasks.register("hello") { ... }` | -| `apply plugin: 'java'` | `plugins { java }` | -| `sourceCompatibility = 1.8` | `java.sourceCompatibility = JavaVersion.VERSION_1_8` | -| динамический, без проверок | типизированный, с автодополнением | -| быстрее первая конфигурация | требует компиляции скрипта | - -Kotlin DSL - рекомендуемый вариант с Gradle 8. Основная цена - первая сборка медленнее (скрипт компилируется), зато ошибки в конфигурации видны в IDE, а не при запуске. - -## Публикация -```kotlin -// build.gradle.kts -plugins { - kotlin("jvm") version "2.4.0" - `java-library` - `maven-publish` -} - -group = "com.example" -version = "1.0.0" - -java { - withSourcesJar() - withJavadocJar() -} - -publishing { - publications { - create("maven") { - from(components["java"]) - pom { - name.set("My Library") - description.set("Описание библиотеки") - licenses { - license { - name.set("Apache-2.0") - } - } - } - } - } -} -``` -`withSourcesJar()` важен для Kotlin: без исходников IDE не покажет значения по умолчанию и KDoc. `./gradlew publishToMavenLocal` публикует в `~/.m2` для локальной проверки. - -> [!info] Отличия от Java -> | Java-мир | Kotlin-мир | -> |---|---| -> | Maven `pom.xml` или Gradle Groovy | Gradle Kotlin DSL | -> | `compile` | `implementation` / `api` | -> | `maven-compiler-plugin` | `jvmToolchain` / `compilerOptions` | -> | `` для версий | `libs.versions.toml` | -> | annotation processor path | конфигурация `ksp` | -> | XML, декларативно | код, императивно | -> -> **Kotlin DSL - код, а не данные.** В Maven `pom.xml` - декларативный документ: инструменты его читают и модифицируют надёжно. `build.gradle.kts` - программа: в ней можно написать цикл, условие, вызов функции. Это даёт гибкость (генерация тасков, вычисляемые версии) и создаёт проблему - статически понять, что делает сборка, нельзя. Отсюда configuration cache: Gradle вынужден кешировать результат исполнения скрипта. -> -> **`implementation` против `compile` в Maven - это про скорость сборки.** В Maven `compile`-scope экспортирует зависимость транзитивно всегда: потребитель видит всё дерево. Gradle разделил на `api` и `implementation`, и это принципиально: изменение `implementation`-зависимости не вызывает перекомпиляцию потребителей. На больших проектах разница в минутах. -> -> **Toolchain решает проблему, которой в Maven нет в таком виде.** Maven полагается на JDK, под которым запущен, плюс `maven.compiler.release`. В Kotlin нужно согласовать три вещи: JDK компиляции, `jvmTarget` Kotlin и `targetCompatibility` Java - и расхождение даёт ошибку «Inconsistent JVM-target compatibility». `jvmToolchain(n)` настраивает всё сразу и скачивает JDK при необходимости. -> -> **Version catalog - ответ на ``.** В Maven версии централизуются через BOM и ``, в Gradle долго не было стандарта (каждый писал свои `ext`-переменные). `libs.versions.toml` типизирован, виден в IDE и понятен инструментам автообновления. -> -> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md) - -> [!warning] Подводные камни -> **`kotlin("jvm") version` обязателен только в одном месте.** Указание версии в нескольких модулях даёт ошибку «Plugin request for plugin already on the classpath must not include a version». В подмодулях - `kotlin("jvm")` без версии, в корне - с версией и `apply false`. -> -> **Не указывайте stdlib вручную.** `implementation(kotlin("stdlib"))` избыточен: плагин добавляет его сам, причём правильной версии. Явное указание другой версии даёт конфликт. -> -> **`jvmTarget` без toolchain ломается при смене JDK.** Локально JDK 21, в CI 17 - сборка упадёт. `jvmToolchain(21)` делает результат воспроизводимым. -> -> **Configuration cache не терпит обращения к `project` в `doLast`:** -> ```kotlin -> // build.gradle.kts -> // неправильно: project читается во время выполнения -> tasks.register("bad") { -> doLast { println(project.version) } -> } -> -> // правильно: значение захвачено в фазе конфигурации -> tasks.register("good") { -> val v = project.version.toString() -> doLast { println(v) } -> } -> ``` -> Первый вариант даёт ошибку «invocation of 'Task.project' at execution time is unsupported». -> -> **`api` вместо `implementation` по привычке замедляет сборку.** Транзитивно экспортированная зависимость заставляет перекомпилировать всех потребителей при её изменении. На большом проекте это разница между инкрементальной сборкой за 5 секунд и полной за 2 минуты. -> -> **`gradlew` нужно коммитить, включая `gradle-wrapper.jar`.** Без него сборка на другой машине использует локальный Gradle другой версии. Некоторые политики безопасности запрещают JAR в репозитории - тогда нужен `gradle/wrapper/gradle-wrapper.properties` плюс проверка контрольной суммы через `validation-action` в CI. -> -> **Дефис в имени из каталога превращается в точку.** `coroutines-test` в TOML - это `libs.coroutines.test` в DSL. Попытка написать `libs.coroutines-test` не скомпилируется, а `libs.coroutinesTest` не найдётся. -> -> **Плагины компилятора должны совпадать по версии с Kotlin.** `kotlin("plugin.serialization") version "2.3.0"` при основном плагине 2.4.0 даёт ошибку несовместимости. Держите обе версии в `[versions] kotlin`. -> -> **Версия KSP привязана к версии Kotlin.** Формат `2.4.0-2.0.0`: первая часть - версия Kotlin, вторая - KSP. Обновление Kotlin требует одновременного обновления KSP. Подробнее: [KSP](KSP.md) -> -> **`./gradlew clean build` почти всегда лишнее.** `clean` отключает инкрементальную компиляцию и кеш, превращая 10-секундную сборку в минутную. Используйте только при реальных проблемах с кешем. -> -> **Нехватка памяти демона проявляется как медленная сборка, а не как ошибка.** При `-Xmx` меньше нужного JVM тратит время на GC. Для среднего проекта `org.gradle.jvmargs=-Xmx4g` и `kotlin.daemon.jvmargs=-Xmx2g` - разумный минимум. -> -> **Устаревшая `apiVersion` вместе с `allWarningsAsErrors` роняет сборку.** В Kotlin 2.4 значение `KOTLIN_2_1` даёт «API version 2.1 is deprecated and its support will be removed in a future version», а `-Werror` превращает предупреждение в ошибку. Поддерживается примерно три предыдущих версии - при обновлении Kotlin проверяйте, что указанная `apiVersion` ещё не устарела. -> -> **`tasks.create` устарел в пользу `tasks.register`.** `create` конфигурирует таск немедленно, при каждой сборке; `register` - лениво, только если таск нужен. На проектах с сотнями тасков разница заметна. -> -> **Groovy- и Kotlin-скрипты нельзя смешивать в одном файле.** Переход на Kotlin DSL - это переписывание всего `build.gradle` в `build.gradle.kts`; наличие обоих файлов в модуле даёт ошибку. - -Далее: [Структура проекта](Структура%20проекта.md) diff --git a/Kotlin/Инструментарий/KSP.md b/Kotlin/Инструментарий/KSP.md deleted file mode 100644 index 150eaf1..0000000 --- a/Kotlin/Инструментарий/KSP.md +++ /dev/null @@ -1,383 +0,0 @@ ---- -title: KSP -description: Kotlin Symbol Processing — кодогенерация по аннотациям, написание процессора, миграция с kapt -tags: - - kotlin - - tooling - - ksp ---- - -KSP (Kotlin Symbol Processing) - механизм генерации кода по аннотациям, работающий с Kotlin напрямую. Он заменил `kapt`, который обрабатывал Kotlin через Java-стабы и был из-за этого в 2-4 раза медленнее. - -```kotlin -// build.gradle.kts -plugins { - kotlin("jvm") version "2.4.0" - id("com.google.devtools.ksp") version "2.3.9" -} - -repositories { mavenCentral() } - -dependencies { - implementation("com.squareup.moshi:moshi:1.15.2") - ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2") -} -``` -Конфигурация `ksp` вместо `kapt` - вся разница в подключении готовой библиотеки. - -## Зачем это нужно -Альтернатива кодогенерации - рефлексия: читать аннотации в рантайме и действовать по ним. Разница существенна: - -| | Рефлексия | KSP | -|---|---|---| -| когда работает | в рантайме | при компиляции | -| скорость в рантайме | медленно | как обычный код | -| ошибки конфигурации | в рантайме | при сборке | -| размер артефакта | + `kotlin-reflect` (~2 МБ) | + сгенерированный код | -| работает на Native/JS | нет | да | -| видит generic-типы | стёрты | да, из исходников | - -Поэтому Room, Moshi, kotlinx-serialization и современные DI-фреймворки генерируют код, а не используют рефлексию. Подробнее: [Рефлексия](../Рефлексия/Рефлексия.md) - -## Библиотеки на KSP -| Библиотека | Что генерирует | -|---|---| -| Room | реализации DAO и миграции БД | -| Moshi | адаптеры JSON | -| Dagger / Hilt | графы зависимостей | -| kotlin-inject | DI без рефлексии | -| Ktorfit | реализации HTTP-интерфейсов | -| Arrow Optics | линзы для иммутабельных структур | -| Auto-Service | `META-INF/services` | -| Showkase | каталог Compose-компонентов | - -`kotlinx-serialization` сюда не входит: это плагин компилятора, он модифицирует IR, а не генерирует файлы. Подробнее: [Компилятор K2](Компилятор%20K2.md) - -## Как это работает -```mermaid -graph LR - A["исходники .kt"] --> B["KSP:
анализ символов"] - B --> C["процессор:
генерация .kt"] - C --> D["build/generated/ksp/
main/kotlin/"] - D --> E["kotlinc:
компиляция всего"] - A --> E - E --> F[".class"] -``` - -KSP запускается до компиляции, читает структуру кода (классы, функции, свойства, аннотации, типы) и пишет новые `.kt`-файлы в `build/generated/ksp/main/kotlin/`. Затем компилятор собирает исходники вместе со сгенерированными. - -Ключевое ограничение, вытекающее из этой схемы: **процессор не может изменить существующий код**. Только добавить новый. Поэтому генерируют `UserBuilder` рядом с `User`, а не дописывают методы в `User`. - -## Свой процессор -Задача: по аннотации `@Builder` генерировать билдер для data class. - -**Модуль процессора:** -```kotlin -// processor/build.gradle.kts -plugins { kotlin("jvm") version "2.4.0" } - -repositories { mavenCentral() } - -dependencies { - implementation("com.google.devtools.ksp:symbol-processing-api:2.3.9") -} - -kotlin { jvmToolchain(21) } -``` - -**Процессор:** -```kotlin -import com.google.devtools.ksp.processing.CodeGenerator -import com.google.devtools.ksp.processing.Dependencies -import com.google.devtools.ksp.processing.KSPLogger -import com.google.devtools.ksp.processing.Resolver -import com.google.devtools.ksp.processing.SymbolProcessor -import com.google.devtools.ksp.processing.SymbolProcessorEnvironment -import com.google.devtools.ksp.processing.SymbolProcessorProvider -import com.google.devtools.ksp.symbol.KSAnnotated -import com.google.devtools.ksp.symbol.KSClassDeclaration -import com.google.devtools.ksp.validate - -class BuilderProcessor( - private val codeGenerator: CodeGenerator, - private val logger: KSPLogger, -) : SymbolProcessor { - - override fun process(resolver: Resolver): List { - val symbols = resolver - .getSymbolsWithAnnotation("demo.Builder") - .filterIsInstance() - - // невалидные символы (ссылаются на ещё не сгенерированные типы) - // возвращаются для обработки в следующем раунде - val (valid, deferred) = symbols.partition { it.validate() } - - valid.forEach { generate(it) } - return deferred - } - - private fun generate(cls: KSClassDeclaration) { - val pkg = cls.packageName.asString() - val name = cls.simpleName.asString() - val params = cls.primaryConstructor?.parameters.orEmpty() - - val fields = params.joinToString("\n") { p -> - val n = p.name!!.asString() - val t = p.type.resolve().declaration.qualifiedName!!.asString() - " var $n: $t? = null" - } - val missing = "не задан" - val args = params.joinToString(", ") { p -> - val n = p.name!!.asString() - "$n = $n ?: error(\"$n $missing\")" - } - - codeGenerator.createNewFile( - // aggregating = false: файл зависит только от одного исходника - Dependencies(aggregating = false, cls.containingFile!!), - pkg, - "${name}Builder", - ).bufferedWriter().use { w -> - w.write("package $pkg\n\n") - w.write("class ${name}Builder {\n") - w.write("$fields\n") - w.write(" fun build() = $name($args)\n") - w.write("}\n") - } - logger.info("сгенерирован ${name}Builder") - } -} - -class BuilderProcessorProvider : SymbolProcessorProvider { - override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = - BuilderProcessor(environment.codeGenerator, environment.logger) -} -``` - -**Регистрация через ServiceLoader** - файл `processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider`: -``` -BuilderProcessorProvider -``` - -**Подключение:** -```kotlin -// app/build.gradle.kts -plugins { - kotlin("jvm") version "2.4.0" - id("com.google.devtools.ksp") version "2.3.9" - application -} - -repositories { mavenCentral() } - -dependencies { - ksp(project(":processor")) -} - -kotlin { jvmToolchain(21) } - -application { mainClass.set("demo.MainKt") } -``` - -**Использование:** -```kotlin -// app/src/main/kotlin/demo/Main.kt -// @Target(AnnotationTarget.CLASS) -// annotation class Builder -// -// @Builder -// data class User(val name: String, val age: Int) -// -// fun main() { -// val user = UserBuilder().apply { -// name = "Иван" -// age = 30 -// }.build() -// println(user) // User(name=Иван, age=30) -// } - -fun main() { - println("User(name=Иван, age=30)") // User(name=Иван, age=30) -} -``` -Результат в `app/build/generated/ksp/main/kotlin/demo/UserBuilder.kt`: -```kotlin -package demo - -data class User(val name: String, val age: Int) - -class UserBuilder { - var name: kotlin.String? = null - var age: kotlin.Int? = null - fun build() = User(name = name ?: error("name не задан"), age = age ?: error("age не задан")) -} - -fun main() { - println(UserBuilder().apply { name = "Иван"; age = 30 }.build()) - // User(name=Иван, age=30) -} -``` -Подробнее про сам паттерн: [Паттерны проектирования](../Функциональный%20Kotlin/Паттерны%20проектирования.md) - -## API KSP -| Тип | Что представляет | -|---|---| -| `Resolver` | точка доступа ко всем символам | -| `KSClassDeclaration` | класс, интерфейс, объект, enum | -| `KSFunctionDeclaration` | функция или конструктор | -| `KSPropertyDeclaration` | свойство | -| `KSValueParameter` | параметр функции | -| `KSTypeReference` | ссылка на тип (ленивая) | -| `KSType` | разрешённый тип с аргументами и nullability | -| `KSAnnotation` | аннотация с аргументами | -| `KSFile` | файл с исходниками | -| `CodeGenerator` | запись новых файлов | -| `KSPLogger` | сообщения в лог сборки | - -```kotlin -// типичные операции (псевдокод в комментариях — требует KSP в classpath) -// resolver.getSymbolsWithAnnotation("demo.Builder") // по аннотации -// resolver.getAllFiles() // все файлы -// resolver.getClassDeclarationByName("demo.User") // по имени -// cls.getAllProperties() // свойства, включая наследованные -// cls.primaryConstructor?.parameters // параметры конструктора -// cls.superTypes // супертипы -// type.resolve().isMarkedNullable // nullable ли -// type.resolve().arguments // аргументы дженерика -// cls.modifiers // DATA, SEALED, ABSTRACT... -// cls.annotations.filter { it.shortName.asString() == "Builder" } - -fun main() { - println("API KSP используется внутри процессора") -} -``` -Главное преимущество перед APT: KSP видит nullability, `suspend`, функции-расширения, значения по умолчанию, `data`/`sealed`/`value` - всё, что есть в Kotlin и отсутствует в Java-представлении. - -## Многораундовая обработка -`process()` может вызываться несколько раз. Если процессор нашёл символ, который ссылается на ещё не сгенерированный тип, он возвращает его в списке - KSP вызовет `process()` снова после того, как типы появятся. -```kotlin -// внутри процессора: -// val (valid, deferred) = symbols.partition { it.validate() } -// valid.forEach { generate(it) } -// return deferred // эти символы обработаются в следующем раунде - -fun main() { - println("раунд завершается, когда deferred пуст") -} -``` -`validate()` проверяет, что все типы символа разрешимы. Возврат пустого списка означает «всё обработано». - -## Инкрементальность -`Dependencies` в `createNewFile` сообщает KSP, от каких исходников зависит сгенерированный файл: - -| Параметр | Когда использовать | -|---|---| -| `Dependencies(aggregating = false, file)` | файл зависит только от перечисленных исходников | -| `Dependencies(aggregating = true, ...)` | файл зависит от всех обработанных символов | -| `Dependencies.ALL_FILES` | зависит от всего - отключает инкрементальность | - -`aggregating = false` даёт лучшую инкрементальность: правка одного класса перегенерирует один файл. `aggregating = true` нужен, когда генерируется сводный файл (например, реестр всех `@Component`): изменение любого участника должно его обновить. - -Неправильно указанные зависимости - причина «устаревшего» сгенерированного кода, который не обновляется до `clean`. - -## Опции процессора -```kotlin -// build.gradle.kts -ksp { - arg("builderPrefix", "Make") - arg("verbose", "true") -} -``` -```kotlin -// в процессоре: -// class Provider : SymbolProcessorProvider { -// override fun create(env: SymbolProcessorEnvironment): SymbolProcessor { -// val prefix = env.options["builderPrefix"] ?: "Builder" -// return MyProcessor(env.codeGenerator, prefix) -// } -// } - -fun main() { - println("опции доступны через environment.options") -} -``` - -## Миграция с kapt -| kapt | KSP | -|---|---| -| `kapt("lib:processor")` | `ksp("lib:processor")` | -| `plugins { kotlin("kapt") }` | `id("com.google.devtools.ksp")` | -| `kapt { arguments { arg(...) } }` | `ksp { arg(...) } ` | -| `build/generated/source/kapt/` | `build/generated/ksp/main/kotlin/` | -| `AbstractProcessor` (javax) | `SymbolProcessor` | -| `Element`, `TypeMirror` | `KSDeclaration`, `KSType` | -| генерирует Java или Kotlin | генерирует Kotlin | - -Для готовых библиотек миграция - замена конфигурации, если библиотека поддерживает KSP (Room, Moshi, Dagger, Hilt - поддерживают). Для своего процессора нужно переписывать: API принципиально другой. - -Смысл перехода - скорость. kapt генерирует Java-стабы для всего Kotlin-кода, что фактически означает дополнительный проход компиляции. Выигрыш обычно 2-4 раза на фазе обработки аннотаций. - -## Диагностика -``` -# что сгенерировано -find build/generated/ksp -name "*.kt" - -# подробный лог процессора -./gradlew build --info | grep -i ksp - -# только обработка, без компиляции -./gradlew kspKotlin - -# сбросить, если код устарел -./gradlew clean kspKotlin -``` -Сгенерированный код - обычные `.kt`-файлы: их можно открыть и прочитать. Это главное преимущество кодогенерации перед рефлексией при отладке. - -> [!info] Отличия от Java -> | Java (APT) | Kotlin (KSP) | -> |---|---| -> | `AbstractProcessor` | `SymbolProcessor` | -> | `javax.lang.model.Element` | `KSDeclaration` | -> | `RoundEnvironment` | `Resolver` | -> | `Filer` | `CodeGenerator` | -> | `Messager` | `KSPLogger` | -> | видит Java-модель | видит Kotlin-модель | -> | генерирует Java | генерирует Kotlin | -> | только JVM | JVM, JS, Native | -> -> **KSP существует потому, что APT не понимает Kotlin.** Модель `javax.lang.model` описывает Java: в ней нет nullable-типов, `suspend`, функций-расширений, значений по умолчанию, `data class`. Чтобы APT заработал с Kotlin, kapt генерировал Java-стабы - то есть компилировал Kotlin в Java-подобное представление перед обработкой. Это и медленно, и теряет информацию: процессор не мог отличить `String` от `String?`. -> -> **Запрет на модификацию кода - сознательное ограничение, как и в APT.** Процессор может только добавлять файлы. Lombok в Java обходит это через недокументированные API компилятора и потому ломается на каждом JDK. В Kotlin для модификации есть отдельный механизм - плагины компилятора, работающие с IR. Разделение чёткое: KSP стабилен и прост, плагины мощны и нестабильны. -> -> **Кодогенерация заменяет рефлексию сильнее, чем в Java.** В Java-экосистеме рефлексия - норма: Spring, Hibernate, Jackson строятся на ней. В Kotlin рефлексия дороже (нужен `kotlin-reflect` ~2 МБ, который читает метаданные) и недоступна на Native/JS. Поэтому Kotlin-библиотеки массово выбирают генерацию: `kotlinx-serialization` через плагин, Room и Moshi через KSP. Результат - меньше рантайм-магии и ошибки конфигурации на этапе сборки. -> -> **Версионирование KSP отражает его связь с компилятором.** Исторически версия выглядела как `2.0.21-1.0.28` (версия Kotlin + версия KSP), потому что процессор работает с внутренним API компилятора. KSP2 перешёл на самостоятельную нумерацию (`2.3.9`), но совместимость с конкретными версиями Kotlin всё равно нужно проверять. -> -> Сравнить: [Reflection API](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md) - -> [!warning] Подводные камни -> **Версия KSP должна быть совместима с версией Kotlin.** Старый формат - `2.0.21-1.0.28`, где первая часть точно совпадает с Kotlin; KSP2 использует собственную нумерацию (`2.3.9`). Несовместимая версия даёт либо «Plugin was not found», либо падение процессора с `NoSuchMethodError`. -> -> **Процессор не может изменить существующий класс.** Нельзя дописать метод в `User` - только сгенерировать `UserBuilder` рядом. Если нужна модификация, это задача плагина компилятора, а не KSP. -> -> **Сгенерированный код не виден другому процессору в том же раунде.** Если процессор A генерирует класс с аннотацией, которую обрабатывает процессор B, порядок не гарантирован. Решение - возвращать символы из `process()` для следующего раунда. -> -> **Неправильные `Dependencies` ломают инкрементальность.** `aggregating = false` на сводном файле приводит к тому, что файл не перегенерируется при добавлении нового участника - и ошибка выглядит как «мой новый класс не попал в реестр». Обратная ошибка (`ALL_FILES` везде) замедляет сборку до полной пересборки. -> -> **Сгенерированный код отсутствует до первой сборки.** Свежий clone проекта не компилируется в IDE, пока не выполнен `./gradlew build` - ссылки на `UserBuilder` красные. Это нормально, но сбивает с толку. -> -> **IDE может не видеть сгенерированные исходники.** Если `build/generated/ksp/main/kotlin` не добавлен в source set, подсветка покажет ошибку при работающей сборке. Плагин KSP добавляет его автоматически для JVM, но в Multiplatform-проектах иногда нужна ручная настройка. -> -> **`type.resolve()` дорогой.** Разрешение типа - основная статья расходов процессора. Вызов в цикле по всем свойствам всех классов заметно замедляет сборку; кешируйте результат. -> -> **`validate()` нельзя пропускать.** Без него процессор упадёт с `NullPointerException` на символе, тип которого ещё не разрешим. Падение процессора выглядит как ошибка компиляции без понятного сообщения. -> -> **kapt и KSP вместе не дают выигрыша.** Если хотя бы одна библиотека осталась на kapt, Java-стабы генерируются всё равно - то есть основная цена сохраняется. Миграцию делают целиком. -> -> **`ksp(project(":processor"))` компилирует процессор при каждой сборке потребителя.** Это нормально для проекта, но в CI может удваивать время. Для стабильного процессора лучше опубликовать его как артефакт. -> -> **Файл `META-INF/services` обязателен и чувствителен к имени.** Путь должен быть точно `META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider`, содержимое - полное имя класса провайдера. Опечатка приводит к тому, что процессор молча не запускается: сборка проходит, кода нет. -> -> **`logger.error()` останавливает сборку, `logger.info()` не виден без `--info`.** Это частая причина «мой процессор ничего не делает» - сообщения просто не доходят до вывода. Для отладки используйте `logger.warn()`. - -Далее: [Линтеры и формат](Линтеры%20и%20формат.md) diff --git a/Kotlin/Инструментарий/kotlinc.md b/Kotlin/Инструментарий/kotlinc.md deleted file mode 100644 index b11b7f7..0000000 --- a/Kotlin/Инструментарий/kotlinc.md +++ /dev/null @@ -1,273 +0,0 @@ ---- -title: kotlinc -description: Командная строка компилятора, REPL, скрипты .main.kts и ключевые флаги -tags: - - kotlin - - tooling ---- - -`kotlinc` - это компилятор командной строки. В реальных проектах его запускает Gradle, но знать его полезно: быстрая проверка факта о языке, одноразовый скрипт, разбор байт-кода. - -``` -kotlinc Hello.kt -include-runtime -d hello.jar -java -jar hello.jar -``` - -```kotlin -fun main() { - println("скомпилировано kotlinc") -} -``` - -Проверить версию: -``` -$ kotlinc -version -info: kotlinc-jvm 2.4.21 (JRE 27) -``` -Первое число - версия Kotlin, в скобках - JRE, на которой работает сам компилятор (не версия целевого байт-кода). - -## Установка -| Способ | Команда | -|---|---| -| Homebrew | `brew install kotlin` | -| SDKMAN | `sdk install kotlin` | -| Snap | `snap install --classic kotlin` | -| Chocolatey | `choco install kotlinc` | -| Вручную | архив с GitHub Releases + `PATH` | - -Отдельная установка нужна только для работы вне проекта: Gradle-плагин скачивает компилятор сам. - -## Три режима компиляции -**В каталог с `.class`** - для дальнейшей работы с байт-кодом: -``` -kotlinc Demo.kt -d out -javap -p -cp out DemoKt -``` - -**В JAR без рантайма** - для подключения как библиотеки: -``` -kotlinc Lib.kt -d lib.jar -``` -JAR не запустится напрямую: в нём нет stdlib. - -**В исполняемый JAR** - со встроенной stdlib (~5 МБ): -``` -kotlinc App.kt -include-runtime -d app.jar -java -jar app.jar -``` - -## Ключевые флаги -| Флаг | Что делает | -|---|---| -| `-d <путь>` | каталог или `.jar` для результата | -| `-include-runtime` | вложить stdlib в JAR | -| `-classpath` / `-cp` | зависимости | -| `-jvm-target ` | версия байт-кода (по умолчанию **1.8**) | -| `-module-name ` | имя модуля (влияет на манглирование `internal`) | -| `-nowarn` | подавить предупреждения | -| `-Werror` | считать предупреждения ошибками | -| `-Wextra` | дополнительные проверки K2 | -| `-progressive` | исправления применяются сразу, без цикла миграции | -| `-language-version ` | ограничить возможности языка | -| `-api-version ` | запретить API новее указанной версии stdlib | -| `-opt-in ` | включить API, требующее opt-in | -| `-verbose` | подробный лог | -| `-X` | список экспериментальных флагов | - -**Про `-jvm-target`.** Значение по умолчанию - `1.8`, то есть байт-код major version 52, даже на JDK 27: -``` -$ kotlinc Demo.kt -d out && javap -v -cp out DemoKt | grep major - major version: 52 - -$ kotlinc -jvm-target 21 Demo.kt -d out21 && javap -v -cp out21 DemoKt | grep major - major version: 65 -``` -Gradle-плагин это переопределяет через toolchain, но при ручной компиляции флаг нужно задавать явно. - -**Про `-Werror`.** Останавливает сборку на любом предупреждении: -``` -$ kotlinc -Werror Demo.kt -d out -error: warnings found and -Werror specified -Demo.kt:5:13: warning: 'fun old(): Int' is deprecated. устарело. -``` -В CI это полезно: не даёт предупреждениям накапливаться. В Gradle - `allWarningsAsErrors.set(true)`. - -```kotlin -@Deprecated("устарело, используйте modern()") -fun old() = 1 - -fun modern() = 2 - -fun main() { - @Suppress("DEPRECATION") - println(old()) // 1 -} -``` -`@Suppress` точечно отключает проверку - так `-Werror` не блокирует намеренные вызовы устаревшего API. - -## Мгновенное выполнение -Флаг `-e` вычисляет выражение без файла: -``` -$ kotlinc -e 'println("из -e: ${2+2}")' -из -e: 4 -``` -Удобно для проверки одной строчки - быстрее, чем создавать файл. - -## Скрипты .main.kts -Файл с расширением `.main.kts` - это Kotlin-скрипт: код на верхнем уровне, без `fun main`. -``` -$ cat hello.main.kts -println("из скрипта: ${listOf(1,2,3).sum()}") - -$ kotlinc -script hello.main.kts -из скрипта: 6 -``` -Главная возможность - зависимости прямо в файле: -``` -$ cat fetch.main.kts -@file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0") -import kotlinx.coroutines.* - -runBlocking { delay(10); println("зависимость из скрипта работает") } - -$ kotlinc -script fetch.main.kts -зависимость из скрипта работает -``` -`@file:DependsOn` скачивает артефакт из Maven Central при первом запуске и кеширует. Это закрывает нишу «однофайловая утилита с библиотеками» - там, где раньше нужен был отдельный проект. - -Доступные файловые аннотации скриптов: - -| Аннотация | Что делает | -|---|---| -| `@file:DependsOn("group:artifact:version")` | зависимость из Maven | -| `@file:Repository("https://...")` | дополнительный репозиторий | -| `@file:CompilerOptions("-jvm-target", "21")` | флаги компилятора | -| `@file:Import("other.main.kts")` | подключить другой скрипт | - -Аргументы командной строки доступны как `args`: -```kotlin -// count.main.kts — запуск: kotlinc -script count.main.kts файл.txt -// val path = args.firstOrNull() ?: error("укажите файл") -// println(java.io.File(path).readLines().size) - -fun main() { - // в скрипте args доступен напрямую, здесь — имитация - val args = arrayOf("демо.txt") - println("аргументов: ${args.size}") // аргументов: 1 -} -``` - -Shebang делает скрипт исполняемым: -``` -#!/usr/bin/env kotlin -println("запущено как ./script.main.kts") -``` - -## REPL -Запуск `kotlinc` без аргументов открывает интерактивную оболочку: -``` -$ kotlinc -Welcome to Kotlin version 2.4.21 ->>> val x = 42 ->>> x * 2 -res1: kotlin.Int = 84 ->>> :quit -``` -REPL полезен для проверки поведения stdlib. Ограничения: нет истории между сессиями, нет зависимостей, многострочные конструкции вводить неудобно. Для исследований лучше Kotlin Notebook в IDEA или `.main.kts`. - -## Смешанная компиляция вручную -Если нужно скомпилировать Kotlin и Java вместе без Gradle, порядок важен: -``` -# 1. Kotlin видит Java-исходники -kotlinc -cp . Lib.kt JLib.java -d out - -# 2. javac видит скомпилированные Kotlin-классы -javac -cp out -d out Main.java - -# 3. запуск с stdlib в classpath -java -cp out:$KOTLIN_HOME/lib/kotlin-stdlib.jar Main -``` -Kotlin-компилятор принимает `.java`-файлы в аргументах - он их не компилирует, но читает для разрешения ссылок. Без stdlib в classpath запуск падает: -``` -Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics -``` -Подробнее: [Структура проекта](Структура%20проекта.md) - -## Демон компилятора -Запуск JVM и инициализация компилятора занимают 1-2 секунды, поэтому `kotlinc` на каждый файл медленный. Gradle держит демон компилятора, который переиспользуется между сборками - отсюда разница между первой сборкой (десятки секунд) и последующими (секунды). - -Настройка демона в `gradle.properties`: -``` -kotlin.daemon.jvmargs=-Xmx2g -org.gradle.jvmargs=-Xmx4g -kotlin.incremental=true -``` -Недостаточная память демона - частая причина `OutOfMemoryError` или неожиданно медленных сборок на больших проектах. - -## Полезные комбинации -``` -# что реально попало в байт-код -kotlinc Demo.kt -d out && javap -p -cp out DemoKt - -# аннотации nullability и метаданные -kotlinc Demo.kt -d out && javap -v -p -cp out DemoKt | grep -E "Nullable|NotNull|Metadata" - -# проверить, что код чистый -kotlinc -Werror -Wextra Demo.kt -d /dev/null - -# совместимость со старым языком -kotlinc -language-version 2.0 -api-version 2.0 Demo.kt -d out - -# список экспериментальных флагов -kotlinc -X -``` -Подробнее про чтение байт-кода: [Kotlin в байт-коде](../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `javac Demo.java` | `kotlinc Demo.kt` | -> | `java Demo` | `java -jar app.jar` или `kotlin DemoKt` | -> | `--release 21` | `-jvm-target 21` | -> | `-Xlint:all -Werror` | `-Wextra -Werror` | -> | JEP 330: `java Demo.java` | `kotlinc -script` или `.main.kts` | -> | `jshell` | `kotlinc` без аргументов | -> | нет аналога | `@file:DependsOn` в скриптах | -> -> **Главное практическое различие - stdlib не входит в платформу.** `javac` компилирует в байт-код, который запустится на любой JVM: вся библиотека уже есть в JDK. Kotlin-код зависит от `kotlin-stdlib.jar`, поэтому нужен либо `-include-runtime`, либо stdlib в classpath. Это цена за то, что stdlib развивается независимо от JDK и одинакова на всех платформах (JVM, JS, Native). -> -> **`-jvm-target 1.8` по умолчанию - решение о совместимости, а не недоработка.** Kotlin много лет был основным языком Android, где долго держался уровень Java 8. Дефолт остался консервативным, и его нужно задавать осознанно: современный серверный проект почти всегда хочет 17 или 21. -> -> **Скрипты в Kotlin сильнее, чем однофайловый запуск в Java.** JEP 330 позволяет `java Demo.java`, но только с тем, что есть в JDK: зависимостей нет. `@file:DependsOn` в `.main.kts` скачивает артефакты из Maven, то есть однофайловый скрипт может использовать корутины, сериализацию, HTTP-клиент. Это полноценная замена Python/Bash для задач автоматизации. -> -> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md) - -> [!warning] Подводные камни -> **`-jvm-target` по умолчанию 1.8.** Байт-код окажется совместимым с Java 8, и современные API JDK будут недоступны, а JIT не применит оптимизации новых версий. На это легко не обратить внимания - предупреждения нет. -> -> **JAR без `-include-runtime` не запускается:** -> ``` -> $ kotlinc App.kt -d app.jar && java -jar app.jar -> Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics -> ``` -> Либо добавьте флаг, либо укажите stdlib в `-cp`. -> -> **`-include-runtime` даёт JAR на ~5 МБ.** Для библиотеки это недопустимо: stdlib продублируется в каждой зависимости. Флаг только для конечных приложений. -> -> **`-e` и `-script` требуют разных форм записи.** `-e` принимает выражение в кавычках, `-script` - путь к `.kts`-файлу. Передать `.kts` в `-e` или выражение в `-script` не получится. -> -> **Файл скрипта должен называться `*.main.kts`, не `*.kts`.** Расширение `.kts` зарезервировано для Gradle-скриптов с другим шаблоном: `@file:DependsOn` в нём не сработает. -> -> **Первый запуск `@file:DependsOn` медленный.** Скачивание артефактов занимает секунды или десятки секунд. Дальше работает кеш `~/.m2`, но в CI без сохранённого кеша задержка повторится на каждом прогоне. -> -> **`-Werror` падает на предупреждениях из зависимостей.** Вызов устаревшего API библиотеки даст warning и остановит сборку. Нужен точечный `@Suppress("DEPRECATION")`, а не отключение флага. -> -> **REPL не видит зависимостей.** `kotlinc` без аргументов даёт только stdlib: подключить библиотеку нельзя (есть `-cp`, но он ненадёжен в интерактивном режиме). Для исследований с библиотеками используйте Kotlin Notebook или `.main.kts`. -> -> **Kotlin-компилятор не компилирует `.java`-файлы.** Передача `.java` в аргументах нужна только для разрешения ссылок; сами классы потом компилирует `javac`. Пропустить второй шаг - получить `ClassNotFoundException` на Java-классе. -> -> **`-module-name` влияет на видимость `internal`.** `internal` компилируется в публичное имя с суффиксом модуля (`helper$main`). Компиляция двух частей проекта с разными `-module-name` сделает их `internal`-члены взаимно недоступными, хотя в одном Gradle-модуле всё работало. -> -> **`-d /dev/null` для проверки работает не везде.** На Linux и macOS это способ скомпилировать без записи результата, но на некоторых версиях компилятор пытается создать каталог. Надёжнее писать во временный каталог и удалять. - -Далее: [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md) diff --git a/Kotlin/Инструментарий/Инструментарий.md b/Kotlin/Инструментарий/Инструментарий.md deleted file mode 100644 index 213a7bb..0000000 --- a/Kotlin/Инструментарий/Инструментарий.md +++ /dev/null @@ -1,205 +0,0 @@ ---- -title: Инструментарий -description: Обзор инструментов Kotlin — компилятор, сборка, кодогенерация, статический анализ — и как они складываются в проект -tags: - - kotlin - - tooling ---- - -Язык - это половина дела. Вторая половина - инструменты: чем компилировать, чем собирать, как генерировать код и как держать стиль. В Kotlin эти роли распределены между четырьмя слоями. - -```mermaid -graph TD - A["исходники .kt"] --> B["компилятор K2
kotlinc"] - B --> C[".class + kotlin_module"] - C --> D["jar / aar"] - E["Gradle + Kotlin DSL"] -->|"управляет"| B - E -->|"управляет"| F["KSP: кодогенерация"] - F -->|"генерирует .kt"| A - E -->|"управляет"| G["ktlint / detekt"] - G -->|"проверяет"| A -``` - -## Четыре слоя -| Слой | Инструмент | Зачем | -|---|---|---| -| компиляция | `kotlinc` / компилятор K2 | исходники → байт-код | -| сборка | Gradle + Kotlin DSL | зависимости, таски, многомодульность | -| кодогенерация | KSP | аннотации → сгенерированный код | -| качество | ktlint, detekt | формат и статический анализ | - -```kotlin -fun main() { - // этот файл можно скомпилировать тремя способами: - // 1. kotlinc Main.kt -include-runtime -d app.jar - // 2. gradle build (с build.gradle.kts) - // 3. kotlinc -script Main.main.kts (как скрипт) - println("любой способ даёт один байт-код") -} -``` - -## Какой инструмент когда -**`kotlinc` напрямую** - для одного файла, эксперимента, проверки факта о языке. Не масштабируется: нет управления зависимостями. - -**Gradle** - стандарт для всего остального. Kotlin DSL (`build.gradle.kts`) - это Kotlin-код, поэтому работает автодополнение и проверка типов. Подробнее: [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md) - -**Maven** поддерживается плагином `kotlin-maven-plugin`, но экосистема Kotlin ориентирована на Gradle: Multiplatform, Compose и большинство плагинов существуют только для него. - -**KSP** - когда нужно генерировать код по аннотациям (Room, Moshi, Dagger, kotlin-inject). Заменил `kapt`, который работал через Java-стабы и был в разы медленнее. Подробнее: [KSP](KSP.md) - -## Минимальный проект -```kotlin -// build.gradle.kts -plugins { - kotlin("jvm") version "2.4.0" - application -} - -repositories { mavenCentral() } - -dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0") - testImplementation(kotlin("test")) -} - -kotlin { - jvmToolchain(21) -} - -application { - mainClass.set("com.example.MainKt") -} - -tasks.test { - useJUnitPlatform() -} -``` -Четыре блока покрывают 90% нужд: плагины, репозитории, зависимости, версия JVM. Остальное - настройка под конкретную задачу. - -## Структура каталогов -``` -project/ -├── build.gradle.kts # сборка модуля -├── settings.gradle.kts # список модулей -├── gradle.properties # флаги Gradle и JVM -├── gradle/ -│ ├── libs.versions.toml # version catalog -│ └── wrapper/ # gradle-wrapper.jar + properties -├── gradlew # запуск без установленного Gradle -└── src/ - ├── main/kotlin/ - ├── main/resources/ - ├── test/kotlin/ - └── test/resources/ -``` -Подробнее: [Структура проекта](Структура%20проекта.md) - -## Что делает компилятор K2 -K2 - фронтенд компилятора, стабильный с Kotlin 2.0 и единственный в 2.x. Он быстрее старого примерно вдвое, точнее выводит типы и даёт более умные приведения. Важно, что K2 - это не только компиляция: один фронтенд обслуживает IDE, компилятор и инструменты анализа, поэтому подсветка в IDE и ошибки сборки наконец совпадают. Подробнее: [Компилятор K2](Компилятор%20K2.md) - -## Версии: три разных числа -| Что | Где задаётся | Что значит | -|---|---|---| -| версия Kotlin | `kotlin("jvm") version "2.4.0"` | версия компилятора и stdlib | -| `languageVersion` | `compilerOptions` | какие возможности языка разрешены | -| `jvmTarget` / toolchain | `jvmToolchain(21)` | версия байт-кода и JDK | - -```kotlin -// build.gradle.kts -plugins { kotlin("jvm") version "2.4.0" } - -kotlin { - jvmToolchain(21) // JDK для компиляции и запуска - - compilerOptions { - // можно ограничить язык более старой версией - // languageVersion.set(KotlinVersion.KOTLIN_2_1) - allWarningsAsErrors.set(false) - } -} -``` -Частая ошибка - считать их одним. Можно компилировать Kotlin 2.4 компилятором, ограничив язык версией 2.1 (для совместимости с библиотекой), и выдавать байт-код для JVM 8. - -## Качество кода -**ktlint** - форматирование по официальному style guide, почти без настроек. **detekt** - статический анализ: находит сложные функции, мёртвый код, подозрительные паттерны. Они дополняют друг друга: первый про форму, второй про смысл. Подробнее: [Линтеры и формат](Линтеры%20и%20формат.md) - -## Карта раздела -| Заметка | О чём | -|---|---| -| [kotlinc](kotlinc.md) | CLI, скрипты, REPL, ключевые флаги | -| [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md) | конфигурации, version catalog, таски | -| [Структура проекта](Структура%20проекта.md) | каталоги, модули, смешанные исходники | -| [Компилятор K2](Компилятор%20K2.md) | фазы, что изменилось, флаги | -| [KSP](KSP.md) | кодогенерация, процессоры, миграция с kapt | -| [Линтеры и формат](Линтеры%20и%20формат.md) | ktlint, detekt, CI | - -## Что ещё есть в экосистеме -| Инструмент | Задача | -|---|---| -| Dokka | документация из KDoc (аналог Javadoc) | -| binary-compatibility-validator | контроль публичного ABI библиотеки | -| kotlinx-benchmark | микробенчмарки на JMH | -| Kover | покрытие тестами | -| kotlin-multiplatform | общий код для JVM/JS/Native | -| Compose Multiplatform | UI поверх Multiplatform | -| kotlinx-serialization | сериализация через плагин компилятора | -| Amper | экспериментальная альтернатива Gradle от JetBrains | - -```kotlin -// build.gradle.kts с типичным набором для библиотеки -plugins { - kotlin("jvm") version "2.4.0" - kotlin("plugin.serialization") version "2.4.0" - id("org.jetbrains.dokka") version "2.0.0" - id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.17.0" - id("io.gitlab.arturbosch.detekt") version "1.23.8" -} - -repositories { mavenCentral() } - -dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") - testImplementation(kotlin("test")) -} -``` -Обратите внимание: `kotlinx-serialization` - это плагин компилятора, а не библиотека рефлексии. Сериализаторы генерируются при компиляции, поэтому работают без `kotlin-reflect` и на Kotlin/Native. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `javac` | `kotlinc` (K2) | -> | Maven или Gradle (Groovy) | Gradle + Kotlin DSL | -> | annotation processing (APT) | KSP | -> | Checkstyle, SpotBugs, PMD | ktlint, detekt | -> | Javadoc | Dokka (KDoc) | -> | Lombok | возможности языка (`data class`, свойства) | -> | один таргет - JVM | JVM, JS, Native, Wasm | -> -> **Gradle в Kotlin-проектах не альтернатива, а стандарт.** В Java-мире Maven остаётся распространённым выбором, и выбор между ними - вопрос предпочтений. В Kotlin инфраструктура (Multiplatform, Compose, KSP, большинство плагинов) строится вокруг Gradle, и Maven-плагин поддерживается по минимуму. Это сужает выбор, но концентрирует усилия. -> -> **KSP вместо APT - следствие того, что аннотационная обработка Java не видит Kotlin.** APT работает с Java-AST; Kotlin-код для него требует генерации стабов, то есть фактически компиляции дважды. KSP читает структуру Kotlin напрямую, поэтому работает в 2-4 раза быстрее и понимает nullable-типы, `suspend` и расширения. Цена - процессоры нужно писать заново: API другой. -> -> **Lombok не нужен.** Половина его функций (`@Data`, `@Getter`, `@Builder`, `@Value`) заменена возможностями языка: свойства, `data class`, значения по умолчанию в конструкторе. Это убирает целый класс проблем - Lombok работал через недокументированные API компилятора и ломался на каждом новом JDK. -> -> **Одна система сборки обслуживает несколько платформ.** Java-проект собирается для JVM; выход за её пределы (GWT, TeaVM, GraalVM) - отдельная история. В Kotlin JS, Native и Wasm - полноправные таргеты в том же `build.gradle.kts`, с общим исходным кодом. Цена - сложность конфигурации Multiplatform-проекта заметно выше. -> -> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md) - -> [!warning] Подводные камни -> **Версия плагина Kotlin и версия stdlib должны совпадать.** Плагин подтягивает stdlib своей версии автоматически; явное указание `implementation(kotlin("stdlib", "2.3.0"))` при плагине 2.4.0 даёт конфликт и предупреждение. Не указывайте stdlib вручную вообще. -> -> **`jvmTarget` без toolchain ломается на другом JDK.** Если локально JDK 21, а в CI - 17, сборка с `jvmTarget = "21"` упадёт. `jvmToolchain(21)` решает: Gradle сам скачает нужный JDK. -> -> **`jvmTarget` Kotlin и `targetCompatibility` Java должны совпадать.** В смешанном проекте разные значения дают ошибку «Inconsistent JVM-target compatibility detected». `jvmToolchain` настраивает оба. -> -> **Значение по умолчанию `jvmTarget` - 1.8.** Компиляция напрямую через `kotlinc` без флага даёт байт-код версии 52 (Java 8), даже на JDK 27. Gradle-плагин это переопределяет, но при ручной компиляции нужно помнить. -> -> **kapt и KSP в одном проекте замедляют сборку.** Если часть библиотек ещё на kapt, оба процессора работают, и стабы генерируются всё равно. Миграцию стоит делать целиком. -> -> **Gradle без `gradlew` - источник расхождений.** Версия Gradle, установленная локально, почти наверняка отличается от CI. Wrapper (`gradlew`) фиксирует версию в репозитории - коммитить его обязательно. -> -> **`kotlin.incremental` может давать ложные сборки.** При смене версии компилятора или странных ошибках «unresolved reference» на корректном коде помогает `./gradlew clean` или удаление `build/` и `~/.gradle/caches/build-cache-1`. -> -> **Плагины компилятора фиксируют версию Kotlin.** `kotlin("plugin.serialization") version "2.4.0"` должен совпадать с версией основного плагина - иначе ошибка о несовместимости плагина компилятора. Используйте version catalog, чтобы версия была в одном месте. - -Далее: [kotlinc](kotlinc.md) diff --git a/Kotlin/Инструментарий/Компилятор K2.md b/Kotlin/Инструментарий/Компилятор K2.md deleted file mode 100644 index 6f0773b..0000000 --- a/Kotlin/Инструментарий/Компилятор K2.md +++ /dev/null @@ -1,386 +0,0 @@ ---- -title: Компилятор K2 -description: Фазы компиляции, что изменилось по сравнению с K1, умные приведения, экспериментальные флаги -tags: - - kotlin - - tooling - - compiler ---- - -K2 - фронтенд компилятора Kotlin, стабильный с версии 2.0 и единственный в линии 2.x. Он быстрее старого примерно вдвое, точнее выводит типы и, что важнее всего, обслуживает одновременно компилятор и IDE - поэтому подсветка ошибок в редакторе совпадает с результатом сборки. - -```kotlin -interface Base { fun f(): String } -class A : Base { override fun f() = "A" } -class B : Base { override fun f() = "B" } - -fun describe(x: Any) { - if (x is A || x is B) { - // K2 выводит общий супертип Base для объединения типов - println(x.f()) - } -} - -fun main() { - describe(A()) // A - describe(B()) // B -} -``` -В K1 этот код не компилировался: после `is A || is B` переменная оставалась `Any`, и `x.f()` требовал явного приведения. - -## Фазы компиляции -Посмотреть реальные фазы можно флагами `-verbose -Xreport-perf`: -``` -$ kotlinc -verbose -Xreport-perf Demo.kt -d out -PERF: INIT 271 ms -PERF: ANALYZE 530 ms 11.311 loc/s -PERF: TRANSLATION to IR 144 ms 41.453 loc/s -PERF: KLIB WRITING 0 ms -PERF: IR LOWERING 122 ms 48.799 loc/s -PERF: BACKEND 75 ms 79.456 loc/s -``` - -```mermaid -graph LR - A[".kt"] --> B["INIT
загрузка classpath"] - B --> C["ANALYZE
фронтенд K2: FIR"] - C --> D["TRANSLATION
FIR → IR"] - D --> E["IR LOWERING
развёртка inline,
suspend, лямбды"] - E --> F["BACKEND
IR → байт-код"] - F --> G[".class + Metadata"] -``` - -| Фаза | Что происходит | -|---|---| -| INIT | чтение classpath, инициализация компилятора | -| ANALYZE | разбор, разрешение имён, вывод типов, проверки - фронтенд K2 строит FIR | -| TRANSLATION to IR | FIR превращается в промежуточное представление, общее для всех бэкендов | -| IR LOWERING | развёртка `inline`, CPS для `suspend`, генерация лямбд, `data class`-методов | -| BACKEND | IR → байт-код JVM (или JS, Native, Wasm) | - -Ключевое архитектурное решение - разделение на FIR (фронтенд) и IR (общий бэкенд). Фронтенд один для всех платформ, бэкенды разные. Отсюда и возможность Multiplatform: анализ типов выполняется один раз, генерация - под каждую цель. - -## Что изменилось в умных приведениях -K2 переписал анализ потока данных, и это самое заметное для повседневной работы улучшение. - -**Приведение через переменную с результатом проверки:** -```kotlin -fun viaVariable(x: Any?) { - val isString = x is String - if (isString) { - // K2 приводит тип по val с результатом проверки - println(x.length) - } -} - -fun main() { - viaVariable("шесть!") // 6 -} -``` - -**Приведение проникает в лямбду:** -```kotlin -fun intoLambda(x: Any) { - if (x is String) { - run { println(x.length) } // приведение видно внутри лямбды - } -} - -fun main() { - intoLambda("семь!!!") // 7 -} -``` - -**Nullable-функция вызывается после проверки:** -```kotlin -fun callable(f: (() -> Unit)?) { - if (f != null) { - f() // без ?.invoke() - } -} - -fun main() { - callable { println("лямбда вызвана") } // лямбда вызвана -} -``` - -**Исчерпывающий `when` по данным потока:** -```kotlin -sealed interface State -data object Loading : State -data class Done(val value: String) : State - -fun render(s: State) = when (s) { - Loading -> "загрузка" - is Done -> "готово: ${s.value}" -} - -fun main() { - println(render(Loading)) // загрузка - println(render(Done("данные"))) // готово: данные -} -``` -`else` не нужен - компилятор знает все варианты `sealed`. Подробнее: [Умные приведения](../Синтаксис%20и%20типы/Null-безопасность/Умные%20приведения.md) - -Чего K2 **не** делает - приведения для изменяемого свойства класса: -``` -$ kotlinc Demo.kt -d out -error: smart cast to 'String' is impossible, because 'value' is a mutable property -that could be mutated concurrently. -``` -Это принципиальное ограничение, а не недоработка: между проверкой и использованием другой поток может записать `null`. Решение - локальная копия: -```kotlin -class Holder { var value: String? = "есть" } - -fun main() { - val h = Holder() - val snapshot = h.value - if (snapshot != null) { - println(snapshot.length) // 4 - } -} -``` - -## Полезные флаги анализа -### -Wextra -Дополнительные проверки K2: неиспользуемые значения, подозрительные конструкции. -``` -kotlinc -Wextra Demo.kt -d out -``` - -### -Xreturn-value-checker -Предупреждает о проигнорированном результате функции - полезно для функций, чей результат нельзя терять: -``` -$ kotlinc -Xreturn-value-checker=full Demo.kt -d out -warning: unused return value of 'compute'. - compute() - ^^^^^^^ -``` -Режимы: `check` (только помеченные `@MustUseReturnValue`), `full` (все), `disable`. Особенно важно для `Result`-возвращающих функций: потерянный `Result` означает молча проглоченную ошибку. Подробнее: [Result и runCatching](../Исключения/Result%20и%20runCatching.md) - -### -Xexplicit-api -Для библиотек: требует явно указывать видимость и типы в публичном API. -``` -$ kotlinc -Xexplicit-api=strict Lib.kt -d out -error: visibility must be specified in explicit API mode. -class Public { -^^^^^^^^^^^^ -error: visibility must be specified in explicit API mode. - fun noExplicitType() = 42 - ^^^^^^^^^^^^^^^^^^ -``` -```kotlin -// так проходит explicit API mode -public class Service { - public fun compute(): Int = 42 - private fun internalHelper(): Int = 1 -} - -fun main() { - println(Service().compute()) // 42 -} -``` -В Gradle - `kotlin { explicitApi() }`. Это защита от случайного расширения публичного API: без флага `fun compute() = 42` публична и имеет выведенный тип, который может измениться при правке реализации. - -### Производительность -| Флаг | Что делает | -|---|---| -| `-Xbackend-threads=N` | кодогенерация в N потоков | -| `-Xreport-perf` | статистика по фазам (с `-verbose`) | -| `-Xlambdas={class\|indy}` | схема компиляции лямбд | -| `-Xstring-concat={indy\|indy-with-constants\|inline}` | схема конкатенации строк | - -`-Xlambdas=indy` (по умолчанию для JVM-таргета 8+) компилирует лямбды через `invokedynamic`, как в Java. `class` создаёт отдельные классы - нужно для совместимости с инструментами, которые ожидают классы лямбд. - -## Экспериментальные возможности языка -K2 - точка входа для новых конструкций. Полный список - `kotlinc -X`. - -| Флаг | Возможность | -|---|---| -| `-Xcontext-parameters` | context parameters (в 2.4 работают без флага) | -| `-Xwhen-guards` | `when` с охранными условиями | -| `-Xnon-local-break-continue` | `break`/`continue` из inline-лямбды | -| `-Xmulti-dollar-interpolation` | `$$` для литералов с `$` | -| `-Xname-based-destructuring` | деструктуризация по именам | -| `-Xnested-type-aliases` | `typealias` внутри класса | -| `-Xexplicit-backing-fields` | отдельный тип backing field и свойства | -| `-Xcontext-sensitive-resolution` | разрешение имён по ожидаемому типу | -| `-Xdata-flow-based-exhaustiveness` | исчерпывающий `when` по анализу потока | -| `-Xannotation-target-all` | `@all:` для аннотаций на свойствах | - -```kotlin -// context parameters — в 2.4 уже без флага -interface Logger { fun log(message: String) } - -context(logger: Logger) -fun process(data: String) { - logger.log("обработка: $data") -} - -fun main() { - val logger = object : Logger { - override fun log(message: String) = println(message) - } - with(logger) { - process("запись") // обработка: запись - } -} -``` -Подробнее: [Receivers и context parameters](../Функциональный%20Kotlin/Receivers%20и%20context%20parameters.md) - -## Три версии, которые путают -```kotlin -// build.gradle.kts -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.dsl.KotlinVersion - -kotlin { - jvmToolchain(21) - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) // версия байт-кода - languageVersion.set(KotlinVersion.KOTLIN_2_4) // возможности языка - apiVersion.set(KotlinVersion.KOTLIN_2_2) // доступный API stdlib - } -} -``` -| Опция | Отвечает за | Типичная ошибка | -|---|---|---| -| `jvmTarget` | формат `.class` | оставить дефолтный 1.8 | -| `languageVersion` | синтаксис и семантика | ожидать новые возможности при старом значении | -| `apiVersion` | какие функции stdlib видны | указать устаревшую версию | - -`apiVersion` ниже `languageVersion` - легальная комбинация: можно писать на новом языке, но не использовать новый API stdlib (чтобы библиотека работала со старым рантаймом). - -Важно: устаревшая `apiVersion` сама даёт предупреждение. В Kotlin 2.4 значение 2.1 выдаёт «API version 2.1 is deprecated», и с `allWarningsAsErrors` это ломает сборку. Подробнее: [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md) - -## Progressive mode -```kotlin -// build.gradle.kts -kotlin { - compilerOptions { - progressiveMode.set(true) - } -} -``` -Обычно исправления некорректного поведения проходят цикл миграции: предупреждение в одной версии, ошибка через две. Progressive mode применяет их сразу. Код, написанный в этом режиме, совместим с обычным, но не наоборот. Разумный выбор для приложений с частым обновлением Kotlin; для библиотек - осторожнее. - -## Плагины компилятора -K2 поддерживает плагины, работающие на уровне FIR/IR - они видят типы и могут генерировать код: - -| Плагин | Что делает | -|---|---| -| `kotlinx-serialization` | генерирует сериализаторы при компиляции | -| `all-open` | делает классы `open` (для Spring, Mockito) | -| `no-arg` | добавляет конструктор без аргументов (JPA) | -| `kotlin-spring` | предустановленный `all-open` для Spring-аннотаций | -| `kotlin-jpa` | предустановленный `no-arg` для JPA | -| `kotlin-lombok` | понимание Lombok-геттеров в смешанном коде | -| `power-assert` | подробные сообщения в `assert` | -| `compose` | оптимизация composable-функций | - -```kotlin -// build.gradle.kts -plugins { - kotlin("jvm") version "2.4.0" - kotlin("plugin.serialization") version "2.4.0" - kotlin("plugin.spring") version "2.4.0" - kotlin("plugin.jpa") version "2.4.0" -} -``` -Отличие плагина компилятора от KSP: плагин модифицирует IR напрямую (может менять существующий код), KSP только генерирует новые файлы. Плагины мощнее, но их API нестабилен между версиями Kotlin - поэтому сторонних плагинов мало. Подробнее: [KSP](KSP.md) - -## Инкрементальная компиляция -``` -# gradle.properties -kotlin.incremental=true -kotlin.incremental.useClasspathSnapshot=true -kotlin.daemon.jvmargs=-Xmx2g -``` -Компилятор отслеживает, какие файлы зависят от изменённых, и пересобирает только их. Снимок classpath позволяет не пересобирать модуль, если в зависимости изменилась только реализация, а не ABI. - -Что ломает инкрементальность: - -| Причина | Следствие | -|---|---| -| изменение `inline`-функции | пересборка всех её вызовов | -| изменение публичного API | пересборка зависимых модулей | -| `api` вместо `implementation` | пересборка потребителей | -| правка в `buildSrc` | пересборка всего проекта | -| `clean` | полная пересборка | - -## Диагностика проблем -``` -# подробный лог компиляции -./gradlew compileKotlin --info - -# время по фазам -kotlinc -verbose -Xreport-perf Demo.kt -d out - -# что попало в байт-код -kotlinc Demo.kt -d out && javap -p -cp out DemoKt - -# проверить без записи результата -./gradlew compileKotlin --rerun-tasks - -# сбросить кеши при странных ошибках -./gradlew clean && rm -rf .gradle .kotlin -``` -Если IDE показывает ошибку, а сборка проходит (или наоборот) - это признак расхождения версий компилятора в IDE и Gradle. В K2 такое встречается заметно реже, чем в K1, но при разных версиях плагина IDE и проекта возможно. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `javac` - один фронтенд и бэкенд | FIR (фронтенд) + IR (бэкенды) | -> | компилятор только для JVM | JVM, JS, Native, Wasm | -> | нет анализа потока для приведений | умные приведения | -> | `instanceof` с pattern matching (16+) | `is` + автоматическое приведение | -> | аннотационные процессоры (APT) | плагины компилятора + KSP | -> | `-Xlint` | `-Wextra`, `-Xreturn-value-checker` | -> | IDE имеет свой анализатор | K2 общий для IDE и сборки | -> -> **Общий фронтенд для IDE и компилятора - главное инфраструктурное отличие.** В Java IntelliJ использует собственную реализацию анализа, отдельную от `javac`, и расхождения возможны, но редки - язык прост. Kotlin с его выводом типов, умными приведениями и перегрузкой разрешения сложнее, и в эпоху K1 расхождения между IDE и сборкой были постоянной болью. K2 устранил причину: это один и тот же код. -> -> **Умные приведения заменяют то, что в Java решается pattern matching.** Java 16 добавил `if (o instanceof String s)` - явное связывание переменной. Kotlin идёт дальше: переменная не вводится, а тип существующей уточняется по всему блоку, включая проверки через `&&`, `||`, переменные с результатом и лямбды. Цена - ограничение на изменяемые свойства: Kotlin отказывается приводить `var`-свойство класса, потому что не может гарантировать неизменность между проверкой и использованием. Java такой гарантии и не пытается дать, потому что связывает значение, а не уточняет тип. -> -> **Плагины компилятора - то, чего в Java осознанно нет.** APT в Java может только генерировать новые файлы: модификация существующего кода запрещена (Lombok делает это через недокументированные API, и потому ломается на каждом JDK). Kotlin разрешает плагинам менять IR, что даёт `kotlinx-serialization` без рефлексии и Compose с его оптимизациями. Цена - API плагинов нестабилен, и сторонний плагин нужно обновлять под каждую версию Kotlin. -> -> **Многоплатформенность определила архитектуру.** `javac` компилирует в байт-код JVM - это его единственная задача. Kotlin выводит типы один раз во фронтенде, а затем генерирует код под JVM, JS, Native или Wasm из общего IR. Отсюда и сложность компилятора, и то, что stdlib не часть платформы, и возможность писать общий код для сервера и браузера. -> -> Сравнить: [javac](../../Программирование%20на%20Java/JDK/javac.md), [JIT](../../Программирование%20на%20Java/JVM/JIT.md) - -> [!warning] Подводные камни -> **Умное приведение не работает для `var`-свойства класса:** -> ```kotlin -> class Holder { var value: String? = "есть" } -> -> fun main() { -> val h = Holder() -> // if (h.value != null) println(h.value.length) // ошибка компиляции -> val snapshot = h.value -> println(if (snapshot != null) snapshot.length else 0) // 4 -> } -> ``` -> Сообщение: «smart cast is impossible, because 'value' is a mutable property that could be mutated concurrently». Это не обойти флагом - нужна локальная копия. -> -> **`-Xexplicit-api` обнаруживает проблему поздно.** Включение на существующей библиотеке даёт сотни ошибок сразу. Начинайте с `-Xexplicit-api=warning` и правьте постепенно, иначе включение откладывается навсегда. -> -> **Экспериментальные флаги `-X` не гарантируют совместимость.** Возможность может измениться или исчезнуть в следующей версии. Код, написанный с `-Xname-based-destructuring`, перестанет компилироваться при смене семантики флага. Для production-кода используйте только стабильные возможности. -> -> **`progressiveMode` - односторонний переход.** Код, написанный в progressive, компилируется и в обычном режиме, но обратное неверно: включение режима может сломать существующий код. Для библиотек, собираемых пользователями, это риск. -> -> **Устаревшая `apiVersion` вместе с `-Werror` роняет сборку.** В Kotlin 2.4 `apiVersion = 2.1` даёт предупреждение об устаревании, которое `allWarningsAsErrors` превращает в ошибку. Поддерживается около трёх предыдущих версий. -> -> **Изменение `inline`-функции обнуляет инкрементальность её вызовов.** Тело встроено во все точки вызова, поэтому пересобирается всё, что её использует - включая другие модули. `inline` в публичном API библиотеки фиксирует реализацию в ABI. -> -> **Нехватка памяти демона маскируется под медленную сборку.** Без `kotlin.daemon.jvmargs=-Xmx2g` компилятор на среднем проекте начинает тратить время на GC. Ошибки нет - просто сборка идёт в разы дольше. -> -> **`-Xbackend-threads` не всегда ускоряет.** На малых модулях накладные расходы на синхронизацию превышают выигрыш. Эффект заметен на модулях с тысячами файлов; измеряйте перед включением. -> -> **Расхождение IDE и сборки означает разные версии компилятора.** Плагин Kotlin в IDE и версия в `build.gradle.kts` должны соответствовать. Если IDE подсвечивает корректный код как ошибку, проверьте «Settings → Languages → Kotlin → Compiler» и перезапустите «Invalidate Caches». -> -> **`-Xreport-perf` без `-verbose` ничего не печатает.** Статистика идёт в лог, который включается отдельным флагом - без него команда выполняется молча, и кажется, что флаг не работает. -> -> **Плагин компилятора фиксирует версию Kotlin.** Обновление Kotlin требует обновления всех плагинов компилятора одновременно, иначе ошибка «plugin is not compatible». Это ограничивает скорость обновления на проектах с Compose или нестандартными плагинами. - -Далее: [KSP](KSP.md) diff --git a/Kotlin/Инструментарий/Линтеры и формат.md b/Kotlin/Инструментарий/Линтеры и формат.md deleted file mode 100644 index 8933e17..0000000 --- a/Kotlin/Инструментарий/Линтеры и формат.md +++ /dev/null @@ -1,385 +0,0 @@ ---- -title: Линтеры и формат -description: ktlint и detekt — автоформатирование, статический анализ, конфигурация и интеграция в CI -tags: - - kotlin - - tooling - - quality ---- - -Два инструмента с разными задачами: **ktlint** приводит код к официальному стилю и умеет исправлять сам, **detekt** ищет проблемы смысла - сложные функции, мёртвый код, подозрительные конструкции. - -| | ktlint | detekt | -|---|---|---| -| проверяет | форматирование и имена | сложность, запахи кода | -| автоисправление | да, большинство правил | частично | -| настройка | `.editorconfig`, минимум правил | `detekt.yml`, ~200 правил | -| нужен ли compile classpath | нет | нет (есть опциональный type resolution) | -| скорость | быстро | медленнее | - -```kotlin -// build.gradle.kts -plugins { - kotlin("jvm") version "2.4.0" - id("org.jlleitschuh.gradle.ktlint") version "12.1.2" - id("io.gitlab.arturbosch.detekt") version "1.23.8" -} - -repositories { mavenCentral() } - -detekt { - buildUponDefaultConfig = true - allRules = false -} -``` - -## ktlint -Реализует официальный Kotlin coding conventions. Настроек почти нет - это и есть идея: не спорить о стиле. - -На намеренно плохом файле: -```kotlin -// неформатированный код -// class bad_name { -// fun TooComplex( a:Int,b:Int ) : Int { -// var r = 0 -// if (a>0) { if (b>0) { if (a>b) { r=1 } else { r=2 } } else { r=3 } } else { r=4 } -// return r -// } -// } - -fun main() { - println("пример неформатированного кода выше") -} -``` -``` -$ ./gradlew ktlintMainSourceSetCheck -Bad.kt:3:7 Class or object name should start with an uppercase letter and use camel case (cannot be auto-corrected) -Bad.kt:4:9 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected) -Bad.kt:4:20 Unexpected spacing after "(" -Bad.kt:4:23 Missing spacing after ":" -Bad.kt:4:27 Missing spacing after "," -Bad.kt:6:14 Missing spacing around ">" -Bad.kt:6:18 A single line if-statement should be kept simple. The 'THEN' may not be wrapped in a block. (cannot be auto-corrected) -Bad.kt:6:20 Missing newline after '{' -``` -`./gradlew ktlintFormat` исправляет всё, что помечено как исправимое: -```kotlin -// после ktlintFormat -class BadName { - fun tooComplex( - a: Int, - b: Int, - ): Int { - var r = 0 - if (a > 0) { - if (b > 0) { - r = if (a > b) 1 else 2 - } else { - r = 3 - } - } else { - r = 4 - } - return r - } -} - -fun main() { - println(BadName().tooComplex(1, 2)) // 2 -} -``` -Пометка «cannot be auto-corrected» означает, что правило требует решения человека: переименование класса меняет API, разворот `if` в блок - семантику читаемости. - -## Настройка ktlint -Через `.editorconfig` в корне проекта: -``` -root = true - -[*.{kt,kts}] -indent_size = 4 -indent_style = space -max_line_length = 120 -insert_final_newline = true -ij_kotlin_allow_trailing_comma = true -ij_kotlin_allow_trailing_comma_on_call_site = true -ktlint_standard_no-wildcard-imports = disabled -ktlint_standard_function-naming = disabled -ktlint_code_style = ktlint_official -``` -| Параметр | Значение | -|---|---| -| `ktlint_code_style` | `ktlint_official`, `intellij_idea`, `android_studio` | -| `max_line_length` | длина строки (`off` отключает) | -| `ktlint_standard_ = disabled` | отключить конкретное правило | -| `ij_kotlin_allow_trailing_comma` | разрешить висячую запятую | - -Важно, что `.editorconfig` читают и ktlint, и IntelliJ IDEA - то есть форматирование в IDE совпадает с проверкой в CI. Это главный аргумент за `.editorconfig` вместо настроек плагина. - -Отключение правила в коде: -```kotlin -@Suppress("ktlint:standard:max-line-length") -val longLine = "очень длинная строка, которую нельзя разбить по смыслу, например регулярное выражение" - -fun main() { - println(longLine.length) // 85 -} -``` - -## detekt -Около 200 правил, разбитых на наборы. Запуск на том же плохом файле: -``` -$ ./gradlew detekt -Bad.kt:3:7: The file name 'Bad' does not match the name of the single top-level declaration 'bad_name'. [MatchingDeclarationName] -Bad.kt:1:1: The package declaration does not match the actual file location. [InvalidPackageDeclaration] -Bad.kt:3:7: Class and Object names should match the pattern: [A-Z][a-zA-Z0-9]* [ClassNaming] -Bad.kt:4:9: Function names should match the pattern: [a-z][a-zA-Z0-9]* [FunctionNaming] -Bad.kt:6:72: This expression contains a magic number. [MagicNumber] -Bad.kt:9:24: Private property `x` is unused. [UnusedPrivateProperty] -``` -Обратите внимание на отличие от ktlint: detekt нашёл несовпадение имени файла с объявлением, магические числа и неиспользуемое свойство - это вопросы смысла, а не форматирования. - -| Набор правил | Что проверяет | -|---|---| -| `complexity` | длинные функции, высокая цикломатическая сложность, много параметров | -| `coroutines` | неправильное использование `suspend`, `GlobalScope`, блокирующие вызовы | -| `empty-blocks` | пустые `catch`, `if`, функции | -| `exceptions` | `catch (e: Exception)`, проглоченные исключения, `throw` в `finally` | -| `naming` | соответствие конвенциям имён | -| `performance` | `forEach` вместо `for`, лишние аллокации, `spread`-оператор | -| `potential-bugs` | `!!`, небезопасные приведения, игнорируемый результат | -| `style` | длинные строки, wildcard-импорты, магические числа | - -Наиболее ценные на практике - `coroutines` и `potential-bugs`: они находят реальные ошибки, а не стилистические отклонения. - -## Конфигурация detekt -``` -./gradlew detektGenerateConfig # создаст config/detekt/detekt.yml -``` -```yaml -# config/detekt/detekt.yml -build: - maxIssues: 0 - excludeCorrectable: false - -complexity: - LongMethod: - active: true - threshold: 60 - LongParameterList: - active: true - functionThreshold: 6 - ignoreDefaultParameters: true - CyclomaticComplexMethod: - active: true - threshold: 15 - -style: - MagicNumber: - active: true - ignoreNumbers: ['-1', '0', '1', '2'] - ignoreHashCodeFunction: true - excludes: ['**/test/**'] - MaxLineLength: - maxLineLength: 120 - WildcardImport: - active: true - -potential-bugs: - UnsafeCallOnNullableType: - active: true - -coroutines: - GlobalCoroutineUsage: - active: true - InjectDispatcher: - active: true -``` -```kotlin -// build.gradle.kts -detekt { - buildUponDefaultConfig = true // своя конфигурация дополняет дефолтную - config.setFrom("$projectDir/config/detekt/detekt.yml") - baseline = file("$projectDir/config/detekt/baseline.xml") - allRules = false - parallel = true -} -``` -`buildUponDefaultConfig = true` критично: без него в `detekt.yml` нужно перечислить **все** правила, и забытое правило окажется отключённым. - -## Baseline для существующего проекта -Включение detekt на большой кодовой базе даёт сотни замечаний. Baseline фиксирует существующие нарушения, чтобы проверять только новый код: -``` -./gradlew detektBaseline # создаст baseline.xml -``` -```xml - - - - - MagicNumber:Bad.kt$6:72 - UnusedPrivateProperty:Bad.kt$private val x - - -``` -Новые нарушения ломают сборку, старые игнорируются. Это единственный реалистичный путь внедрения в существующий проект - иначе включение откладывается навсегда. - -## Подавление правил -```kotlin -@Suppress("MagicNumber") -fun httpTimeout() = 30_000 - -class Legacy { - @Suppress("TooGenericExceptionCaught", "SwallowedException") - fun risky() { - try { - error("сбой") - } catch (e: Exception) { - // осознанно проглочено: вызывающий код не должен знать о сбое - } - } -} - -fun main() { - println(httpTimeout()) // 30000 - Legacy().risky() -} -``` -Подавление - нормальная практика, когда правило не применимо к конкретному случаю. Плохая практика - подавлять правило целиком в `detekt.yml`, если оно нарушается в двух местах из тысячи. - -## Type resolution -detekt может работать с информацией о типах - тогда доступны правила, требующие знания classpath: -```kotlin -// build.gradle.kts -tasks.withType().configureEach { - jvmTarget = "21" -} - -tasks.register("detektWithTypes") { - description = "detekt с разрешением типов" - setSource(files("src/main/kotlin")) - classpath.setFrom( - configurations.getByName("detekt"), - sourceSets["main"].compileClasspath, - ) -} -``` -Правила вроде `UnnecessaryNotNullOperator` или `RedundantSuspendModifier` без classpath работать не могут. Цена - заметно медленнее. - -## Интеграция в CI -```yaml -# .github/workflows/check.yml -name: check -on: [push, pull_request] - -jobs: - quality: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 21 - - uses: gradle/actions/setup-gradle@v4 - - run: ./gradlew ktlintCheck detekt --continue - - uses: actions/upload-artifact@v4 - if: always() - with: - name: reports - path: | - build/reports/detekt/ - build/reports/ktlint/ -``` -`--continue` важен: без него сборка остановится на первой проваленной проверке, и вы не увидите остальные замечания. - -## Git-хук -```bash -# .git/hooks/pre-commit -#!/bin/sh -./gradlew ktlintFormat --daemon -q || exit 1 -git add -u -``` -Автоформат перед коммитом избавляет от «правок форматирования» в diff. `detekt` в хук обычно не ставят - он слишком медленный для каждого коммита. - -Плагин ktlint умеет установить хук сам: -``` -./gradlew addKtlintFormatGitPreCommitHook -``` - -## Таски -| Таск | Что делает | -|---|---| -| `ktlintCheck` | проверка всех source sets | -| `ktlintFormat` | автоисправление | -| `ktlintMainSourceSetCheck` | только `main` | -| `detekt` | анализ | -| `detektBaseline` | создать baseline | -| `detektGenerateConfig` | создать `detekt.yml` | -| `detektMain` | только `main` (при `buildUponDefaultConfig`) | - -## Альтернативы и дополнения -| Инструмент | Задача | -|---|---| -| Spotless | единая обёртка для форматирования (включая ktlint) | -| Kover | покрытие тестами | -| binary-compatibility-validator | контроль публичного ABI | -| `-Xexplicit-api` | требовать явные типы в публичном API | -| Qodana / SonarQube | анализ на стороне сервера | - -`-Xexplicit-api` - встроенная в компилятор проверка, дополняющая линтеры: она требует явно указывать видимость и типы возврата для публичных объявлений. Подробнее: [Компилятор K2](Компилятор%20K2.md) - -## Разумный минимум -1. `.editorconfig` с `ktlint_official` - согласованный стиль в IDE и CI. -2. `ktlintFormat` в pre-commit хуке - форматирование не попадает в diff. -3. detekt с `buildUponDefaultConfig` и baseline - новый код проверяется, старый не блокирует. -4. `ktlintCheck detekt --continue` в CI с публикацией отчётов. -5. `-Xexplicit-api=strict` для библиотек. - -Этого достаточно; дальнейшая настройка правил - вопрос договорённостей команды, а не инструментов. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | Checkstyle | ktlint | -> | SpotBugs, PMD, ErrorProne | detekt | -> | google-java-format, Spotless | ktlint (или Spotless + ktlint) | -> | стиль - вопрос договорённости | официальный coding conventions | -> | конфигурация XML | `.editorconfig` + YAML | -> | нет аналога | `-Xexplicit-api` в компиляторе | -> -> **Официальный style guide меняет роль форматтера.** В Java конкурируют несколько стилей (Oracle, Google, AOSP), и Checkstyle приходится настраивать под выбранный. Kotlin имеет один официальный стиль от JetBrains, реализованный и в IDE, и в ktlint - настройка почти не нужна, и споров о форматировании не возникает. Это сознательная позиция: единый стиль важнее возможности выбрать свой. -> -> **`.editorconfig` как единый источник правды - важнее, чем кажется.** В Java настройки Checkstyle и настройки форматирования IDE - разные файлы, которые расходятся. Файл `.editorconfig` читают и IntelliJ, и ktlint, поэтому «форматирование в IDE» и «проверка в CI» - буквально одна конфигурация. Это устраняет классический конфликт, когда IDE переформатирует код так, что падает сборка. -> -> **detekt берёт на себя то, что в Java распределено между тремя инструментами.** SpotBugs анализирует байт-код, PMD - исходники, ErrorProne встроен в компиляцию. detekt - один инструмент на исходниках, с опциональным разрешением типов. Проще в настройке, но находит меньше: без анализа байт-кода часть проблем (например, утечки ресурсов по потоку данных) недоступна. -> -> **Правила для корутин - специфика, которой нет аналога.** Набор `coroutines` в detekt находит `GlobalScope`, блокирующие вызовы внутри `suspend`, захардкоженные диспетчеры. В Java параллельные ошибки (блокировка в пуле, утечка `ExecutorService`) ловятся хуже, потому что нет структуры, которую можно статически проверить. Структурированная конкурентность Kotlin даёт линтеру опору. Подробнее: [Структурированная конкурентность](../Корутины/Структурированная%20конкурентность.md) -> -> Сравнить: [javac](../../Программирование%20на%20Java/JDK/javac.md) - -> [!warning] Подводные камни -> **`buildUponDefaultConfig = false` (по умолчанию) отключает все неперечисленные правила.** Свой `detekt.yml` без этого флага заменяет дефолтную конфигурацию целиком: сборка зелёная, проверок нет. Это самая частая ошибка настройки detekt. -> -> **detekt падает на слишком новом JDK.** Версия 1.23.8 на JDK 26 завершается с `GradleException: 26.0.1` - встроенный парсер не знает такой версии байт-кода. Запускайте таск на поддерживаемом JDK: `jvmToolchain` для компиляции и отдельный JDK для detekt, или обновите detekt. -> -> **ktlint и detekt могут конфликтовать по правилам.** Оба проверяют длину строки и имена, и при разных настройках код, исправленный одним, нарушает правило другого. Задавайте `max_line_length` в `.editorconfig`, а в detekt - `MaxLineLength.maxLineLength` с тем же значением. -> -> **`ktlintFormat` может изменить семантику в редких случаях.** Перестановка импортов и добавление висячих запятых безопасны, но правила вроде `if-else` → выражение меняют структуру кода. После массового автоформата запускайте тесты. -> -> **Baseline легко превращается в свалку.** Файл создаётся раз и начинает жить вечно: правила «починим позже» не выполняются. Перегенерируйте baseline периодически и следите, чтобы он сокращался. -> -> **Правила с type resolution требуют classpath и замедляют анализ.** Без `classpath.setFrom(...)` правила вроде `UnnecessaryNotNullOperator` молча не работают. С ним detekt становится в разы медленнее - обычно такой таск запускают только в CI. -> -> **`MagicNumber` шумит в тестах.** Тесты полны литералов, и правило даёт десятки замечаний. Добавьте `excludes: ['**/test/**']` в конфигурацию, иначе придётся подавлять в каждом файле. -> -> **Висячие запятые требуют согласованности.** `ij_kotlin_allow_trailing_comma = true` в `.editorconfig` и настройка IDE должны совпадать, иначе IDE удаляет запятую, а ktlint требует её вернуть - бесконечный цикл правок. -> -> **Версия плагина ktlint и версия самого ktlint - разные числа.** `org.jlleitschuh.gradle.ktlint` версии 12.x по умолчанию использует ktlint 1.x; при необходимости версия задаётся через `ktlint { version.set("1.5.0") }`. Несогласованность приводит к разным результатам локально и в CI. -> -> **Проверка в CI без `--continue` скрывает часть проблем.** Сборка падает на `ktlintCheck`, и `detekt` не выполняется - разработчик исправляет форматирование, пушит, и только тогда узнаёт о замечаниях detekt. Два цикла вместо одного. -> -> **Git-хук с detekt делает коммиты медленными.** Полный анализ среднего проекта - десятки секунд. В хук ставьте только `ktlintFormat`, detekt оставьте для CI. -> -> **`@Suppress` с неверным именем правила молча не работает.** Имена правил detekt (`MagicNumber`) и ktlint (`ktlint:standard:max-line-length`) записываются по-разному; опечатка не вызывает ошибки - просто подавление не применяется. - -Далее: [Тестирование](../Тестирование/Тестирование.md) diff --git a/Kotlin/Инструментарий/Структура проекта.md b/Kotlin/Инструментарий/Структура проекта.md deleted file mode 100644 index d950923..0000000 --- a/Kotlin/Инструментарий/Структура проекта.md +++ /dev/null @@ -1,379 +0,0 @@ ---- -title: Структура проекта -description: Каталоги исходников, пакеты, многомодульность, смешанный Kotlin+Java код и convention plugins -tags: - - kotlin - - tooling - - gradle ---- - -Структура Kotlin-проекта наследует соглашения Maven, но с двумя отличиями: каталог называется `kotlin`, а требование «путь пакета = путь каталога» необязательно. - -``` -myapp/ -├── settings.gradle.kts -├── build.gradle.kts -├── gradle.properties -├── gradle/ -│ ├── libs.versions.toml -│ └── wrapper/ -├── gradlew -└── src/ - ├── main/ - │ ├── kotlin/com/example/ - │ └── resources/ - └── test/ - ├── kotlin/com/example/ - └── resources/ -``` - -```kotlin -// src/main/kotlin/com/example/Main.kt -package com.example - -fun main() { - println("стандартная структура") -} -``` -Главный класс для `application` - `com.example.MainKt`: имя файла с суффиксом `Kt`. Подробнее: [Kotlin в байт-коде](../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md) - -## Source sets -| Source set | Каталоги | Зависимости | -|---|---|---| -| `main` | `src/main/kotlin`, `src/main/java`, `src/main/resources` | `implementation`, `api` | -| `test` | `src/test/kotlin`, `src/test/java`, `src/test/resources` | `testImplementation` + всё из `main` | - -Gradle-плагин Kotlin добавляет `src/main/java` в Kotlin source set, поэтому `.kt`-файл, лежащий в `src/main/java`, скомпилируется: -``` -$ ls build/classes/kotlin/main/demo/ -ExtraKt.class # из src/main/java/demo/Extra.kt -KotlinImpl.class # из src/main/kotlin/demo/KotlinImpl.kt -``` -Работает, но вводит в заблуждение - держите Kotlin в `src/main/kotlin`. - -Добавить свой source set: -```kotlin -// build.gradle.kts -sourceSets { - val integrationTest by creating { - kotlin.srcDir("src/integrationTest/kotlin") - resources.srcDir("src/integrationTest/resources") - compileClasspath += sourceSets["main"].output - runtimeClasspath += output + compileClasspath - } -} - -val integrationTest by tasks.registering(Test::class) { - testClassesDirs = sourceSets["integrationTest"].output.classesDirs - classpath = sourceSets["integrationTest"].runtimeClasspath - useJUnitPlatform() -} -``` -Для этой задачи есть и готовое решение - плагин `jvm-test-suite`: -```kotlin -// build.gradle.kts -testing { - suites { - val test by getting(JvmTestSuite::class) { - useKotlinTest() - } - register("integrationTest") { - useKotlinTest() - dependencies { - implementation(project()) - } - } - } -} -``` - -## Пакеты и каталоги -В Kotlin объявленный пакет не обязан совпадать с путём каталога: -```kotlin -// файл src/main/kotlin/Utils.kt — лежит в корне -package com.example.utils - -fun helper() = "работает из любого каталога" - -fun main() { - println(helper()) // работает из любого каталога -} -``` -Компилятор примет это без ошибки. Тем не менее совпадение - сильное соглашение: иначе навигация по проекту ломается, а IDE предупреждает. Исключение, которое делают осознанно: мелкие файлы с функциями верхнего уровня можно держать плоско. - -Правило, которое действительно обязательно: один файл может содержать сколько угодно классов верхнего уровня, и их имена не связаны с именем файла. Это заметно отличается от Java. Подробнее: [Пакеты и импорты](../Синтаксис%20и%20типы/Базовый%20синтаксис/Пакеты%20и%20импорты.md) - -## Как раскладывать файлы -``` -com/example/ -├── Main.kt # точка входа -├── domain/ -│ ├── User.kt # data class User + связанные типы -│ ├── Order.kt -│ └── OrderStatus.kt # enum или sealed -├── repository/ -│ ├── UserRepository.kt # interface -│ └── UserRepositoryImpl.kt -├── service/ -│ └── OrderService.kt -├── api/ -│ ├── Routes.kt # функции верхнего уровня -│ └── Dto.kt # несколько data class в одном файле -└── util/ - ├── Strings.kt # функции-расширения - └── Dates.kt -``` -Две практики, специфичные для Kotlin: - -**Мелкие связанные типы - в один файл.** `sealed interface Result` вместе со всеми наследниками, DTO одного эндпоинта, `enum` рядом с использующим классом. В Java каждый публичный класс требовал отдельного файла; здесь это выбор, а не обязанность. - -**Расширения группируются по типу-получателю, а не по назначению.** `Strings.kt` со всеми расширениями `String`, `Collections.kt` - для коллекций. Это заменяет утилитные классы вроде `StringUtils`. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md) - -## Смешанный Kotlin + Java -Один модуль может содержать оба языка, причём зависимости допускаются в обе стороны одновременно: -``` -src/main/ -├── kotlin/demo/KotlinImpl.kt -└── java/demo/ - ├── JavaBase.java - └── UsesKotlin.java -``` -```java -// src/main/java/demo/JavaBase.java -package demo; - -public abstract class JavaBase { - public abstract String name(); - public String describe() { return "Java-база: " + name(); } -} -``` -```kotlin -// src/main/kotlin/demo/KotlinImpl.kt -// Kotlin наследуется от Java-класса -// class KotlinImpl : JavaBase() { -// override fun name() = "из Kotlin" -// } - -fun main() { - println("Java-база: из Kotlin") // Java-база: из Kotlin -} -``` -```java -// src/main/java/demo/UsesKotlin.java -package demo; - -public class UsesKotlin { - // Java использует Kotlin-класс, который наследуется от Java-класса - public static String call() { return new KotlinImpl().describe(); } -} -``` -Это работает, хотя выглядит как циклическая зависимость. Порядок компиляции: - -```mermaid -graph LR - A["Java-исходники"] -->|"читаются как стабы"| B["kotlinc:
компиляция Kotlin"] - B --> C["Kotlin .class"] - C -->|"в classpath"| D["javac:
компиляция Java"] - D --> E["Java .class"] -``` - -`kotlinc` получает `.java`-файлы в аргументах и разбирает их для разрешения ссылок, не компилируя. Затем `javac` компилирует Java, уже видя готовые Kotlin-классы. Поэтому Java может наследоваться от Kotlin-класса, который наследуется от Java-класса. Подробнее: [Взаимодействие с Java](../Взаимодействие%20с%20Java/Взаимодействие%20с%20Java.md) - -Ограничение: эта схема работает в пределах одного модуля. Между модулями циклов быть не может. - -## Многомодульный проект -``` -myapp/ -├── settings.gradle.kts -├── build.gradle.kts -├── gradle/libs.versions.toml -├── build-logic/ # convention plugins -│ ├── settings.gradle.kts -│ └── src/main/kotlin/ -│ └── kotlin-conventions.gradle.kts -├── domain/ # чистая логика, без зависимостей -├── data/ # БД, HTTP -├── service/ # бизнес-логика -└── app/ # точка входа, DI -``` -```kotlin -// settings.gradle.kts -rootProject.name = "myapp" - -include("domain", "data", "service", "app") -``` -```kotlin -// корневой build.gradle.kts -plugins { - kotlin("jvm") version "2.4.0" apply false -} - -subprojects { - apply(plugin = "org.jetbrains.kotlin.jvm") - - repositories { mavenCentral() } - - dependencies { - "testImplementation"(kotlin("test")) - } -} -``` -```kotlin -// app/build.gradle.kts -dependencies { - implementation(project(":service")) - implementation(project(":data")) -} -``` -Направление зависимостей важно: `app` → `service` → `data` → `domain`, и `domain` не зависит ни от чего. Gradle обнаружит цикл между модулями и откажется собирать. - -## Convention plugins -`subprojects { }` в корневом файле работает, но несовместим с configuration cache и не даёт типизации. Правильное решение - convention plugin: -``` -build-logic/ -├── settings.gradle.kts -├── build.gradle.kts -└── src/main/kotlin/kotlin-conventions.gradle.kts -``` -```kotlin -// build-logic/build.gradle.kts -plugins { - `kotlin-dsl` -} - -repositories { - mavenCentral() - gradlePluginPortal() -} - -dependencies { - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.0") -} -``` -```kotlin -// build-logic/src/main/kotlin/kotlin-conventions.gradle.kts -plugins { - kotlin("jvm") -} - -repositories { mavenCentral() } - -kotlin { - jvmToolchain(21) - compilerOptions { - allWarningsAsErrors.set(true) - } -} - -dependencies { - "testImplementation"(kotlin("test")) -} - -tasks.withType { - useJUnitPlatform() -} -``` -```kotlin -// settings.gradle.kts -pluginManagement { - includeBuild("build-logic") -} - -rootProject.name = "myapp" -include("domain", "data", "service", "app") -``` -```kotlin -// domain/build.gradle.kts — одна строка вместо дублирования -plugins { - id("kotlin-conventions") -} -``` -Имя плагина - это имя файла без `.gradle.kts`. Такой подход даёт типизированную конфигурацию, работает с configuration cache и не требует ничего знать о других модулях. - -Исторически ту же роль играл каталог `buildSrc/`. Он проще (не нужен `includeBuild`), но у него есть недостаток: любое изменение в `buildSrc` инвалидирует кеш всего проекта. Для больших проектов предпочтителен `build-logic` как included build. - -## Ресурсы -```kotlin -fun main() { - // чтение из src/main/resources - val stream = object {}.javaClass.getResourceAsStream("/config.properties") - println("ресурс найден: ${stream != null}") // ресурс найден: false - - // идиоматичнее — через класс или top-level объект - val text = ClassLoader.getSystemResourceAsStream("missing.txt") - ?.bufferedReader()?.readText() ?: "ресурс отсутствует" - println(text) // ресурс отсутствует -} -``` -Путь с `/` - от корня classpath, без `/` - относительно пакета класса. Файлы из `src/main/resources` попадают в корень JAR. Для функций верхнего уровня ссылки на класс нет, поэтому используют `object {}.javaClass` или `ClassLoader`. - -## Файлы, которые нужны в репозитории -| Путь | Коммитить | -|---|---| -| `gradlew`, `gradlew.bat` | да | -| `gradle/wrapper/gradle-wrapper.jar` | да | -| `gradle/wrapper/gradle-wrapper.properties` | да | -| `gradle/libs.versions.toml` | да | -| `gradle.properties` | да | -| `.gradle/`, `build/` | нет | -| `local.properties` | нет | -| `.idea/` | частично (`codeStyles`, `inspectionProfiles`) | -| `*.iml` | нет | - -``` -# .gitignore -build/ -.gradle/ -local.properties -*.iml -.idea/* -!.idea/codeStyles/ -!.idea/inspectionProfiles/ -.kotlin/ -``` -Каталог `.kotlin/` появился в Kotlin 2.x - это служебные данные компилятора, их не коммитят. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `src/main/java` | `src/main/kotlin` | -> | один публичный класс на файл | сколько угодно | -> | имя файла = имя публичного класса | не связаны | -> | пакет обязан совпадать с каталогом | не обязан | -> | утилитный класс `StringUtils` | файл `Strings.kt` с расширениями | -> | `Main.class` | `MainKt.class` | -> -> **Освобождение от «один класс на файл» меняет раскладку кода.** В Java структура каталогов повторяла структуру типов: 20 классов - 20 файлов. В Kotlin файл - единица логической группировки, а не технического ограничения. `sealed interface` со всеми вариантами в одном файле читается целиком, а не по кускам. Побочный эффект: навигация «по файлам» работает хуже, и важнее осмысленные имена файлов. -> -> **Несовпадение пакета и каталога разрешено, но соглашение всё равно действует.** Kotlin не требует соответствия, потому что для компилятора оно не нужно: пакет указан в файле. Однако IDE, инструменты анализа и люди ожидают совпадения, поэтому отклоняются от него редко и только для плоских файлов с функциями. -> -> **Смешанная компиляция в одном модуле - то, чего нет в однодходовом `javac`.** Двухфазная схема (kotlinc читает Java-стабы, затем javac видит Kotlin-классы) позволяет взаимные ссылки между языками внутри модуля. Это ключевая возможность для миграции: можно конвертировать по одному файлу, не трогая остальное. Ограничение - только в пределах модуля. -> -> **Convention plugins - ответ Gradle на отсутствие наследования `pom.xml`.** В Maven общая конфигурация выносится в parent POM, и это декларативно. В Gradle аналога не было: писали `subprojects { }` или `allprojects { }`, что работает, но несовместимо с configuration cache и лишено типизации. Convention plugin - это обычный Gradle-плагин, лежащий в проекте: типизированный, кешируемый, переиспользуемый. -> -> Сравнить: [javac](../../Программирование%20на%20Java/JDK/javac.md), [jar](../../Программирование%20на%20Java/JDK/jar.md) - -> [!warning] Подводные камни -> **Главный класс для `application` - не имя файла.** Для `src/main/kotlin/com/example/Main.kt` это `com.example.MainKt`, а не `com.example.Main`. Ошибка проявляется как `ClassNotFoundException` при `./gradlew run`. Если в файле стоит `@file:JvmName("App")`, то класс - `com.example.App`. -> -> **Kotlin-файлы в `src/main/java` компилируются молча.** Плагин добавляет этот каталог в Kotlin source set, так что ошибки не будет - но структура станет путаной, а инструменты, ожидающие Java в `java/`, могут сбиться. -> -> **`buildSrc` инвалидирует кеш всего проекта.** Любая правка в `buildSrc` заставляет Gradle пересобрать все модули. На больших проектах это минуты на каждое изменение конфигурации - используйте `build-logic` как included build. -> -> **`subprojects { }` несовместим с configuration cache.** Он обращается к другим проектам в фазе конфигурации, что кеш запрещает. При включении `org.gradle.configuration-cache=true` появятся ошибки «invocation of 'Project.subprojects'». Решение - convention plugins. -> -> **Циклы между модулями запрещены.** Если `data` ссылается на `service`, а `service` на `data`, Gradle откажется собирать с ошибкой «Circular dependency between tasks». Внутри одного модуля цикл Kotlin↔Java допустим, между модулями - нет. Общие типы выносят в третий модуль. -> -> **Ресурс не найден из функции верхнего уровня.** `javaClass` недоступен вне класса; `this::class.java` в top-level функции не скомпилируется. Используйте `object {}.javaClass.getResourceAsStream(...)` или `ClassLoader.getSystemResourceAsStream(...)`. -> -> **Путь к ресурсу чувствителен к ведущему слэшу.** `getResourceAsStream("config.txt")` ищет относительно пакета класса, `getResourceAsStream("/config.txt")` - от корня classpath. Первый вариант неожиданно возвращает `null`, если класс лежит в пакете. -> -> **`gradle-wrapper.jar` обязателен в репозитории.** Без него `./gradlew` не работает, и сборка пойдёт локальным Gradle другой версии. Если политика запрещает бинарники, добавьте в CI шаг `gradle/actions/wrapper-validation`. -> -> **Переименование файла меняет имя класса.** `Utils.kt` даёт `UtilsKt`, после переименования в `Helpers.kt` - `HelpersKt`. Для Java-клиентов и рефлексии это ломающее изменение; фиксируйте имя через `@file:JvmName`. -> -> **Тесты не видят `internal` из другого модуля.** `internal` в Kotlin - это видимость в пределах модуля компиляции, и `src/test` - отдельный source set. Gradle-плагин специально связывает `test` с `main`, поэтому тесты `internal`-члены видят; но тесты **другого** модуля - нет. -> -> **Файл без `package` попадает в корневой пакет.** Такой класс не импортируется из других пакетов нормальным способом и недоступен из Java-кода в пакете. Всегда объявляйте пакет, кроме скриптов. - -Далее: [Компилятор K2](Компилятор%20K2.md) diff --git a/Kotlin/Исключения/Result и runCatching.md b/Kotlin/Исключения/Result и runCatching.md deleted file mode 100644 index cb4ee4a..0000000 --- a/Kotlin/Исключения/Result и runCatching.md +++ /dev/null @@ -1,336 +0,0 @@ ---- -title: Result и runCatching -description: Ошибка как значение: Result, runCatching, свои sealed-типы и когда что выбирать -tags: - - kotlin - - exceptions ---- - -Исключение - не единственный способ сообщить об ошибке. Ошибку можно вернуть как значение, и тогда компилятор заставит её обработать. - -Три уровня, от простого к гибкому: - -| Способ | Что несёт ошибка | Когда | -|---|---|---| -| `T?` | ничего, только «не получилось» | причина не важна | -| `Result` | `Throwable` | причина нужна, но типизировать её незачем | -| свой `sealed`-тип | что угодно | причин несколько и они разные | - -## Nullable как простейший Result -Для одной очевидной причины сбоя `null` достаточно: -```kotlin -fun main() { - println("42".toIntOrNull()) // 42 - println("abc".toIntOrNull()) // null - println("abc".toIntOrNull() ?: 0) // 0 -} -``` -Стандартная библиотека придерживается соглашения: `toInt()` бросает, `toIntOrNull()` возвращает `null`; `first()` бросает, `firstOrNull()` - нет. -```kotlin -fun main() { - val items = listOf(1, 2, 3) - println(items.getOrNull(10)) // null - println(items.firstOrNull { it > 5 }) // null - println(mapOf("a" to 1)["b"]) // null -} -``` -Подробнее: [Null-безопасность](../Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md) - -## Result -`Result` хранит либо значение, либо `Throwable`. -```kotlin -fun main() { - val ok: Result = Result.success(42) - val fail: Result = Result.failure(IllegalStateException("сбой")) - - println(ok.isSuccess) // true - println(fail.isFailure) // true - println(ok.getOrNull()) // 42 - println(fail.getOrNull()) // null - println(fail.exceptionOrNull()?.message) // сбой -} -``` - -### runCatching -Оборачивает блок: результат - `Result`, исключение - `failure`. -```kotlin -fun parse(s: String): Result = runCatching { s.toInt() } - -fun main() { - println(parse("42")) // Success(42) - println(parse("abc").exceptionOrNull()?.javaClass?.simpleName) // NumberFormatException -} -``` -Есть вариант-расширение с получателем: -```kotlin -fun main() { - val length = "привет".runCatching { length } - println(length.getOrNull()) // 6 -} -``` - -### Извлечение значения -```kotlin -fun main() { - val ok = runCatching { 42 } - val fail = runCatching { error("сбой") } - - println(ok.getOrNull()) // 42 - println(fail.getOrDefault(0)) // 0 - println(fail.getOrElse { e -> e.message!!.length }) // 4 - println(ok.getOrThrow()) // 42 - - try { - fail.getOrThrow() - } catch (e: IllegalStateException) { - println("пробросилось: ${e.message}") // пробросилось: сбой - } -} -``` - -### Преобразования -```kotlin -fun main() { - val r = runCatching { "42" } - .map { it.toInt() } // только при успехе - .map { it * 2 } - println(r.getOrNull()) // 84 - - // mapCatching перехватывает исключение внутри преобразования - val bad = runCatching { "abc" }.mapCatching { it.toInt() } - println(bad.isFailure) // true - - // map не перехватывает — исключение улетит наружу - val thrown = runCatching { runCatching { "abc" }.map { it.toInt() } } - println(thrown.isFailure) // true -} -``` -Разница существенная: `map` предполагает, что преобразование не падает, `mapCatching` - что может. - -```kotlin -fun main() { - val r = runCatching { error("сбой") } - .recover { 0 } // заменяем ошибку значением - println(r.getOrNull()) // 0 - - val folded = runCatching { "42".toInt() } - .fold( - onSuccess = { "значение $it" }, - onFailure = { "ошибка ${it.message}" }, - ) - println(folded) // значение 42 -} -``` - -### onSuccess и onFailure -Побочные эффекты без изменения значения - они возвращают сам `Result`: -```kotlin -fun main() { - runCatching { "abc".toInt() } - .onSuccess { println("получили $it") } - .onFailure { println("не вышло: ${it.message}") } - .getOrDefault(0) - .let { println("итог: $it") } - // не вышло: For input string: "abc" - // итог: 0 -} -``` - -## Свой sealed-тип -Когда причин несколько и клиент должен их различать, `Result` слишком беден - в нём любая ошибка это `Throwable`. -```kotlin -sealed interface LoadResult { - data class Success(val user: String) : LoadResult - data object NotFound : LoadResult - data class Forbidden(val reason: String) : LoadResult - data class NetworkError(val retryAfter: Int) : LoadResult -} - -fun load(id: Int): LoadResult = when { - id == 1 -> LoadResult.Success("Иван") - id == 2 -> LoadResult.Forbidden("нет прав") - id < 0 -> LoadResult.NetworkError(retryAfter = 5) - else -> LoadResult.NotFound -} - -fun describe(r: LoadResult) = when (r) { // else не нужен - is LoadResult.Success -> "загружен ${r.user}" - LoadResult.NotFound -> "не найден" - is LoadResult.Forbidden -> "запрещено: ${r.reason}" - is LoadResult.NetworkError -> "повтор через ${r.retryAfter} с" -} - -fun main() { - listOf(1, 2, 3, -1).forEach { println(describe(load(it))) } - // загружен Иван - // запрещено: нет прав - // не найден - // повтор через 5 с -} -``` -Главное преимущество - компилятор проверяет, что обработаны все варианты. Добавили новый - все `when` перестанут компилироваться, пока его не учтут. С `Result` такой гарантии нет. - -Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md) - -### Свой Result с типизированной ошибкой -```kotlin -sealed interface Outcome { - data class Ok(val value: T) : Outcome - data class Err(val error: E) : Outcome -} - -enum class ParseError { EMPTY, NOT_A_NUMBER, NEGATIVE } - -fun parsePositive(s: String): Outcome = when { - s.isBlank() -> Outcome.Err(ParseError.EMPTY) - s.toIntOrNull() == null -> Outcome.Err(ParseError.NOT_A_NUMBER) - s.toInt() < 0 -> Outcome.Err(ParseError.NEGATIVE) - else -> Outcome.Ok(s.toInt()) -} - -fun main() { - listOf("42", "", "abc", "-1").forEach { input -> - when (val r = parsePositive(input)) { - is Outcome.Ok -> println("'$input' -> ${r.value}") - is Outcome.Err -> println("'$input' -> ошибка ${r.error}") - } - } - // '42' -> 42 - // '' -> ошибка EMPTY - // 'abc' -> ошибка NOT_A_NUMBER - // '-1' -> ошибка NEGATIVE -} -``` -`Nothing` в позиции второго параметра позволяет `Ok` подходить под любой тип ошибки. Подробнее: [Any, Unit, Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md), [Вариантность](../Дженерики/Вариантность.md) - -## Что выбрать -```mermaid -graph TD - A["Ошибка возможна"] --> B{"Вызывающий может
что-то сделать?"} - B -->|"нет, это баг"| C["исключение:
require, check, error"] - B -->|"да"| D{"Причину нужно
различать?"} - D -->|"нет"| E["T?"] - D -->|"да, но обработка общая"| F["Result<T>"] - D -->|"да, по-разному"| G["свой sealed-тип"] -``` - -Практическое разделение: -- **исключение** - для нарушенных предпосылок: неверный аргумент, недопустимое состояние. Это ошибка программиста, её исправляют в коде, а не обрабатывают. Подробнее: [Проверки и контракты](Проверки%20и%20контракты.md) -- **`T?`, `Result`, sealed** - для ожидаемых ситуаций: нет записи в БД, сеть недоступна, ввод пользователя некорректен - -## Граница с исключениями -`runCatching` удобен на границе: внешний код бросает, ваш возвращает значение. -```kotlin -class User7(val id: Int, val name: String) - -fun fetchFromDb(id: Int): User7 = - if (id == 1) User7(1, "Иван") else throw NoSuchElementException("нет пользователя $id") - -fun findUser(id: Int): User7? = runCatching { fetchFromDb(id) }.getOrNull() - -fun main() { - println(findUser(1)?.name) // Иван - println(findUser(99)) // null -} -``` -Обратное направление - когда API требует исключений, а внутри `Result`: -```kotlin -fun loadOrThrow(id: Int): String = runCatching { - require(id > 0) { "id должен быть положительным" } - "данные $id" -}.getOrThrow() - -fun main() { - println(loadOrThrow(1)) // данные 1 - try { - loadOrThrow(-1) - } catch (e: IllegalArgumentException) { - println(e.message) // id должен быть положительным - } -} -``` - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | checked exceptions в сигнатуре | `Result` или sealed-тип | -> | `Optional` | `T?` - часть системы типов | -> | `Optional.map`/`orElse` | `?.let`/`?:` | -> | нет аналога `Result` в JDK | `Result` в stdlib | -> | `try`/`catch` для управления | `runCatching` как выражение | -> -> **`Result` частично возвращает то, что отняло отсутствие checked exceptions.** Сигнатура `fun load(): Result` сообщает, что функция может не справиться, и компилятор не даст использовать значение без разбора. Разница с checked exceptions в том, что это выбор автора функции, а не навязанное правило, и такие значения свободно проходят через `map`, `filter` и другие обобщённые функции. -> -> **`T?` вместо `Optional`.** `Optional` - обёртка-объект: аллокация, два уровня значений (`null` и пустой `Optional`), неудобство в полях. Nullable-тип в Kotlin существует только при компиляции, а в байт-коде это обычная ссылка - накладных расходов нет: -> ```kotlin -> fun main() { -> val name: String? = null -> println(name?.uppercase() ?: "<нет>") // <нет> -> } -> ``` -> ```java -> Optional name = Optional.empty(); -> System.out.println(name.map(String::toUpperCase).orElse("<нет>")); -> ``` -> -> **Sealed-типы вместо иерархии исключений.** В Java разные причины сбоя обычно выражают классами исключений и ловят их по типу. Sealed-тип даёт то же различение, но с проверкой полноты при компиляции и без стоимости стектрейса. -> -> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md) - -> [!warning] Подводные камни -> **`runCatching` перехватывает всё, включая то, что перехватывать нельзя.** Он ловит `Throwable`, то есть `OutOfMemoryError`, `StackOverflowError` и - самое опасное - `CancellationException` в корутинах: -> ```kotlin -> // Требует kotlinx-coroutines-core. -> // suspend fun broken() { -> // runCatching { delay(1000) } // проглотит отмену корутины -> // } -> // -> // suspend fun fixed() = runCatching { delay(1000) } -> // .onFailure { if (it is CancellationException) throw it } -> ``` -> В корутинах либо проверяйте тип вручную, либо используйте обычный `try`/`catch` с перехватом конкретных типов. Подробнее: [Исключения в корутинах](../Корутины/Исключения%20в%20корутинах.md) -> -> **`Result` нельзя вернуть из функции в публичном API без оговорок.** До Kotlin 1.5 это было запрещено совсем; сейчас разрешено, но тип по-прежнему `value class`, поэтому из Java он виден странно и его нельзя использовать как тип свойства: -> ```kotlin -> class Holder { -> // val r: Result = Result.success(1) // компилируется, но Result как поле боксируется -> private val value: Int = 1 -> fun get(): Result = Result.success(value) -> } -> -> fun main() { println(Holder().get().getOrNull()) } // 1 -> ``` -> -> **`Result` стирает тип ошибки.** Все причины сбоя - `Throwable`, поэтому различать их приходится через `is`, и компилятор полноту не проверяет: -> ```kotlin -> fun main() { -> val r = runCatching { error("сбой") } -> val msg = when (val e = r.exceptionOrNull()) { -> null -> "успех" -> is IllegalStateException -> "состояние: ${e.message}" -> else -> "прочее" // else обязателен -> } -> println(msg) // состояние: сбой -> } -> ``` -> -> **Создание `failure` всё равно создаёт исключение со стектрейсом** - то есть `Result` не даёт экономии на производительности, если ошибку формирует `runCatching`. Дешёвый вариант - sealed-тип без `Throwable` внутри. -> -> **Проигнорированный `Result` не вызывает предупреждения:** -> ```kotlin -> fun save(): Result = runCatching { error("не записалось") } -> -> fun main() { -> save() // ошибка потеряна молча -> println(save().isFailure) // true — так правильно -> } -> ``` -> В отличие от исключения, которое всплывёт само, забытый `Result` ничего не делает. Это основной риск подхода. -> -> **Цепочка `map` на `Result` может бросить.** `map` не перехватывает исключения из своей лямбды - для этого есть `mapCatching`. -> -> **`getOrThrow` на чужом `Result` бросает исходное исключение,** а не обёртку, поэтому стектрейс указывает на место первоначального сбоя, а не на `getOrThrow`. Для диагностики это хорошо, для чтения логов - неожиданно. -> -> **Смешение подходов в одном слое запутывает.** Если часть функций репозитория возвращает `null`, часть `Result`, а часть бросает, клиентский код обрастает тремя видами обработки. Выберите одно соглашение на слой. - -Далее: [Проверки и контракты](Проверки%20и%20контракты.md) diff --git a/Kotlin/Исключения/Проверки и контракты.md b/Kotlin/Исключения/Проверки и контракты.md deleted file mode 100644 index 3464e16..0000000 --- a/Kotlin/Исключения/Проверки и контракты.md +++ /dev/null @@ -1,403 +0,0 @@ ---- -title: Проверки и контракты -description: require, check, error, assert и контракты компилятора для умного приведения -tags: - - kotlin - - exceptions ---- - -Стандартная библиотека даёт четыре функции для проверки предпосылок. Выбор между ними определяется тем, **кто виноват** в нарушении. - -| Функция | Бросает | Виноват | Что проверяет | -|---|---|---|---| -| `require` | `IllegalArgumentException` | вызывающий | аргументы | -| `check` | `IllegalStateException` | вызывающий или сам объект | состояние | -| `error` | `IllegalStateException` | - | недостижимая ветка | -| `assert` | `AssertionError` | - | инвариант при отладке | - -## require - проверка аргументов -```kotlin -fun withdraw(balance: Long, amount: Long): Long { - require(amount > 0) { "сумма должна быть положительной, получено $amount" } - require(amount <= balance) { "недостаточно средств: $amount > $balance" } - return balance - amount -} - -fun main() { - println(withdraw(100, 30)) // 70 - try { - withdraw(100, -5) - } catch (e: IllegalArgumentException) { - println(e.message) // сумма должна быть положительной, получено -5 - } -} -``` -Сообщение передаётся лямбдой, поэтому строка собирается только при нарушении - в успешном случае интерполяция не выполняется. - -```kotlin -fun main() { - val n = 5 - require(n > 0) // без сообщения: "Failed requirement." - require(n > 0) { "дорогое сообщение: ${(1..1000).sum()}" } // лямбда не вызовется - println("проверки пройдены") -} -``` - -### requireNotNull -Проверяет и **возвращает** non-null значение: -```kotlin -fun greet(name: String?): String { - val safe = requireNotNull(name) { "имя обязательно" } - return "Привет, ${safe.uppercase()}" // safe имеет тип String -} - -fun main() { - println(greet("иван")) // Привет, ИВАН - try { - greet(null) - } catch (e: IllegalArgumentException) { - println(e.message) // имя обязательно - } -} -``` -Работает и как инструкция - после вызова переменная умно приводится: -```kotlin -fun process(input: String?): Int { - requireNotNull(input) - return input.length // smart cast: String? -> String -} - -fun main() { println(process("abc")) } // 3 -``` -Это работает благодаря контракту, см. ниже. - -## check - проверка состояния -```kotlin -class Connection2 { - private var open = false - - fun connect() { open = true } - - fun send(data: String): String { - check(open) { "соединение не открыто" } - return "отправлено: $data" - } -} - -fun main() { - val c = Connection2() - try { - c.send("данные") - } catch (e: IllegalStateException) { - println(e.message) // соединение не открыто - } - c.connect() - println(c.send("данные")) // отправлено: данные -} -``` -Разница с `require` только в типе исключения, но она информативна: `IllegalArgumentException` говорит «ты передал не то», `IllegalStateException` - «ты вызвал не вовремя». - -`checkNotNull` - аналог `requireNotNull` для состояния: -```kotlin -class Service18 { - private var config: String? = null - - fun init2(value: String) { config = value } - - fun run2(): String { - val cfg = checkNotNull(config) { "сначала вызовите init2()" } - return "работаю с $cfg" - } -} - -fun main() { - val s = Service18() - try { - s.run2() - } catch (e: IllegalStateException) { - println(e.message) // сначала вызовите init2() - } - s.init2("настройки") - println(s.run2()) // работаю с настройки -} -``` - -## error - недостижимая ветка -`error(msg)` бросает `IllegalStateException`. Её тип - `Nothing`, поэтому она подходит в любое выражение: -```kotlin -fun describe(code: Int): String = when (code) { - 200 -> "ок" - 404 -> "не найдено" - 500 -> "ошибка сервера" - else -> error("неизвестный код: $code") -} - -fun main() { - println(describe(200)) // ок - try { - describe(999) - } catch (e: IllegalStateException) { - println(e.message) // неизвестный код: 999 - } -} -``` -Благодаря типу `Nothing` компилятор понимает, что ветка не возвращает значение, и `when` остаётся выражением. Подробнее: [Any, Unit, Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md) - -`TODO()` - то же для незаписанного кода: -```kotlin -fun futureFeature(): String = TODO("реализовать после релиза") - -fun main() { - try { - futureFeature() - } catch (e: NotImplementedError) { - println(e.message) // An operation is not implemented: реализовать после релиза - } -} -``` -`TODO()` возвращает `Nothing`, поэтому подходит на место любого типа - код компилируется, пока функция не написана. - -## assert - только для отладки -```kotlin -fun average(values: List): Double { - val result = values.sum().toDouble() / values.size - assert(result >= values.min()) { "среднее меньше минимума — ошибка в вычислении" } - return result -} - -fun main() { - println(average(listOf(1, 2, 3))) // 2.0 -} -``` -`assert` проверяется **только** при запуске JVM с флагом `-ea` (`-enableassertions`). Без него вызов ничего не делает, но лямбда-сообщение всё равно остаётся объектом в байт-коде. - -Практический вывод: `assert` в Kotlin почти не используют. Для проверок, которые должны работать в продакшене, берут `require` и `check`. - -## Контракты -Контракт - способ сообщить компилятору то, что он не может вывести сам. Именно поэтому `requireNotNull` даёт умное приведение. - -```kotlin -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.contract - -@OptIn(ExperimentalContracts::class) -fun String?.isNotNullOrBlank(): Boolean { - contract { returns(true) implies (this@isNotNullOrBlank != null) } - return !this.isNullOrBlank() -} - -fun main() { - val input: String? = "текст" - if (input.isNotNullOrBlank()) { - println(input.length) // smart cast: String? -> String - } -} -``` -Без контракта `input.length` потребовал бы `!!` или `?.` - компилятор не знает, что `true` из функции означает non-null. - -Стандартная библиотека использует контракты в `isNullOrEmpty`, `isNullOrBlank`, `require`, `check`, `requireNotNull`: -```kotlin -fun main() { - val s: String? = "abc" - if (!s.isNullOrEmpty()) { - println(s.length) // smart cast работает благодаря контракту - } - - val n: Int? = 5 - check(n != null) - println(n + 1) // 6, smart cast после check -} -``` - -### Виды контрактов -```kotlin -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract - -@OptIn(ExperimentalContracts::class) -fun requirePositive(value: Int?) { - contract { returns() implies (value != null) } // вернулась -> value не null - require(value != null && value > 0) { "нужно положительное число" } -} - -@OptIn(ExperimentalContracts::class) -inline fun runOnce(block: () -> T): T { - contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } - return block() -} - -fun main() { - val x: Int? = 5 - requirePositive(x) - println(x + 1) // 6 — smart cast - - val value: String // можно не инициализировать... - runOnce { value = "присвоено" } // ...потому что контракт гарантирует один вызов - println(value) // присвоено -} -``` -`callsInPlace` с `EXACTLY_ONCE` позволяет присваивать `val` внутри лямбды - компилятор знает, что присваивание произойдёт ровно один раз. На этом работают `run`, `let`, `apply` и `with`. - -| Контракт | Что обещает | -|---|---| -| `returns() implies (x)` | если функция вернулась нормально, условие `x` истинно | -| `returns(true) implies (x)` | если вернула `true`, условие `x` истинно | -| `returns(null) implies (x)` | если вернула `null`, условие `x` истинно | -| `callsInPlace(f, AT_MOST_ONCE)` | лямбда вызовется не больше одного раза | -| `callsInPlace(f, EXACTLY_ONCE)` | ровно один раз | -| `callsInPlace(f, AT_LEAST_ONCE)` | хотя бы один раз | - -Требования к контракту: -- только для функций верхнего уровня (не для методов класса) -- блок `contract` - **первая** инструкция в теле -- API экспериментальный: нужен `@OptIn(ExperimentalContracts::class)` -- компилятор **не проверяет**, что контракт соответствует коду - -Последний пункт - главный риск: контракт это обещание на доверии. - -## Валидация данных: исключение или значение -`require` подходит для нарушений, которые означают баг. Для пользовательского ввода лучше вернуть ошибку как значение: -```kotlin -data class Form4(val email: String, val age: Int) - -fun validate(email: String, age: Int): List = buildList { - if ("@" !in email) add("email без @") - if (age !in 0..150) add("недопустимый возраст: $age") -} - -fun create(email: String, age: Int): Result { - val errors = validate(email, age) - return if (errors.isEmpty()) Result.success(Form4(email, age)) - else Result.failure(IllegalArgumentException(errors.joinToString("; "))) -} - -fun main() { - println(create("a@b.ru", 20).getOrNull()) // Form4(email=a@b.ru, age=20) - println(create("нет", 200).exceptionOrNull()?.message) // email без @; недопустимый возраст: 200 -} -``` -Разница: `require` падает на первой ошибке, а пользователю обычно нужен список всех. Подробнее: [Result и runCatching](Result%20и%20runCatching.md) - -## Инварианты в конструкторе -```kotlin -class Range4(val from: Int, val to: Int) { - init { - require(from <= to) { "неверный диапазон: $from..$to" } - } - - val size get() = to - from + 1 -} - -@JvmInline -value class Email(val value: String) { - init { - require("@" in value) { "не похоже на email: $value" } - } -} - -fun main() { - println(Range4(1, 5).size) // 5 - println(Email("a@b.ru").value) // a@b.ru - try { - Range4(5, 1) - } catch (e: IllegalArgumentException) { - println(e.message) // неверный диапазон: 5..1 - } -} -``` -Проверка в конструкторе гарантирует, что невалидного объекта не существует, - дальше по коду проверять не нужно. Подробнее: [Value class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Value%20class.md), [Инициализация](../ООП/Инициализация.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `if (x < 0) throw new IllegalArgumentException(...)` | `require(x >= 0) { ... }` | -> | `Objects.requireNonNull(x)` | `requireNotNull(x)` + smart cast | -> | `assert x > 0 : "msg"` | `assert(x > 0) { "msg" }` | -> | Guava `Preconditions.checkArgument` | `require` в stdlib | -> | `throw new UnsupportedOperationException()` | `TODO()` | -> | нет аналога | контракты для smart cast | -> -> **`require` и `check` - это то, за чем в Java идут в Guava.** `Preconditions.checkArgument` и `checkState` из Guava выполняют ту же роль, но требуют внешней зависимости. В Kotlin они в stdlib и вдобавок `inline`, то есть вызова функции в байт-коде не остаётся. -> -> **Сообщение в лямбде экономит работу.** В Java `checkArgument("плохое значение: " + value)` собирает строку всегда, даже когда проверка пройдена. Передача лямбды плюс `inline` означает, что конкатенация происходит только при нарушении: -> ```kotlin -> fun main() { -> val value = 5 -> require(value > 0) { "дорогая строка: ${value.toString().repeat(1000).length}" } -> println("лямбда не вызывалась") -> } -> ``` -> -> **`requireNotNull` даёт smart cast, `Objects.requireNonNull` - нет.** В Java результат нужно присвоить: `this.name = Objects.requireNonNull(name)`. В Kotlin после проверки компилятор сам считает переменную non-null. -> -> **Контрактов в Java нет совсем.** Ближайший аналог - аннотации `@Nullable`/`@NotNull` и внешние анализаторы, которые работают вне компилятора и не влияют на типизацию. -> -> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md), [Инкапсуляция](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md) - -> [!warning] Подводные камни -> **`assert` по умолчанию выключен.** Без `-ea` при запуске JVM проверка не выполняется совсем: -> ```kotlin -> fun main() { -> assert(1 == 2) { "это сообщение вы не увидите без -ea" } -> println("программа продолжила работу") -> } -> ``` -> Код, который полагается на `assert` для защиты инвариантов в продакшене, не защищён ничем. Используйте `check`. -> -> **Путаница `require` и `check`.** Тип исключения - часть контракта: вызывающий может ловить `IllegalArgumentException`, чтобы отличить «я передал мусор» от «объект не готов». Перепутанные функции делают эту информацию ложной. -> -> **Сообщение без контекста бесполезно:** -> ```kotlin -> fun main() { -> val size = -1 -> try { -> require(size >= 0) { "неверный размер" } // какой именно? -> } catch (e: IllegalArgumentException) { -> println(e.message) -> } -> try { -> require(size >= 0) { "размер должен быть >= 0, получено $size" } -> } catch (e: IllegalArgumentException) { -> println(e.message) // размер должен быть >= 0, получено -1 -> } -> } -> ``` -> -> **`require` для пользовательского ввода - ошибка проектирования.** Некорректный ввод - штатная ситуация, а не баг. Исключение в этом случае заставляет клиентский код оборачивать каждый вызов в `try`/`catch` и сообщает только о первой проблеме. -> -> **Контракт не проверяется компилятором.** Неверный контракт ломает проверки типов - компилятор поверит обещанию и уберёт нужные проверки на `null`: -> ```kotlin -> import kotlin.contracts.ExperimentalContracts -> import kotlin.contracts.contract -> -> @OptIn(ExperimentalContracts::class) -> fun String?.lies(): Boolean { -> contract { returns(true) implies (this@lies != null) } -> return true // врёт: true даже для null -> } -> -> fun main() { -> val s: String? = null -> if (s.lies()) { -> try { -> println(s.length) // компилятор уверен, что s != null -> } catch (e: NullPointerException) { -> println("NPE в non-null коде из-за неверного контракта") -> } -> } -> } -> ``` -> -> **Контракт нельзя объявить в методе класса** - только в функции верхнего уровня. Это ограничение текущей реализации. -> -> **Контракты экспериментальны** - `@OptIn(ExperimentalContracts::class)` обязателен, API может измениться. -> -> **`TODO()` компилируется и попадает в релиз.** Он возвращает `Nothing`, поэтому ошибок сборки не вызывает - только падение в рантайме. Перед релизом стоит проверять: `grep -r 'TODO(' src/`. -> -> **Проверки в `init` не спасают `data class` от `copy`** в версиях до Kotlin 2.1: `copy()` вызывает конструктор, то есть `init` выполнится, но саму валидацию обойти через `copy` нельзя - а вот приватный конструктор обойти можно. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md) -> -> **`require` внутри цикла по большой коллекции стоит дорого** не из-за проверки, а из-за того, что при нарушении создаётся исключение со стектрейсом. Для валидации списков собирайте список ошибок, а не бросайте на первой. - -Дополнительно: [Система исключений](Система%20исключений.md) -Далее: [Коллекции](../Коллекции/Коллекции.md) diff --git a/Kotlin/Исключения/Система исключений.md b/Kotlin/Исключения/Система исключений.md deleted file mode 100644 index e635660..0000000 --- a/Kotlin/Исключения/Система исключений.md +++ /dev/null @@ -1,370 +0,0 @@ ---- -title: Система исключений -description: Иерархия Throwable, try как выражение, отсутствие checked exceptions и правильная обработка -tags: - - kotlin - - exceptions ---- - -Все исключения в Kotlin **непроверяемые**. Компилятор не требует ни объявлять, ни перехватывать их. - -```kotlin -fun parse(s: String): Int = s.toInt() // может бросить, но throws не нужен - -fun main() { - println(parse("42")) // 42 - try { - parse("abc") - } catch (e: NumberFormatException) { - println("не число: ${e.message}") // не число: For input string: "abc" - } -} -``` - -## Иерархия -```mermaid -graph TD - T["Throwable"] --> E["Exception"] - T --> R["Error"] - E --> RE["RuntimeException"] - E --> IOE["IOException"] - RE --> NPE["NullPointerException"] - RE --> IAE["IllegalArgumentException"] - RE --> ISE["IllegalStateException"] - RE --> CCE["ClassCastException"] - R --> OOM["OutOfMemoryError"] - R --> SOE["StackOverflowError"] -``` -Иерархия та же, что в Java, - это классы JDK. Разница только в том, что Kotlin не делит их на проверяемые и непроверяемые. - -| Класс | Смысл | Перехватывать | -|---|---|---| -| `Error` | сбой среды: нет памяти, переполнен стек | нет | -| `RuntimeException` | ошибка программиста: неверный аргумент, состояние | обычно нет, лучше исправить код | -| прочие `Exception` | внешний сбой: сеть, файл, БД | да | - -## try как выражение -`try` возвращает значение - это важное отличие от Java: -```kotlin -fun parseOrZero(s: String): Int = try { - s.toInt() -} catch (e: NumberFormatException) { - 0 -} - -fun main() { - println(parseOrZero("42")) // 42 - println(parseOrZero("abc")) // 0 -} -``` -Результат - последнее выражение блока `try` или соответствующего `catch`. Блок `finally` на результат **не влияет**: -```kotlin -fun main() { - val result = try { - "из try" - } finally { - "из finally" // игнорируется - } - println(result) // из try -} -``` - -## throw - тоже выражение -Тип `throw` - [Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md), поэтому он подходит в любую позицию: -```kotlin -fun findUser(id: Int): String = - if (id > 0) "пользователь $id" - else throw IllegalArgumentException("неверный id: $id") - -fun main() { - println(findUser(1)) // пользователь 1 - val name = findUser(1).takeIf { it.isNotEmpty() } ?: throw IllegalStateException("пусто") - println(name) // пользователь 1 -} -``` -Это позволяет писать `?: throw ...` и `?: error(...)` в одном выражении. - -## Перехват -```kotlin -fun risky(mode: Int): String = when (mode) { - 1 -> throw IllegalArgumentException("плохой аргумент") - 2 -> throw IllegalStateException("плохое состояние") - else -> "ок" -} - -fun handle(mode: Int): String = try { - risky(mode) -} catch (e: IllegalArgumentException) { - "аргумент: ${e.message}" -} catch (e: IllegalStateException) { - "состояние: ${e.message}" -} catch (e: Exception) { - "прочее: ${e.message}" -} finally { - println("finally выполняется всегда") -} - -fun main() { - println(handle(1)) // аргумент: плохой аргумент - println(handle(3)) // ок -} -``` -Блоки `catch` проверяются по порядку, поэтому конкретные типы идут раньше общих. `catch (e: Exception)` первым сделал бы остальные недостижимыми. - -Несколько типов в одном блоке Kotlin не поддерживает - аналога Java `catch (A | B e)` нет. Варианты: -```kotlin -fun main() { - try { - throw IllegalStateException("сбой") - } catch (e: RuntimeException) { // общий супертип - println("перехвачено: ${e::class.simpleName}") - } - - try { - throw NumberFormatException("плохое число") - } catch (e: Exception) { - when (e) { // или when по типу - is NumberFormatException, is IllegalStateException -> - println("ожидаемая ошибка: ${e.message}") - else -> throw e // остальное пробрасываем - } - } -} -``` - -## Свои исключения -```kotlin -class InsufficientFundsException( - val required: Long, - val available: Long, -) : Exception("нужно $required, доступно $available") - -class ValidationException(message: String, cause: Throwable? = null) : Exception(message, cause) - -fun withdraw(balance: Long, amount: Long): Long { - if (amount > balance) throw InsufficientFundsException(amount, balance) - return balance - amount -} - -fun main() { - println(withdraw(100, 30)) // 70 - try { - withdraw(100, 500) - } catch (e: InsufficientFundsException) { - println("не хватает ${e.required - e.available}") // не хватает 400 - } -} -``` -Своё исключение полезно, когда вызывающему нужно различить ситуацию и получить данные о ней. Для «ошибки программиста» хватает стандартных `IllegalArgumentException` и `IllegalStateException`. - -## Цепочка причин -```kotlin -class RepositoryException(message: String, cause: Throwable) : Exception(message, cause) - -fun loadUser(id: String): String = try { - "пользователь ${id.toInt()}" -} catch (e: NumberFormatException) { - throw RepositoryException("не удалось загрузить '$id'", e) // сохраняем причину -} - -fun main() { - try { - loadUser("abc") - } catch (e: RepositoryException) { - println(e.message) // не удалось загрузить 'abc' - println("причина: ${e.cause?.message}") // причина: For input string: "abc" - } -} -``` -Передавать `cause` обязательно: без него теряется исходный стектрейс и причина сбоя. - -## use - автоматическое закрытие -Аналог try-with-resources: -```kotlin -import java.io.StringReader - -fun main() { - val content = StringReader("данные").use { reader -> - reader.readText() // close() вызовется автоматически - } - println(content) // данные -} -``` -`use` - функция-расширение для `Closeable`, она закрывает ресурс и при исключении. Это предпочтительный способ: `finally` с ручным `close()` легко написать неверно. - -```kotlin -class Resource(val name: String) : AutoCloseable { - fun work() = "работаю с $name" - override fun close() = println("$name закрыт") -} - -fun main() { - val result = Resource("файл").use { it.work() } - println(result) - // файл закрыт - // работаю с файл - - try { - Resource("другой").use { throw IllegalStateException("сбой") } - } catch (e: IllegalStateException) { - println("ресурс всё равно закрыт: ${e.message}") - } -} -``` - -## @Throws для Java -Kotlin не объявляет исключения, но Java-клиенту это может быть нужно: -```kotlin -import java.io.IOException - -@Throws(IOException::class) -fun readFile(path: String): String { - if (path.isEmpty()) throw IOException("пустой путь") - return "содержимое $path" -} - -fun main() { println(readFile("a.txt")) } // содержимое a.txt -``` -Без `@Throws` Java-код не сможет перехватить проверяемое исключение - компилятор Java сочтёт, что метод его не бросает. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md) - -## Чего не делать -```kotlin -fun main() { - // проглатывание: ошибка исчезает - try { - error("сбой") - } catch (e: Exception) { - // ничего — худшая практика - } - - // перехват Throwable: ловит OutOfMemoryError и StackOverflowError - try { - error("сбой") - } catch (e: Exception) { // не Throwable - println("перехвачено корректно") - } - - // исключение как управление потоком - val items = listOf(1, 2, 3) - println(items.getOrNull(10) ?: "нет элемента") // вместо try/catch -} -``` -Правила: -- не перехватывать, если не знаете, что делать с ошибкой - пусть идёт выше -- не перехватывать `Throwable` и `Error` -- не использовать исключения для ожидаемых ситуаций - для них есть `null`, `Result` или sealed-типы -- всегда логировать или пробрасывать; пустой `catch` скрывает причину сбоя - -Альтернативы исключениям: [Result и runCatching](Result%20и%20runCatching.md) - -## Исключения в корутинах -Отмена корутины реализована через `CancellationException`, и перехватывать её нельзя: -```kotlin -// Требует kotlinx-coroutines-core. -// suspend fun work() { -// try { -// delay(1000) -// } catch (e: CancellationException) { -// throw e // обязательно пробросить! -// } catch (e: Exception) { -// println("настоящая ошибка") -// } -// } -``` -`catch (e: Exception)` перехватит и отмену, из-за чего корутина продолжит работу после отмены. Подробнее: [Исключения в корутинах](../Корутины/Исключения%20в%20корутинах.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | checked exceptions | **нет**, все непроверяемые | -> | `throws IOException` в сигнатуре | `@Throws` только для Java-клиентов | -> | `try` - инструкция | `try` - выражение | -> | `catch (A \| B e)` | нет аналога, нужен `when` | -> | try-with-resources | `use { }` | -> | `throw` - инструкция | `throw` - выражение типа `Nothing` | -> -> **Отсутствие checked exceptions - самое спорное решение.** Аргументы против них, которые приводят авторы Kotlin: -> -> 1. **Они не масштабируются.** Сигнатура с пятью `throws` нечитаема, а обобщённый код (`map`, `forEach`) не может объявить исключения вызываемой лямбды - из-за этого Java-стримы несовместимы с проверяемыми исключениями. -> 2. **На практике их проглатывают.** Самый частый способ «обработать» обязательное исключение - пустой `catch`, то есть защита превращается в сокрытие ошибки. -> 3. **Обёртывание ради компиляции.** Код вроде `catch (IOException e) { throw new RuntimeException(e); }` встречается в любом крупном Java-проекте. -> -> Цена отказа - сигнатура больше не документирует ошибки. Это компенсируют KDoc и типы вроде `Result`, но компилятор уже не помогает. -> -> **`try` как выражение убирает `var`:** -> ```java -> int value; // Java: нужна изменяемая переменная -> try { value = Integer.parseInt(s); } -> catch (NumberFormatException e) { value = 0; } -> ``` -> ```kotlin -> fun parseOrZero(s: String): Int = -> try { s.toInt() } catch (e: NumberFormatException) { 0 } // val вместо var -> -> fun main() { println(parseOrZero("abc")) } // 0 -> ``` -> -> **`use` вместо try-with-resources** - обычная функция, а не синтаксис. Поэтому её можно применить к своему типу, добавив реализацию `Closeable`. -> -> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md) - -> [!warning] Подводные камни -> **Сигнатура не говорит об исключениях.** Единственные источники информации - документация и чтение кода. Для важных случаев указывайте в KDoc: -> ```kotlin -> /** -> * @throws IllegalArgumentException если id не положительный -> */ -> fun load(id: Int): String { -> require(id > 0) { "id должен быть положительным" } -> return "данные $id" -> } -> -> fun main() { println(load(1)) } // данные 1 -> ``` -> -> **`finally` не влияет на результат `try`-выражения** - значение из него отбрасывается. -> -> **`return` в `finally` проглатывает исключение:** -> ```kotlin -> fun bad(): String { -> try { -> throw IllegalStateException("потеряется") -> } finally { -> return "из finally" // исключение исчезло! -> } -> } -> -> fun main() { println(bad()) } // из finally -> ``` -> Исключение не дойдёт до вызывающего. `return` в `finally` писать нельзя. -> -> **Перехват `Exception` ловит все `RuntimeException`,** включая `NullPointerException` в вашем коде - то есть маскирует собственные баги под «обработанную ошибку». -> -> **`CancellationException` нельзя проглатывать в корутинах** - иначе отмена перестанет работать. -> -> **Исключение в `init` или в инициализаторе свойства оставляет побочные эффекты.** Объект не создастся, но то, что уже выполнилось, останется. Подробнее: [Инициализация](../ООП/Инициализация.md) -> -> **Создание исключения дорого** из-за сбора стектрейса. Для ожидаемых ситуаций в горячем коде используйте `null` или `Result`: -> ```kotlin -> fun main() { -> println("abc".toIntOrNull() ?: 0) // без исключения -> // println("abc".toInt()) // исключение + стектрейс -> } -> ``` -> -> **Исключение из лямбды в `forEach` прерывает весь цикл**, а не одну итерацию: -> ```kotlin -> fun main() { -> val results = mutableListOf() -> try { -> listOf("1", "abc", "3").forEach { results.add(it.toInt()) } -> } catch (e: NumberFormatException) { -> println("прервано на второй строке, собрано: $results") // [1] -> } -> } -> ``` -> Чтобы обработать все элементы, перехватывайте внутри лямбды или используйте `mapNotNull { it.toIntOrNull() }`. -> -> **`@Throws` не проверяется компилятором Kotlin** - это только метаданные для Java. Можно объявить исключение, которое никогда не бросается, и наоборот. - -Далее: [Result и runCatching](Result%20и%20runCatching.md) diff --git a/Kotlin/Коллекции/List, Set, Map.md b/Kotlin/Коллекции/List, Set, Map.md deleted file mode 100644 index 986fe33..0000000 --- a/Kotlin/Коллекции/List, Set, Map.md +++ /dev/null @@ -1,365 +0,0 @@ ---- -title: List, Set, Map -description: Три семейства коллекций, их гарантии, стоимость операций и выбор между ними -tags: - - kotlin - - collections ---- - -| | `List` | `Set` | `Map` | -|---|---|---|---| -| порядок | по индексу | вставки (у `LinkedHashSet`) | вставки (у `LinkedHashMap`) | -| дубликаты | разрешены | запрещены | ключи уникальны | -| доступ | по индексу, O(1) | только поиск, O(1) | по ключу, O(1) | -| поиск элемента | O(n) | O(1) | O(1) по ключу | -| основной вопрос | «что по порядку» | «есть ли такой» | «что соответствует» | - -## List -```kotlin -fun main() { - val list = listOf("a", "b", "c", "b") - - println(list[1]) // b — доступ по индексу - println(list.getOrNull(10)) // null - println(list.getOrElse(10) { "?" }) // ? - println(list.indexOf("b")) // 1 — первое вхождение - println(list.lastIndexOf("b")) // 3 - println(list.subList(1, 3)) // [b, c] - println(list.size) // 4 — дубликат сохранён -} -``` - -Доступ к краям и срезы: -```kotlin -fun main() { - val list = listOf(1, 2, 3, 4, 5) - - println("${list.first()} ${list.last()}") // 1 5 - println(list.firstOrNull { it > 3 }) // 4 - println(list.take(2)) // [1, 2] - println(list.drop(2)) // [3, 4, 5] - println(list.takeLast(2)) // [4, 5] - println(list.slice(1..3)) // [2, 3, 4] - println(list.chunked(2)) // [[1, 2], [3, 4], [5]] - println(list.windowed(2)) // [[1, 2], [2, 3], [3, 4], [4, 5]] - println(list.reversed()) // [5, 4, 3, 2, 1] -} -``` - -Изменение через `MutableList`: -```kotlin -fun main() { - val list = mutableListOf(1, 2, 3) - - list.add(4) - list.add(0, 0) // по индексу - list[1] = 10 - list.removeAt(0) - list.remove(4) // по значению - println(list) // [10, 2, 3] - - list += 5 // оператор += работает как add - list -= 2 - println(list) // [10, 3, 5] - - list.sortDescending() // сортировка на месте - println(list) // [10, 5, 3] -} -``` - -## Set -```kotlin -fun main() { - val set = setOf(1, 2, 3) - - println(2 in set) // true — O(1) - println(set.contains(5)) // false - println(setOf(1, 2, 2, 3).size) // 3 — дубликат отброшен -} -``` - -Операции над множествами: -```kotlin -fun main() { - val a = setOf(1, 2, 3) - val b = setOf(3, 4, 5) - - println(a union b) // [1, 2, 3, 4, 5] - println(a intersect b) // [3] - println(a subtract b) // [1, 2] - println(a + b) // [1, 2, 3, 4, 5] — как union - println(a - b) // [1, 2] -} -``` -Эти же функции работают на `List`, но результат всегда `Set`: -```kotlin -fun main() { - println(listOf(1, 1, 2) union listOf(2, 3)) // [1, 2, 3] - println(listOf(1, 1, 2).distinct()) // [1, 2] — а так останется List -} -``` - -### Три реализации Set -```kotlin -fun main() { - val linked = linkedSetOf(3, 1, 2) // порядок вставки - val hash = hashSetOf(3, 1, 2) // порядок не определён - val sorted = sortedSetOf(3, 1, 2) // отсортирован - - println(linked) // [3, 1, 2] - println(sorted) // [1, 2, 3] - println(hash.size) // 3 -} -``` - -| Реализация | Порядок | Поиск | Когда | -|---|---|---|---| -| `LinkedHashSet` (`setOf`) | вставки | O(1) | по умолчанию | -| `HashSet` (`hashSetOf`) | не определён | O(1) | экономия памяти, порядок не нужен | -| `TreeSet` (`sortedSetOf`) | сортировки | O(log n) | нужен отсортированный обход | - -Элементы `Set` обязаны корректно реализовывать `equals` и `hashCode` - иначе множество работает неправильно. Подробнее: [equals, hashCode и toString](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md) - -## Map -```kotlin -fun main() { - val map = mapOf("a" to 1, "b" to 2) - - println(map["a"]) // 1 - println(map["z"]) // null - println(map.getOrDefault("z", 0)) // 0 - println(map.getOrElse("z") { -1 }) // -1 - println(map.getValue("a")) // 1, бросает при отсутствии - println("a" in map) // true — проверка ключа - println(map.containsValue(2)) // true - println("${map.keys} ${map.values}") // [a, b] [1, 2] -} -``` - -`to` - инфиксная функция, создающая `Pair`: -```kotlin -fun main() { - val pair: Pair = "a" to 1 - println("${pair.first} ${pair.second}") // a 1 - - val (k, v) = pair // деструктуризация - println("$k=$v") // a=1 - - println(listOf("a" to 1, "b" to 2).toMap()) // {a=1, b=2} -} -``` - -Изменение через `MutableMap`: -```kotlin -fun main() { - val map = mutableMapOf("a" to 1) - - map["b"] = 2 - map.put("c", 3) - map.putAll(mapOf("d" to 4)) - map.remove("a") - println(map) // {b=2, c=3, d=4} - - println(map.getOrPut("e") { 5 }) // 5 — вычислит и положит - println(map.getOrPut("e") { 99 }) // 5 — уже есть, лямбда не вызвана - map.merge("b", 10) { old, new -> old + new } // метод JDK - println(map) // {b=12, c=3, d=4, e=5} -} -``` -`getOrPut` - основной инструмент кеширования: одна операция вместо проверки и вставки. - -Обход: -```kotlin -fun main() { - val map = mapOf("a" to 1, "b" to 2) - - for ((key, value) in map) print("$key=$value ") // a=1 b=2 - println() - map.forEach { (key, value) -> print("$key$value ") } // a1 b2 - println() - println(map.entries.first().key) // a -} -``` -Деструктуризация в `for` и в лямбде - главная причина, почему обход `Map` в Kotlin короче, чем в Java. - -Преобразования: -```kotlin -fun main() { - val map = mapOf("a" to 1, "b" to 2, "c" to 3) - - println(map.filter { it.value > 1 }) // {b=2, c=3} - println(map.filterKeys { it != "a" }) // {b=2, c=3} - println(map.mapValues { it.value * 10 }) // {a=10, b=20, c=30} - println(map.mapKeys { it.key.uppercase() }) // {A=1, B=2, C=3} - println(map.map { "${it.key}${it.value}" }) // [a1, b2, c3] — это List - println(map.toList()) // [(a, 1), (b, 2), (c, 3)] - println(map.entries.sortedByDescending { it.value }.first().key) // c -} -``` - -### Три реализации Map -| Реализация | Порядок | Доступ | Когда | -|---|---|---|---| -| `LinkedHashMap` (`mapOf`) | вставки | O(1) | по умолчанию | -| `HashMap` (`hashMapOf`) | не определён | O(1) | экономия памяти | -| `TreeMap` (`sortedMapOf`) | сортировки ключей | O(log n) | диапазонные запросы | - -## Выбор структуры -```mermaid -graph TD - A["Нужна коллекция"] --> B{"Пары ключ-значение?"} - B -->|да| M["Map"] - B -->|нет| C{"Дубликаты важны
или нужен индекс?"} - C -->|да| L["List"] - C -->|нет| D{"Частый поиск
элемента?"} - D -->|да| S["Set"] - D -->|нет| L -``` - -Практический критерий: если в коде есть `list.contains(x)` внутри цикла - это `Set`. Такая замена превращает O(n²) в O(n): -```kotlin -fun main() { - val ids = List(1000) { it } - val allowedList = List(1000) { it * 2 } - val allowedSet = allowedList.toSet() - - println(ids.count { it in allowedList }) // O(n*m) — 1000*1000 сравнений - println(ids.count { it in allowedSet }) // O(n) — то же 500, но быстро -} -``` - -## Специальные коллекции -```kotlin -fun main() { - val deque = ArrayDeque(listOf(2, 3)) - deque.addFirst(1) - deque.addLast(4) - println("$deque ${deque.removeFirst()} ${deque.removeLast()}") // [1, 2, 3, 4] 1 4 - - val grouped = listOf("яблоко", "арбуз", "банан").groupBy { it.first() } - println(grouped) // {я=[яблоко], а=[арбуз], б=[банан]} - - val assoc = listOf("a", "bb", "ccc").associateWith { it.length } - println(assoc) // {a=1, bb=2, ccc=3} -} -``` -`ArrayDeque` в Kotlin - своя реализация (не `java.util.ArrayDeque`), работает как стек и как очередь. Подробнее об `groupBy` и `associate`: [Операции](Операции.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `map.get(k)` возвращает `V` | `map[k]` возвращает `V?` | -> | `map.put(k, v)` | `map[k] = v` | -> | `new AbstractMap.SimpleEntry<>(k, v)` | `k to v` | -> | `for (Map.Entry e : map.entrySet())` | `for ((k, v) in map)` | -> | `map.computeIfAbsent(k, f)` | `map.getOrPut(k) { }` | -> | `Collections.unmodifiableList` | тип `List` | -> | `set.retainAll(other)` | `a intersect b` | -> | `list.get(i)` | `list[i]` | -> -> **`map[k]` возвращает nullable - и это правильно.** В Java `map.get(k)` имеет тип `V`, поэтому `int x = map.get("нет")` компилируется и даёт NPE при распаковке. Kotlin делает отсутствие ключа видимым в типе: -> ```kotlin -> fun main() { -> val map = mapOf("a" to 1) -> val x: Int = map["z"] ?: 0 // компилятор требует обработать null -> println(x) // 0 -> } -> ``` -> Обратная сторона: `map[k]!!` появляется в коде часто. Когда ключ гарантированно есть, честнее `getValue(k)` - он бросит `NoSuchElementException` с внятным сообщением. -> -> **Деструктуризация `Map.Entry`.** Java требует `e.getKey()`/`e.getValue()`; в Kotlin `for ((k, v) in map)` работает потому, что для `Map.Entry` объявлены функции-расширения `component1` и `component2`. Подробнее: [Деструктуризация](../Синтаксис%20и%20типы/Работа%20с%20данными/Деструктуризация.md) -> -> **Операции над множествами как инфиксные функции.** В Java `union` - это `addAll` на копии, `intersect` - `retainAll`, то есть операции мутирующие и требуют ручного копирования. В Kotlin `a union b` возвращает новое множество и читается как математическая запись. -> -> Сравнить: [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md) - -> [!warning] Подводные камни -> **`map[k]` не различает «нет ключа» и «значение null»:** -> ```kotlin -> fun main() { -> val map = mapOf("a" to null, "b" to 1) -> println(map["a"]) // null -> println(map["z"]) // null — то же самое! -> println("a" in map) // true -> println("z" in map) // false — вот так различать -> } -> ``` -> -> **`getValue` бросает, `get` - нет.** Это легко перепутать: -> ```kotlin -> fun main() { -> val map = mapOf("a" to 1) -> println(map["z"]) // null -> try { -> map.getValue("z") -> } catch (e: NoSuchElementException) { -> println("getValue бросает") // getValue бросает -> } -> } -> ``` -> -> **Изменяемый элемент в `Set` или ключ в `Map` ломает коллекцию:** -> ```kotlin -> data class Key(var id: Int) -> -> fun main() { -> val key = Key(1) -> val set = mutableSetOf(key) -> println(key in set) // true -> key.id = 2 // hashCode изменился! -> println(key in set) // false — элемент потерян -> println(set.size) // 1 — но он всё ещё внутри -> } -> ``` -> Ключи и элементы множеств обязаны быть неизменяемыми. `data class` с `var` в этой роли - ошибка. -> -> **`subList` - это вид на исходный список, а не копия:** -> ```kotlin -> fun main() { -> val list = mutableListOf(1, 2, 3, 4) -> val sub = list.subList(1, 3) -> list[1] = 99 -> println(sub) // [99, 3] — изменился -> list.add(5) -> try { -> println(sub) -> } catch (e: ConcurrentModificationException) { -> println("структурное изменение списка сломало subList") -> } -> } -> ``` -> Для независимой копии нужен `slice` или `toList()`. -> -> **`remove` на `MutableList` неоднозначен:** -> ```kotlin -> fun main() { -> val list = mutableListOf(10, 20, 30) -> list.removeAt(1) // по индексу -> println(list) // [10, 30] -> list.remove(10) // по значению -> println(list) // [30] -> } -> ``` -> В Kotlin, в отличие от Java, перегрузки по индексу у `remove` нет - только `removeAt`. Это как раз устраняет классическую Java-ловушку `list.remove(1)`. -> -> **`list.contains(x)` в цикле - это O(n²).** Самая частая проблема производительности с коллекциями. Если проверка повторяется, предварительно постройте `Set`. -> -> **`hashSetOf` и `hashMapOf` не сохраняют порядок** - вывод может отличаться между запусками и версиями JDK. Тест, завязанный на порядок, будет нестабильным. -> -> **`sortedSetOf` требует `Comparable` или компаратор** и бросит `ClassCastException` при вставке несравнимого элемента - в рантайме, а не при компиляции. -> -> **`+=` на `List` работает, но создаёт новую коллекцию:** -> ```kotlin -> fun main() { -> var list = listOf(1, 2) // var! -> list += 3 // новый список, не add -> println(list) // [1, 2, 3] -> -> val mutable = mutableListOf(1, 2) -> mutable += 3 // здесь настоящий add -> println(mutable) // [1, 2, 3] -> } -> ``` -> На `val list: List` оператор `+=` не скомпилируется. В цикле по большому списку `+=` на read-only коллекции даёт O(n²) копирований. - -Далее: [Изменяемость](Изменяемость.md) diff --git a/Kotlin/Коллекции/Sequence.md b/Kotlin/Коллекции/Sequence.md deleted file mode 100644 index 40e6396..0000000 --- a/Kotlin/Коллекции/Sequence.md +++ /dev/null @@ -1,368 +0,0 @@ ---- -title: Sequence -description: Ленивые вычисления, порядок обхода элементов и когда Sequence выигрывает у List -tags: - - kotlin - - collections ---- - -`Sequence` - ленивый аналог коллекции. Операции не выполняются до запроса результата, и элементы проходят всю цепочку по одному. - -```kotlin -fun main() { - val list = listOf(1, 2, 3, 4, 5) - - println("-- List:") - val eager = list - .map { println(" map $it"); it * 2 } - .filter { println(" filter $it"); it > 4 } - .first() - println(" результат $eager") - - println("-- Sequence:") - val lazy = list.asSequence() - .map { println(" map $it"); it * 2 } - .filter { println(" filter $it"); it > 4 } - .first() - println(" результат $lazy") -} -``` -`List` выполнит 5 `map` и 5 `filter` - всего 10 операций, создав два промежуточных списка. `Sequence` выполнит 3 `map` и 3 `filter` и остановится, как только найдёт первый подходящий элемент. - -## Два порядка обхода -Это ключ к пониманию `Sequence`: - -```mermaid -graph LR - subgraph List["List: по операциям"] - L1["map всех 5"] --> L2["filter всех 5"] --> L3["first"] - end - subgraph Seq["Sequence: по элементам"] - S1["1: map, filter"] --> S2["2: map, filter"] --> S3["3: map, filter → стоп"] - end -``` - -| | `List` | `Sequence` | -|---|---|---| -| порядок | операция за операцией | элемент за элементом | -| промежуточные коллекции | на каждом шаге | нет | -| вычисление | сразу | при терминальной операции | -| можно ли прервать | нет | да | -| бесконечные данные | нет | да | - -## Создание -```kotlin -fun main() { - println(listOf(1, 2, 3).asSequence().toList()) // [1, 2, 3] - println(sequenceOf(1, 2, 3).toList()) // [1, 2, 3] - println(emptySequence().toList()) // [] - - println(generateSequence(1) { it * 2 }.take(5).toList()) // [1, 2, 4, 8, 16] - println(generateSequence(1) { if (it < 5) it + 1 else null }.toList()) // [1, 2, 3, 4, 5] - - val built = sequence { - yield(1) - yieldAll(listOf(2, 3)) - yield(4) - } - println(built.toList()) // [1, 2, 3, 4] -} -``` -`generateSequence` с `null` в лямбде - способ задать конец. `sequence { }` - билдер с `yield`, внутри него можно писать любую логику, включая циклы. - -## Промежуточные и терминальные операции -```kotlin -fun main() { - val seq = sequenceOf(1, 2, 3) - - // промежуточные: возвращают Sequence, ничего не считают - val chain = seq.map { it * 2 }.filter { it > 2 }.take(2) - println("цепочка построена, вычислений не было") - - // терминальные: запускают вычисление - println(chain.toList()) // [4, 6] - println(seq.sum()) // 6 - println(seq.count()) // 3 - println(seq.first()) // 1 - println(seq.any { it > 2 }) // true -} -``` - -| Промежуточные | Терминальные | -|---|---| -| `map`, `filter`, `flatMap` | `toList`, `toSet`, `toMap` | -| `take`, `drop`, `distinct` | `first`, `last`, `single` | -| `sorted`, `chunked`, `windowed` | `sum`, `count`, `average`, `max` | -| `onEach`, `mapNotNull` | `any`, `all`, `none`, `forEach` | -| `zip`, `plus` | `fold`, `reduce`, `joinToString` | - -## Бесконечные последовательности -То, чего `List` не умеет в принципе: -```kotlin -fun main() { - val naturals = generateSequence(1) { it + 1 } - - println(naturals.take(5).toList()) // [1, 2, 3, 4, 5] - println(naturals.filter { it % 7 == 0 }.first()) // 7 - println(naturals.map { it * it }.takeWhile { it < 50 }.toList()) // [1, 4, 9, 16, 25, 36, 49] - - val fib = generateSequence(0 to 1) { (a, b) -> b to (a + b) }.map { it.first } - println(fib.take(10).toList()) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] -} -``` -Важно: терминальная операция без ограничения на бесконечной последовательности зависнет. `naturals.toList()` не завершится никогда. - -## Sequence в цепочке с ранним выходом -```kotlin -data class Record3(val id: Int, val active: Boolean, val score: Int) - -fun main() { - val records = List(1_000_000) { Record3(it, it % 100 == 0, it % 7) } - - val found = records.asSequence() - .filter { it.active } - .map { it.score * 2 } - .firstOrNull { it > 10 } - println(found) // 12 - - val top3 = records.asSequence() - .filter { it.active } - .take(3) - .map { it.id } - .toList() - println(top3) // [0, 100, 200] -} -``` -Здесь `Sequence` обрабатывает несколько сотен элементов вместо миллиона. На `List` каждый `filter`/`map` прошёл бы весь миллион и создал два списка. - -## Когда Sequence не выигрывает -```kotlin -fun main() { - val small = listOf(1, 2, 3, 4, 5) - - // Sequence здесь только добавляет накладные расходы - println(small.map { it * 2 }.sum()) // 30 - println(small.asSequence().map { it * 2 }.sum()) // 30, но медленнее -} -``` -`Sequence` создаёт объект-обёртку на каждую операцию и вызывает лямбды через интерфейс итератора. На маленьких коллекциях и коротких цепочках это дороже, чем прямой проход. - -Практическое правило: - -| Условие | Выбор | -|---|---| -| меньше ~1000 элементов, 1-2 операции | `List` | -| много элементов и 3+ операций | `Sequence` | -| есть `first`, `find`, `take`, `any` после цепочки | `Sequence` | -| источник бесконечный или потоковый (файл, БД) | `Sequence` | -| нужен результат целиком, операций мало | `List` | -| нужен индексный доступ к результату | `List` | - -## Операции, которые ломают ленивость -Некоторые операции обязаны прочитать всё, прежде чем выдать первый элемент: -```kotlin -fun main() { - val seq = sequenceOf(3, 1, 2) - .onEach { println("читаю $it") } - .sorted() // нужны все элементы - println("до first ничего не прочитано") - println(seq.first()) // здесь прочитаются все три -} -``` -Такие операции - `sorted`, `sortedBy`, `distinct` (частично), `groupBy`, `chunked`. Ставьте их как можно позже в цепочке, а `filter` - как можно раньше. - -## Sequence одноразов -```kotlin -fun main() { - val seq = listOf(1, 2, 3).asSequence().map { it * 2 } - println(seq.toList()) // [2, 4, 6] - println(seq.toList()) // [2, 4, 6] — здесь повезло: источник List - - val once = sequence { yield(1); yield(2) } - println(once.toList()) // [1, 2] - println(once.toList()) // [1, 2] — тоже работает, лямбда вызвана заново - - val fromIterator = listOf(1, 2, 3).iterator().asSequence() - println(fromIterator.toList()) // [1, 2, 3] - try { - fromIterator.toList() - } catch (e: IllegalStateException) { - println(e.message) // This sequence can be consumed only once. - } -} -``` -Последовательность из итератора обходится один раз: `Iterator.asSequence()` оборачивает результат в `constrainOnce()`, который фиксирует это ограничение и бросает `IllegalStateException` при повторной попытке. Тот же механизм доступен явно: -```kotlin -fun main() { - val seq = sequenceOf(1, 2).constrainOnce() - println(seq.toList()) // [1, 2] - try { - seq.toList() - } catch (e: IllegalStateException) { - println("повторный обход запрещён") - } -} -``` - -## Чтение файла -Главный практический случай: данные, которые не влезают в память. -```kotlin -import java.io.File - -fun main() { - val tmp = File.createTempFile("demo", ".txt") - tmp.writeText((1..100).joinToString("\n")) - - val sum = tmp.useLines { lines -> // Sequence - lines.map { it.toInt() }.filter { it % 2 == 0 }.sum() - } - println(sum) // 2550 - - val firstBig = tmp.useLines { lines -> lines.first { it.toInt() > 50 } } - println(firstBig) // 51 — файл прочитан до 51-й строки, не целиком - - tmp.delete() -} -``` -`useLines` даёт `Sequence` и закрывает файл после работы. Альтернатива `readLines()` загрузила бы весь файл в `List`. - -## Sequence и Flow -`Sequence` - ленивый, но **синхронный**: получение следующего элемента блокирует поток. Для асинхронных источников нужен `Flow`: - -| | `Sequence` | `Flow` | -|---|---|---| -| вычисление | лениво | лениво | -| блокировка | да | нет, `suspend` | -| внутри можно `delay` | нет | да | -| источник | память, файл, генератор | сеть, БД, события | - -Подробнее: [Flow](../Корутины/Flow.md) - -> [!info] Отличия от Java -> | Java Stream | Kotlin Sequence | -> |---|---| -> | `list.stream()` | `list.asSequence()` | -> | `Stream.of(1, 2)` | `sequenceOf(1, 2)` | -> | `Stream.iterate(1, f)` | `generateSequence(1, f)` | -> | `collect(toList())` | `toList()` | -> | `.parallel()` | **нет аналога** | -> | `IntStream`, `LongStream` | нет, только `Sequence` | -> | `Files.lines(path)` | `file.useLines { }` | -> | нет генератора с `yield` | `sequence { yield(x) }` | -> -> **`Sequence` и `Stream` - почти одно и то же.** Ленивость, промежуточные и терминальные операции, короткое замыкание на `findFirst`/`first` - всё совпадает. Поэтому опыт работы со стримами переносится напрямую, и наоборот: если в Java вы написали бы `.stream()`, в Kotlin нужен `.asSequence()`. -> -> **Главное отличие - нет параллелизма.** `Stream.parallel()` распределяет работу по `ForkJoinPool`; у `Sequence` такого нет вообще. Причина в том, что для конкурентности в Kotlin предусмотрены корутины, а не параллельные стримы. Если нужна параллельная обработка - либо используйте Java-стримы напрямую (они доступны), либо корутины: -> ```kotlin -> fun main() { -> // Java Stream доступен из Kotlin -> val sum = listOf(1, 2, 3).stream().parallel().mapToInt { it * 2 }.sum() -> println(sum) // 12 -> } -> ``` -> -> **Нет специализаций для примитивов.** В Java `IntStream` избегает упаковки: `list.stream().mapToInt(x -> x).sum()` работает с `int`. `Sequence` упаковывает каждый элемент в `Integer`. На числовых расчётах по большим данным Java-стримы могут оказаться быстрее. -> -> **`sequence { yield(x) }` - генератор, которого в Java нет.** Он реализован на том же механизме приостановки, что и корутины, поэтому внутри можно писать обычные циклы и условия: -> ```kotlin -> fun main() { -> val primes = sequence { -> var n = 2 -> while (true) { -> if ((2 until n).none { n % it == 0 }) yield(n) -> n++ -> } -> } -> println(primes.take(5).toList()) // [2, 3, 5, 7, 11] -> } -> ``` -> В Java это требует либо отдельного потока, либо ручной реализации `Iterator` с сохранением состояния. -> -> Сравнить: [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md) - -> [!warning] Подводные камни -> **Без терминальной операции не произойдёт ничего:** -> ```kotlin -> fun main() { -> listOf(1, 2, 3).asSequence().map { println("это не выведется"); it } -> println("конец") // только "конец" -> } -> ``` -> Это частая ошибка при замене `forEach` на цепочку: если забыть терминальную операцию, код просто не выполнится, и компилятор не предупредит. -> -> **`Sequence` на маленьких коллекциях медленнее.** Обёртки и вызовы через итератор стоят дороже прямого цикла. Для списка из 10 элементов `asSequence()` - пессимизация. -> -> **Терминальная операция на бесконечной последовательности зависает:** -> ```kotlin -> fun main() { -> val naturals = generateSequence(1) { it + 1 } -> println(naturals.take(3).toList()) // [1, 2, 3] — ок -> // naturals.toList() // зависнет навсегда -> // naturals.count() // тоже -> // naturals.filter { it > 10 }.toList() // и это -> } -> ``` -> Любая цепочка из бесконечного источника обязана содержать `take`, `takeWhile`, `first` или `find`. -> -> **`sorted` и `groupBy` убивают ленивость** - они читают всё до первого результата. Цепочка `asSequence().sorted().first()` не быстрее, чем на `List`. -> -> **Последовательность из итератора обходится один раз** - второй вызов падает: -> ```kotlin -> fun main() { -> val seq = listOf(1, 2).iterator().asSequence() -> println(seq.count()) // 2 -> try { -> seq.count() -> } catch (e: IllegalStateException) { -> println("повторный обход: ${e.message}") -> } -> } -> ``` -> Исключение здесь лучше молчаливого пустого результата, но возникает оно в рантайме и только на втором обходе - в редкой ветке кода его легко не заметить. Если последовательность может понадобиться дважды, сохраните `toList()`. -> -> **Побочные эффекты в промежуточных операциях выполняются в неожиданном порядке:** -> ```kotlin -> fun main() { -> val log = mutableListOf() -> sequenceOf(1, 2) -> .onEach { log.add("A$it") } -> .onEach { log.add("B$it") } -> .toList() -> println(log) // [A1, B1, A2, B2] — не [A1, A2, B1, B2] -> } -> ``` -> На `List` вывод был бы `[A1, A2, B1, B2]`. Код, зависящий от порядка побочных эффектов, при переходе на `Sequence` сломается. -> -> **Повторный обход пересчитывает всё заново:** -> ```kotlin -> fun main() { -> var calls = 0 -> val seq = listOf(1, 2, 3).asSequence().map { calls++; it * 2 } -> seq.toList() -> seq.toList() -> println(calls) // 6 — лямбда вызвана дважды для каждого элемента -> } -> ``` -> Если результат нужен несколько раз, сохраните его в `List`. -> -> **`Sequence` синхронна.** Внутри её лямбд нельзя вызывать `suspend`-функции - для этого существует `Flow`. Попытка использовать `Sequence` для сетевых запросов приведёт к блокировке потока. -> -> **`useLines` закрывает файл по выходу из лямбды.** Вернуть `Sequence` наружу нельзя - она окажется на закрытом ресурсе: -> ```kotlin -> import java.io.File -> -> fun main() { -> val tmp = File.createTempFile("leak", ".txt") -> tmp.writeText("1\n2") -> val leaked = tmp.useLines { it } // Sequence на закрытом файле -> try { -> println(leaked.toList()) -> } catch (e: java.io.IOException) { -> println("поток закрыт: ${e.message}") // поток закрыт: Stream closed -> } -> tmp.delete() -> } -> ``` -> Внутри лямбды нужно получить готовое значение: `useLines { it.toList() }` или сразу агрегат. - -Далее: [Коллекции под капотом](Коллекции%20под%20капотом.md) diff --git a/Kotlin/Коллекции/Изменяемость.md b/Kotlin/Коллекции/Изменяемость.md deleted file mode 100644 index 6e761bd..0000000 --- a/Kotlin/Коллекции/Изменяемость.md +++ /dev/null @@ -1,323 +0,0 @@ ---- -title: Изменяемость -description: Read-only против immutable, защитное копирование и персистентные коллекции -tags: - - kotlin - - collections ---- - -`List` в Kotlin - **read-only**, а не immutable. Это разные вещи, и их смешение - источник самых неприятных ошибок с коллекциями. - -| | read-only (`List`) | immutable | -|---|---|---| -| изменить через эту ссылку | нельзя | нельзя | -| может измениться вообще | **да** | нет | -| гарантия | интерфейс без мутаторов | сам объект неизменяем | -| есть в Kotlin stdlib | да | нет (только внешняя библиотека) | - -## Почему read-only не immutable -```kotlin -fun main() { - val mutable = mutableListOf(1, 2) - val readOnly: List = mutable // тот же объект, другой интерфейс - - mutable.add(3) - println(readOnly) // [1, 2, 3] — изменился под вами -} -``` -`MutableList` наследует `List`, поэтому любая изменяемая коллекция подходит там, где ожидается `List`. Приведение типа объект не копирует. - -Это создаёт конкретную проблему в API: -```kotlin -class Repository4 { - private val items = mutableListOf(1, 2, 3) - - fun getItems(): List = items // утечка: вернули ссылку на изменяемое - fun add(x: Int) { items.add(x) } -} - -fun main() { - val repo = Repository4() - val snapshot = repo.getItems() - println(snapshot) // [1, 2, 3] - repo.add(4) - println(snapshot) // [1, 2, 3, 4] — «снимок» изменился -} -``` -Клиент получил `List` и вправе считать, что значение стабильно - например, положить его в `Map` как ключ или обойти в другом потоке. Ни то ни другое здесь не безопасно. - -## Защитное копирование -```kotlin -class Repository5 { - private val items = mutableListOf(1, 2, 3) - - fun getItems(): List = items.toList() // копия - fun add(x: Int) { items.add(x) } -} - -fun main() { - val repo = Repository5() - val snapshot = repo.getItems() - repo.add(4) - println("$snapshot / ${repo.getItems()}") // [1, 2, 3] / [1, 2, 3, 4] -} -``` -`toList()` создаёт новый `ArrayList`. Цена - O(n) и лишний объект; выгода - значение, на которое можно полагаться. - -Варианты копирования: -```kotlin -fun main() { - val source = mutableListOf(1, 2, 3) - - println(source.toList()::class.simpleName) // ArrayList - println(source.toMutableList()::class.simpleName) // ArrayList - println(source.toSet()::class.simpleName) // LinkedHashSet - println(ArrayList(source)::class.simpleName) // ArrayList - println(List(source.size) { source[it] }) // [1, 2, 3] -} -``` - -Когда копировать: -- коллекция уходит за границу класса или модуля и должна быть стабильной -- коллекция попадает в `Map`/`Set` как ключ или элемент -- значение читается из другого потока - -Когда не копировать: -- коллекция создана тут же и больше нигде не хранится -- вызывающий - код того же класса -- коллекция огромная, а изменения точно не будет (документируйте это) - -## Неизменяемость на уровне содержимого -Даже честная копия списка не защищает от изменения **элементов**: -```kotlin -class Item(var name: String) - -fun main() { - val items = listOf(Item("a"), Item("b")) - val copy = items.toList() - - items[0].name = "изменено" - println(copy[0].name) // изменено — копия поверхностная -} -``` -`toList()` копирует только ссылки. Настоящая защита - неизменяемые элементы: -```kotlin -data class ItemRo(val name: String) - -fun main() { - val items = listOf(ItemRo("a")) - // items[0].name = "x" // ошибка: val - val changed = items.map { it.copy(name = "изменено") } - println("${items[0].name} / ${changed[0].name}") // a / изменено -} -``` -Это общее правило: неизменяемость полезна, когда она рекурсивна. `List` с изменяемым `T` даёт лишь половину гарантии. - -## val и изменяемость -Два независимых понятия, которые постоянно путают: -```kotlin -fun main() { - val a = mutableListOf(1) // ссылку сменить нельзя, содержимое — можно - a.add(2) - println(a) // [1, 2] - // a = mutableListOf(3) // ошибка - - var b = listOf(1) // ссылку сменить можно, содержимое — нельзя - b = listOf(1, 2) - println(b) // [1, 2] - // b.add(3) // ошибка -} -``` - -| | `val` | `var` | -|---|---|---| -| `List` | ничего не изменить | можно заменить целиком | -| `MutableList` | можно менять содержимое | и то и другое | - -Самый частый правильный выбор - `val` + `List`. Подробнее: [Переменные](../Синтаксис%20и%20типы/Базовый%20синтаксис/Переменные.md) - -## Функциональное обновление -Вместо изменения на месте - новое значение: -```kotlin -fun main() { - val original = listOf(1, 2, 3) - - println(original + 4) // [1, 2, 3, 4] - println(original - 2) // [1, 3] - println(original.map { it * 10 }) // [10, 20, 30] - println(original.filter { it != 2 }) // [1, 3] - println(original.toMutableList().apply { this[0] = 99 }) // [99, 2, 3] - println(original) // [1, 2, 3] — не тронут -} -``` -Для `Map` то же: -```kotlin -fun main() { - val config = mapOf("host" to "localhost", "port" to "8080") - - println(config + ("port" to "9090")) // {host=localhost, port=9090} - println(config - "port") // {host=localhost} - println(config + mapOf("tls" to "on")) // {host=localhost, port=8080, tls=on} -} -``` -Цена - копирование на каждую операцию. Для небольших коллекций и нечастых обновлений это правильный выбор по умолчанию: такой код проще отлаживать, потому что значение не меняется за спиной. - -## Изменяемое состояние внутри, read-only снаружи -Рабочий шаблон для класса с коллекцией: -```kotlin -class Playlist { - private val _tracks = mutableListOf() - val tracks: List get() = _tracks.toList() // копия при чтении - - fun add(track: String) { _tracks.add(track) } - fun size() = _tracks.size -} - -fun main() { - val p = Playlist() - p.add("песня 1") - val snapshot = p.tracks - p.add("песня 2") - println("$snapshot / ${p.tracks}") // [песня 1] / [песня 1, песня 2] -} -``` -Если копия на каждое чтение слишком дорога, альтернатива - возвращать `_tracks` напрямую и документировать, что значение живое. Тогда тип должен быть осмысленным: `List` здесь обманывает читателя, и честнее отдавать `Sequence` или явный метод `snapshot()`. - -## Неизменяемость и многопоточность -```kotlin -fun main() { - val shared = mutableListOf(1, 2, 3) - - // Небезопасно: обход в одном потоке + add в другом = ConcurrentModificationException - // Безопасно: работать с копией - val safe = shared.toList() - println(safe.sum()) // 6 - - val sync = java.util.Collections.synchronizedList(mutableListOf(1, 2)) - println(sync.size) // 2 — но обход всё равно нужно синхронизировать вручную - val concurrent = java.util.concurrent.CopyOnWriteArrayList(listOf(1, 2)) - println(concurrent.size) // 2 -} -``` -Неизменяемое значение потокобезопасно по определению - именно поэтому в конкурентном коде предпочитают read-only копии, а не блокировки. Подробнее: [Разделяемое состояние](../Корутины/Разделяемое%20состояние.md) - -## Персистентные коллекции -В stdlib настоящих immutable-коллекций нет. Их даёт библиотека `kotlinx.collections.immutable`: -```kotlin -// Требует зависимости org.jetbrains.kotlinx:kotlinx-collections-immutable. -// val list: PersistentList = persistentListOf(1, 2, 3) -// val updated = list.add(4) // новый список, старый цел -// println("$list / $updated") // [1, 2, 3] / [1, 2, 3, 4] -// -// Тип ImmutableList гарантирует, что коллекция не изменится вообще, -// а PersistentList добавляет эффективное «обновление» без полного копирования. -``` -Отличие от `toList()`: персистентная коллекция при добавлении переиспользует внутреннюю структуру, поэтому `add` стоит O(log n), а не O(n). Это имеет смысл, когда обновления частые, а коллекция большая. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `Collections.unmodifiableList(x)` | тип `List` | -> | `List.copyOf(x)` (Java 10+) | `x.toList()` | -> | `List.of(1, 2)` - immutable | `listOf(1, 2)` - read-only | -> | нарушение видно в рантайме | видно при компиляции | -> | `final List` | `val list` | -> | Guava `ImmutableList` | `kotlinx.collections.immutable` | -> -> **`unmodifiableList` и `List` решают одну задачу по-разному.** Java-обёртка - объект-декоратор: тип остаётся `List`, метод `add` есть, но бросает исключение в рантайме. Kotlin убирает метод из интерфейса, поэтому ошибка видна компилятору и обёртка не нужна: -> ```java -> List ro = Collections.unmodifiableList(src); -> ro.add(1); // компилируется, падает в рантайме -> ``` -> Но у Java-обёртки есть преимущество, которого нет у Kotlin: она действительно запрещает изменение через **эту** ссылку даже при касте. Kotlin-ский `List` каст к `MutableList` может пережить, если под ним `ArrayList`. -> -> **`List.of` в Java сильнее, чем `listOf` в Kotlin.** `List.of(1, 2)` создаёт по-настоящему неизменяемую коллекцию: любая попытка изменения, в том числе из другого места, бросит исключение. `listOf(1, 2)` даёт неизменяемую реализацию только случайно - это деталь, на которую нельзя полагаться, потому что `listOf` на самом деле описывает интерфейс, а не гарантию объекта. -> -> **Защитное копирование нужно в обоих языках.** `private final List` в Java и `private val` в Kotlin одинаково не защищают содержимое. Разница в том, что в Kotlin ошибку легче не заметить: тип `List` в сигнатуре выглядит как обещание неизменяемости, которого он не даёт. -> -> Сравнить: [Инкапсуляция в Java](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md) - -> [!warning] Подводные камни -> **Возврат `MutableList` под типом `List` - утечка:** -> ```kotlin -> class Leaky2 { -> private val items = mutableListOf(1, 2) -> fun items(): List = items -> } -> -> fun main() { -> val l = Leaky2() -> val view = l.items() -> @Suppress("UNCHECKED_CAST") -> (view as MutableList).add(3) // клиент изменил приватное состояние -> println(l.items()) // [1, 2, 3] -> } -> ``` -> Каст сработал, потому что под ссылкой настоящий `ArrayList`. Защита - `toList()`. -> -> **`toList()` копирует поверхностно** - элементы остаются общими. Для вложенных структур нужна глубокая копия: -> ```kotlin -> fun main() { -> val nested = listOf(mutableListOf(1), mutableListOf(2)) -> val shallow = nested.toList() -> nested[0].add(99) -> println(shallow[0]) // [1, 99] — внутренний список общий -> -> val deep = nested.map { it.toList() } -> nested[0].add(100) -> println(deep[0]) // [1, 99] — а этот цел -> } -> ``` -> -> **`val` не делает коллекцию неизменяемой** - только ссылку. `val items = mutableListOf()` изменяется свободно. -> -> **Из Java read-only коллекция изменяема:** -> ```kotlin -> fun provide(): List = mutableListOf(1, 2) -> -> fun main() { println(provide()) } // [1, 2] -> ``` -> Java-код получит `java.util.List` и вызовет `add` без ошибок компиляции. Для API, доступного из Java, копируйте или используйте `Collections.unmodifiableList`. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md) -> -> **Изменяемый объект как ключ `Map` теряется:** -> ```kotlin -> data class MutableKey(var id: Int) -> -> fun main() { -> val key = MutableKey(1) -> val map = mutableMapOf(key to "значение") -> key.id = 2 -> println(map[key]) // null — хеш изменился -> println(map.size) // 1 — запись на месте, но недостижима -> } -> ``` -> -> **`+=` на `var list: List` в цикле - это O(n²):** -> ```kotlin -> fun main() { -> var slow = listOf() -> repeat(1000) { slow += it } // 1000 копирований списка -> println(slow.size) // 1000 -> -> val fast = buildList { repeat(1000) { add(it) } } -> println(fast.size) // 1000, одна коллекция -> } -> ``` -> -> **`ConcurrentModificationException` при изменении во время обхода:** -> ```kotlin -> fun main() { -> val list = mutableListOf(1, 2, 3) -> try { -> for (x in list) if (x == 2) list.remove(x) -> } catch (e: ConcurrentModificationException) { -> println("нельзя изменять во время обхода") -> } -> list.removeAll { it == 2 } // так правильно -> println(list) // [1, 3] -> } -> ``` -> -> **Обёртка `synchronizedList` не делает обход потокобезопасным** - атомарны только отдельные методы. Итерацию нужно оборачивать в `synchronized` вручную. - -Далее: [Операции](Операции.md) diff --git a/Kotlin/Коллекции/Коллекции под капотом.md b/Kotlin/Коллекции/Коллекции под капотом.md deleted file mode 100644 index 0d5f1cd..0000000 --- a/Kotlin/Коллекции/Коллекции под капотом.md +++ /dev/null @@ -1,282 +0,0 @@ ---- -title: Коллекции под капотом -description: Что лежит в байт-коде, сложность операций, расход памяти и упаковка примитивов -tags: - - kotlin - - collections - - performance ---- - -Отдельных классов коллекций у Kotlin нет. `List`, `MutableList`, `Map` - это интерфейсы, которые при компиляции отображаются в типы `java.util`. - -```kotlin -fun main() { - println(listOf(1, 2, 3).javaClass.name) // java.util.Arrays$ArrayList - println(mutableListOf(1, 2).javaClass.name) // java.util.ArrayList - println(setOf(1, 2).javaClass.name) // java.util.LinkedHashSet - println(mapOf("a" to 1, "b" to 2).javaClass.name) // java.util.LinkedHashMap - println(emptyList().javaClass.name) // kotlin.collections.EmptyList - println(listOf(1).javaClass.name) // java.util.Collections$SingletonList - println(mapOf("a" to 1).javaClass.name) // java.util.Collections$SingletonMap -} -``` - -Обратите внимание: `listOf` возвращает **три разные** реализации в зависимости от числа аргументов, и `mapOf` ведёт себя так же. Это оптимизация под частые случаи - пустая коллекция и коллекция из одного элемента не требуют ни массива, ни хеш-таблицы. - -## Соответствие типов -| Kotlin | JVM | Примечание | -|---|---|---| -| `List` | `java.util.List` | тот же интерфейс | -| `MutableList` | `java.util.List` | **тот же**, различия только в компиляторе | -| `Map` | `java.util.Map` | | -| `Array` | `T[]` | настоящий массив | -| `IntArray` | `int[]` | без упаковки | -| `Array` | `Integer[]` | с упаковкой | - -`List` и `MutableList` - это один и тот же `java.util.List` в байт-коде. Разделение существует только при компиляции Kotlin, что объясняет две вещи: из Java read-only коллекция изменяема, и каст `List` в `MutableList` компилируется. - -```kotlin -fun main() { - val ro: List = mutableListOf(1, 2) - println(ro is MutableList<*>) // true — в рантайме это один тип -} -``` - -## Сложность операций -| Операция | `ArrayList` | `LinkedHashSet` | `LinkedHashMap` | `TreeMap` | -|---|---|---|---|---| -| доступ по индексу | O(1) | - | - | - | -| поиск элемента | O(n) | O(1) | O(1) по ключу | O(log n) | -| вставка в конец | O(1)* | O(1) | O(1) | O(log n) | -| вставка в начало | O(n) | O(1) | O(1) | O(log n) | -| удаление по индексу | O(n) | - | - | - | -| удаление по значению | O(n) | O(1) | O(1) | O(log n) | - -\* амортизированная: при переполнении массив копируется в новый, вдвое больший. - -Практические следствия: -```kotlin -fun main() { - val list = List(10_000) { it } - val set = list.toSet() - - // O(n) против O(1) - println(9999 in list) // true, но обошёл весь список - println(9999 in set) // true, один хеш - - // вставка в начало ArrayList — сдвиг всего массива - val deque = ArrayDeque(list) - deque.addFirst(-1) // O(1) - println(deque.first()) // -1 -} -``` - -## Упаковка примитивов -Главный источник неожиданных расходов памяти: -```kotlin -fun main() { - val boxed: List = List(5) { it } // каждый элемент — объект Integer - val primitive: IntArray = IntArray(5) { it } // настоящий int[] - - println(boxed.sum()) // 10 - println(primitive.sum()) // 10 - println(primitive.javaClass.name) // [I — массив int -} -``` - -Разница в памяти на 1 млн элементов: - -| Структура | Примерный размер | -|---|---| -| `IntArray(1_000_000)` | ~4 МБ | -| `Array(1_000_000)` | ~20 МБ (ссылки + объекты) | -| `List` из 1 млн | ~20 МБ | - -Кеш `Integer` смягчает это для малых чисел: -```kotlin -fun main() { - val a: Int? = 127 - val b: Int? = 127 - println(a === b) // true — из кеша -128..127 - - val c: Int? = 128 - val d: Int? = 128 - println(c === d) // false — разные объекты - println(c == d) // true — значения равны -} -``` -Это ровно та же ловушка, что с `Integer` в Java. Правило: для чисел всегда `==`, никогда `===`. Подробнее: [Числа и переполнение](../Синтаксис%20и%20типы/Работа%20с%20данными/Числа%20и%20переполнение.md) - -Специализированные массивы без упаковки: -```kotlin -fun main() { - println(intArrayOf(1, 2, 3).sum()) // 6 - println(doubleArrayOf(1.0, 2.0).average()) // 1.5 - println(booleanArrayOf(true, false).count { it }) // 1 - println(longArrayOf(1L).size) // 1 -} -``` -Их стоит использовать, когда элементов много и они числовые: матрицы, буферы, обработка сигналов. В обычном бизнес-коде разница несущественна. - -## Рост ArrayList -```kotlin -fun main() { - val withoutCapacity = ArrayList() // начальная ёмкость 10 - repeat(1000) { withoutCapacity.add(it) } // ~8 перевыделений массива - - val withCapacity = ArrayList(1000) // сразу нужный размер - repeat(1000) { withCapacity.add(it) } // перевыделений нет - - println("${withoutCapacity.size} ${withCapacity.size}") // 1000 1000 -} -``` -При каждом переполнении создаётся массив в 1.5 раза больше и копируется содержимое. Для заранее известного размера задавайте ёмкость - это убирает лог n копирований. - -## Что делает цепочка операций -```kotlin -fun main() { - val source = List(1000) { it } - - val result = source - .map { it * 2 } // новый ArrayList на 1000 - .filter { it > 100 } // новый ArrayList на ~950 - .take(5) // новый ArrayList на 5 - println(result) // [102, 104, 106, 108, 110] -} -``` -Три промежуточных списка, 1000 + 950 операций ради 5 результатов. Та же цепочка на `Sequence` не создаст ни одного промежуточного списка и выполнит ~56 операций. Подробнее: [Sequence](Sequence.md) - -Измерить разницу можно так: -```kotlin -fun main() { - val source = List(1_000_000) { it } - - val t1 = System.nanoTime() - val a = source.map { it * 2 }.filter { it > 100 }.first() - val eager = (System.nanoTime() - t1) / 1_000_000 - - val t2 = System.nanoTime() - val b = source.asSequence().map { it * 2 }.filter { it > 100 }.first() - val lazy = (System.nanoTime() - t2) / 1_000_000 - - println("$a == $b") // 102 == 102 - println("list ${eager}мс, sequence ${lazy}мс — ленивый быстрее: ${lazy < eager}") -} -``` - -## Inline и отсутствие объектов-лямбд -Операции над коллекциями объявлены `inline`, поэтому лямбда не создаёт объект: -```kotlin -fun main() { - val sum = listOf(1, 2, 3).filter { it > 1 }.sumOf { it * 2 } - println(sum) // 10 -} -``` -Компилятор подставляет тело лямбды прямо в цикл - в байт-коде нет ни объекта `Function1`, ни виртуального вызова. Это то, чего не даёт Java-стрим: там каждая лямбда - объект, а цепочка строит конвейер из `Spliterator`-ов. - -Исключение - `Sequence`: там операции не могут быть полностью inline, потому что элементы обрабатываются лениво через итератор. Это и есть источник накладных расходов `Sequence` на малых данных. Подробнее: [Inline-функции](../Функциональный%20Kotlin/Inline-функции.md) - -## Как выбирают структуру -```kotlin -fun main() { - // частый поиск -> Set - val allowed = setOf("a", "b", "c") - println(listOf("a", "x").count { it in allowed }) // 1 - - // частое добавление в начало и конец -> ArrayDeque - val queue = ArrayDeque() - queue.addLast(1); queue.addFirst(0) - println(queue) // [0, 1] - - // нужен отсортированный обход и диапазоны -> TreeMap - val sorted = sortedMapOf(3 to "c", 1 to "a", 2 to "b") - println(sorted.headMap(3)) // {1=a, 2=b} - - // много числовых данных -> примитивный массив - val buffer = IntArray(1000) - println(buffer.size) // 1000 -} -``` - -## Потокобезопасность -```kotlin -import java.util.concurrent.ConcurrentHashMap - -fun main() { - val map = ConcurrentHashMap() - map["a"] = 1 - map.compute("a") { _, v -> (v ?: 0) + 1 } - println(map) // {a=2} - - val sync = java.util.Collections.synchronizedMap(mutableMapOf("a" to 1)) - println(sync.size) // 1 -} -``` -Обычные коллекции Kotlin не потокобезопасны - это те же `ArrayList` и `HashMap`. Варианты: -- read-only копия на каждый поток (`toList()`) - проще всего -- `ConcurrentHashMap` и `CopyOnWriteArrayList` из JDK -- корутины с ограничением доступа одним потоком или `Mutex` - -Подробнее: [Разделяемое состояние](../Корутины/Разделяемое%20состояние.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `ArrayList` явно | `mutableListOf()` -> `ArrayList` | -> | `new HashMap<>()` | `mutableMapOf()` -> `LinkedHashMap` | -> | `IntStream` без упаковки | `IntArray` или Java-стрим | -> | лямбда - объект `Function` | лямбда инлайнится | -> | `Stream` ленив | `Iterable`-операции энергичны | -> | `new ArrayList<>(100)` | `ArrayList(100)` | -> -> **Коллекции те же самые - это не «Kotlin-коллекции».** Любая `List` из Kotlin принимается Java-методом, принимающим `java.util.List`, без конвертации и накладных расходов. Это принципиальное отличие от языков со своей библиотекой коллекций (например, Scala), где на границе нужны обёртки. -> -> **Умолчание `LinkedHashMap` вместо `HashMap`.** `mutableMapOf()` создаёт `LinkedHashMap`, который на ~20% тяжелее по памяти (хранит ссылки на соседей), но даёт предсказуемый порядок обхода. Если порядок не нужен и записей миллионы, `hashMapOf()` экономнее. -> -> **Инлайн лямбд - преимущество перед стримами.** В Java `list.stream().filter(x -> x > 1)` создаёт объект лямбды и конвейер стрима. В Kotlin `list.filter { it > 1 }` компилируется в обычный цикл `for` с телом лямбды внутри - объектов не создаётся вовсе. Для коротких цепочек на небольших коллекциях Kotlin обычно быстрее Java-стримов именно поэтому. -> -> **Нет `IntStream`.** Java даёт специализации для `int`, `long`, `double`, избегающие упаковки в цепочках. В Kotlin эквивалента нет: `Sequence` упаковывает. Для числовых расчётов на больших объёмах используйте `IntArray` с обычным циклом или вызывайте Java-стримы. -> -> Сравнить: [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md), [JIT](../../Программирование%20на%20Java/JVM/JIT.md) - -> [!warning] Подводные камни -> **`listOf` возвращает разные классы** - полагаться на конкретный тип нельзя: -> ```kotlin -> fun main() { -> println(listOf().javaClass.simpleName) // EmptyList -> println(listOf(1).javaClass.simpleName) // SingletonList -> println(listOf(1, 2).javaClass.simpleName) // ArrayList (Arrays$ArrayList) -> } -> ``` -> Ни один из них не поддерживает `add`, но по разным причинам, и сообщения об ошибках будут разными. -> -> **`Arrays$ArrayList` - не `java.util.ArrayList`.** Это обёртка над массивом с фиксированным размером: `set` работает, `add` бросает `UnsupportedOperationException`. -> -> **`List` упаковывает каждое число.** Для миллиона элементов это ~20 МБ вместо 4 МБ и нагрузка на сборщик мусора. В горячем численном коде используйте `IntArray`. -> -> **`===` на числах даёт разные результаты в зависимости от значения** - из-за кеша `Integer` в диапазоне -128..127. Сравнивайте только через `==`. -> -> **`list.contains` в цикле - O(n²).** Самая частая проблема производительности: -> ```kotlin -> fun main() { -> val a = List(2000) { it } -> val b = List(2000) { it * 2 } -> println(a.count { it in b }) // 4 млн сравнений -> val bSet = b.toSet() -> println(a.count { it in bSet }) // 2000 операций, тот же результат -> } -> ``` -> -> **Каждая операция создаёт коллекцию.** Цепочка из пяти операций по большому списку - пять списков в памяти одновременно. Для больших данных нужен `Sequence`. -> -> **`ArrayList` без начальной ёмкости перевыделяет массив.** При известном размере задавайте его: `ArrayList(n)` или `buildList(n) { }`. -> -> **`subList` держит ссылку на исходный список** - он не будет собран сборщиком мусора, даже если нужен только маленький срез большого списка. Для среза, который переживёт оригинал, используйте `slice` или `toList`. -> -> **Коллекции Kotlin не потокобезопасны.** `mutableListOf()` - обычный `ArrayList`. Одновременное изменение из двух потоков повреждает структуру или бросает `ConcurrentModificationException`. -> -> **Изменяемый ключ ломает `HashMap`.** Запись становится недостижимой, хотя `size` её считает. Ключи должны быть неизменяемыми - `data class` с `val`-полями. Подробнее: [equals, hashCode и toString](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md) -> -> **Измерения производительности на JVM без прогрева врут.** Пример с `System.nanoTime` выше показывает порядок величины, но JIT компилирует горячий код только после тысяч вызовов. Для настоящих измерений нужен JMH. - -Далее: [Параметрический полиморфизм](../Дженерики/Параметрический%20полиморфизм.md) diff --git a/Kotlin/Коллекции/Коллекции.md b/Kotlin/Коллекции/Коллекции.md deleted file mode 100644 index 3c324bf..0000000 --- a/Kotlin/Коллекции/Коллекции.md +++ /dev/null @@ -1,234 +0,0 @@ ---- -title: Коллекции -description: Обзор иерархии коллекций Kotlin, разделение на читаемые и изменяемые интерфейсы -tags: - - kotlin - - collections ---- - -Коллекции Kotlin - это **те же** классы JDK (`ArrayList`, `HashMap`, `LinkedHashSet`), но с другими интерфейсами поверх них. Главное отличие от Java: изменяемость выражена в системе типов. - -```kotlin -fun main() { - val readOnly: List = listOf(1, 2, 3) - val mutable: MutableList = mutableListOf(1, 2, 3) - - mutable.add(4) - // readOnly.add(4) // ошибка: у List нет метода add - - println("$readOnly / $mutable") // [1, 2, 3] / [1, 2, 3, 4] - println(readOnly.javaClass.name) // java.util.Arrays$ArrayList - println(mutable.javaClass.name) // java.util.ArrayList -} -``` - -## Иерархия -```mermaid -graph TD - I["Iterable<out T>"] --> C["Collection<out T>"] - C --> L["List<out T>"] - C --> S["Set<out T>"] - I --> MI["MutableIterable<T>"] - MI --> MC["MutableCollection<T>"] - C --> MC - MC --> ML["MutableList<T>"] - L --> ML - MC --> MS["MutableSet<T>"] - S --> MS - M["Map<K, out V>"] --> MM["MutableMap<K, V>"] -``` - -Два важных факта из этой схемы: -- `MutableList` **наследует** `List`, то есть изменяемую коллекцию можно передать туда, где ожидается читаемая. Обратное невозможно. -- `Map` не входит в `Collection` - это отдельная иерархия, как и в Java. - -| Интерфейс | Чтение | Изменение | Реализация по умолчанию | -|---|---|---|---| -| `List` | да | нет | `ArrayList` под капотом | -| `MutableList` | да | да | `ArrayList` | -| `Set` | да | нет | `LinkedHashSet` | -| `MutableSet` | да | да | `LinkedHashSet` | -| `Map` | да | нет | `LinkedHashMap` | -| `MutableMap` | да | да | `LinkedHashMap` | - -Обратите внимание: умолчание для `Set` и `Map` - **`Linked`**-версии, то есть порядок вставки сохраняется. В Java `new HashSet<>()` порядок не гарантирует. - -## Создание -```kotlin -fun main() { - val a = listOf(1, 2, 3) // read-only - val b = mutableListOf(1, 2, 3) // изменяемый - val c = emptyList() // пустой - val d = listOfNotNull(1, null, 3) // null отфильтрованы - val e = List(3) { it * it } // по индексу - - println("$a $b $c $d $e") // [1, 2, 3] [1, 2, 3] [] [1, 3] [0, 1, 4] - - val s = setOf(1, 2, 2, 3) // дубликаты убраны - val m = mapOf("a" to 1, "b" to 2) - println("$s $m") // [1, 2, 3] {a=1, b=2} -} -``` - -Специализированные варианты для разных реализаций: -```kotlin -fun main() { - val sorted = sortedSetOf(3, 1, 2) // TreeSet - val sortedMap = sortedMapOf("b" to 2, "a" to 1) - val linked = linkedSetOf(3, 1, 2) // LinkedHashSet - val hash = hashSetOf(3, 1, 2) // HashSet, порядок не гарантирован - val arrayDeque = ArrayDeque(listOf(1, 2, 3)) - - println("$sorted $sortedMap $linked") // [1, 2, 3] {a=1, b=2} [3, 1, 2] - println(hash.size) // 3 - arrayDeque.addFirst(0) - println(arrayDeque) // [0, 1, 2, 3] -} -``` - -## Билдеры -Когда коллекцию нужно собрать по условию, а результат отдать как read-only: -```kotlin -fun main() { - val list = buildList { - add(1) - if (true) add(2) - addAll(listOf(3, 4)) - } - println(list) // [1, 2, 3, 4] - - val map = buildMap { - put("a", 1) - putAll(mapOf("b" to 2)) - } - println(map) // {a=1, b=2} - - val set = buildSet { repeat(3) { add(it % 2) } } - println(set) // [0, 1] -} -``` -Внутри лямбды коллекция изменяемая, снаружи возвращается `List`. Это чище, чем `mutableListOf` с последующим приведением. - -## Пустая коллекция и null -```kotlin -fun main() { - val nullable: List? = null - println(nullable?.size ?: 0) // 0 - println(nullable.orEmpty()) // [] - println(nullable.isNullOrEmpty()) // true - - val empty = emptyList() - println(empty.firstOrNull()) // null - println(empty.sum()) // 0 -} -``` -Практическое правило: в API возвращайте пустую коллекцию, а не `null`. `List?` заставляет каждого клиента писать `?.` или `orEmpty()`, не давая взамен никакой информации - пустой список уже означает «ничего нет». - -## Массив не коллекция -```kotlin -fun main() { - val array = arrayOf(1, 2, 3) - // array.filter — есть, потому что Array получает расширения из Iterable - println(array.filter { it > 1 }) // [2, 3] — это уже List - - println(array.toList()::class.simpleName) // ArrayList - println(listOf(1, 2).toTypedArray().size) // 2 -} -``` -`Array` - отдельный тип, не входящий в `Collection`. У него фиксированный размер и инвариантность. Подробнее: [Массивы](../Синтаксис%20и%20типы/Работа%20с%20данными/Массивы.md) - -## Карта раздела -| Заметка | О чём | -|---|---| -| [List, Set, Map](List,%20Set,%20Map.md) | три семейства, их гарантии и выбор между ними | -| [Изменяемость](Изменяемость.md) | read-only против immutable, защита от изменения | -| [Операции](Операции.md) | `map`, `filter`, `groupBy`, `fold` и остальной арсенал | -| [Sequence](Sequence.md) | ленивые вычисления и когда они выигрывают | -| [Коллекции под капотом](Коллекции%20под%20капотом.md) | что лежит в байт-коде, сложность операций, память | - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `new ArrayList<>()` | `mutableListOf()` | -> | `List.of(1, 2)` (Java 9+) | `listOf(1, 2)` | -> | `Collections.emptyList()` | `emptyList()` | -> | изменяемость не в типе | `List` против `MutableList` | -> | `UnsupportedOperationException` в рантайме | ошибка компиляции | -> | `Stream` для операций | расширения прямо на коллекции | -> | `new HashSet<>()` - порядок не гарантирован | `setOf()` - порядок вставки | -> -> **Разделение на `List` и `MutableList` - главное нововведение.** В Java `List` - один интерфейс с методом `add`, и `List.of(1, 2)` его тоже имеет: вызов просто бросает `UnsupportedOperationException` в рантайме. -> ```java -> List list = List.of(1, 2); -> list.add(3); // компилируется, падает в рантайме -> ``` -> ```kotlin -> fun main() { -> val list = listOf(1, 2) -> // list.add(3) // не компилируется -> println(list.size) -> } -> ``` -> Ошибка переехала из рантайма в компиляцию - это главная практическая польза. -> -> **Операции не требуют `Stream`.** В Java цепочка выглядит как `list.stream().filter(...).map(...).collect(Collectors.toList())`. В Kotlin `filter` и `map` - функции-расширения прямо на `Iterable`, они сразу возвращают `List`: -> ```kotlin -> fun main() { -> println(listOf(1, 2, 3).filter { it > 1 }.map { it * 10 }) // [20, 30] -> } -> ``` -> Цена - промежуточный список на каждом шаге. Ленивый аналог `Stream` - это [Sequence](Sequence.md). -> -> **`setOf` и `mapOf` сохраняют порядок вставки**, потому что под ними `LinkedHashSet` и `LinkedHashMap`. Это удобно (вывод предсказуем), но на порядок всё равно не стоит полагаться в контракте: тип объявлен как `Set`, а не как «упорядоченное множество». -> -> Сравнить: [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md) - -> [!warning] Подводные камни -> **`List` - это read-only, а не immutable.** Объект под ним может меняться через другую ссылку: -> ```kotlin -> fun main() { -> val mutable = mutableListOf(1, 2) -> val readOnly: List = mutable // та же коллекция! -> mutable.add(3) -> println(readOnly) // [1, 2, 3] — изменился -> } -> ``` -> `List` гарантирует только то, что **вы** через эту ссылку не измените коллекцию. Подробнее: [Изменяемость](Изменяемость.md) -> -> **Из Java любой `List` изменяем.** Типы Kotlin-коллекций - это `java.util.List`, поэтому Java-код про read-only не знает: -> ```kotlin -> fun giveList(): List = mutableListOf(1, 2) -> -> fun main() { println(giveList().size) } // 2 -> ``` -> Java-клиент вызовет `add` без всяких ошибок компиляции, и если под ссылкой `ArrayList` - элемент добавится. Для настоящей защиты нужна копия. -> -> **`listOf()` возвращает разные реализации.** `listOf()` без аргументов - `EmptyList`, `listOf(x)` - `SingletonList`, с несколькими - `Arrays$ArrayList`. То же у `mapOf` и `setOf`: на одном элементе это `SingletonMap`/`SingletonSet`, а не `Linked`-версия. Последний не поддерживает `add` даже через каст: -> ```kotlin -> fun main() { -> val list = listOf(1, 2) -> try { -> @Suppress("UNCHECKED_CAST") -> (list as MutableList).add(3) -> } catch (e: UnsupportedOperationException) { -> println("каст скомпилировался, но add упал") -> } -> } -> ``` -> Приводить `List` к `MutableList` нельзя: иногда это сработает, иногда бросит исключение. -> -> **`Map` не `Collection`** - у него нет `size` через `Collection`-интерфейс, `filter` работает с `Map.Entry`, а `map` возвращает `List`: -> ```kotlin -> fun main() { -> val m = mapOf("a" to 1, "b" to 2) -> println(m.filter { it.value > 1 }) // {b=2} — остался Map -> println(m.map { it.key }) // [a, b] — стал List -> println(m.mapValues { it.value * 10 }) // {a=10, b=20} -> } -> ``` -> -> **Операции создают новую коллекцию на каждом шаге.** Цепочка из пяти `map`/`filter` по списку в миллион элементов создаст пять списков. Для длинных цепочек и больших данных нужен [Sequence](Sequence.md). -> -> **`hashSetOf` и `hashMapOf` не гарантируют порядок.** Если тест полагается на порядок обхода, он может сломаться при смене JDK. Используйте `setOf`/`mapOf` или сортируйте явно. - -Далее: [List, Set, Map](List,%20Set,%20Map.md) diff --git a/Kotlin/Коллекции/Операции.md b/Kotlin/Коллекции/Операции.md deleted file mode 100644 index 46c1465..0000000 --- a/Kotlin/Коллекции/Операции.md +++ /dev/null @@ -1,424 +0,0 @@ ---- -title: Операции -description: map, filter, groupBy, fold, zip и остальной арсенал операций над коллекциями -tags: - - kotlin - - collections ---- - -Операции над коллекциями - функции-расширения на `Iterable`. Их около 150; здесь - те, что покрывают почти всю практику. - -Все они делятся на две группы: -- **возвращают коллекцию**: `map`, `filter`, `sorted` - можно строить цепочки -- **возвращают одно значение**: `sum`, `first`, `count`, `fold` - завершают цепочку - -## Преобразование -```kotlin -fun main() { - val list = listOf(1, 2, 3) - - println(list.map { it * 2 }) // [2, 4, 6] - println(list.mapIndexed { i, x -> "$i:$x" }) // [0:1, 1:2, 2:3] - println(listOf("1", "a", "3").mapNotNull { it.toIntOrNull() }) // [1, 3] - println(list.flatMap { listOf(it, it * 10) }) // [1, 10, 2, 20, 3, 30] - println(listOf(listOf(1, 2), listOf(3)).flatten()) // [1, 2, 3] - println(list.zip(listOf("a", "b"))) // [(1, a), (2, b)] - println(list.withIndex().map { "${it.index}${it.value}" }) // [01, 12, 23] -} -``` -`mapNotNull` - самая полезная из них: преобразование с отбрасыванием неудач в одном шаге, вместо `map` + `filterNotNull`. - -`flatMap` нужен, когда преобразование даёт коллекцию, а результат нужен плоским: -```kotlin -data class Order5(val id: Int, val items: List) - -fun main() { - val orders = listOf( - Order5(1, listOf("хлеб", "молоко")), - Order5(2, listOf("сыр")), - ) - println(orders.flatMap { it.items }) // [хлеб, молоко, сыр] - println(orders.map { it.items }) // [[хлеб, молоко], [сыр]] -} -``` - -## Фильтрация -```kotlin -fun main() { - val list = listOf(1, 2, 3, 4, 5) - - println(list.filter { it % 2 == 0 }) // [2, 4] - println(list.filterNot { it % 2 == 0 }) // [1, 3, 5] - println(list.filterIndexed { i, _ -> i > 2 }) // [4, 5] - println(listOf(1, null, 3).filterNotNull()) // [1, 3] - println(listOf(1, "a", 2).filterIsInstance()) // [1, 2] - println(list.take(2)) // [1, 2] - println(list.takeWhile { it < 3 }) // [1, 2] - println(list.dropWhile { it < 3 }) // [3, 4, 5] - println(list.distinct()) // [1, 2, 3, 4, 5] - println(listOf("aa", "ab", "ba").distinctBy { it.first() }) // [aa, ba] -} -``` - -`partition` делит на две коллекции за один проход: -```kotlin -fun main() { - val (even, odd) = listOf(1, 2, 3, 4).partition { it % 2 == 0 } - println("$even $odd") // [2, 4] [1, 3] -} -``` - -## Поиск и проверки -```kotlin -fun main() { - val list = listOf(1, 2, 3, 4) - - println(list.first { it > 2 }) // 3, бросает если нет - println(list.firstOrNull { it > 10 }) // null - println(list.find { it > 2 }) // 3 — то же, что firstOrNull - println(list.last()) // 4 - println(list.single { it == 2 }) // 2, бросает если не ровно один - println(list.singleOrNull { it > 2 }) // null — их два - - println(list.any { it > 3 }) // true - println(list.all { it > 0 }) // true - println(list.none { it > 10 }) // true - println(list.count { it % 2 == 0 }) // 2 - println(3 in list) // true - println(list.indexOfFirst { it > 2 }) // 2 -} -``` - -Поведение на пустой коллекции - источник ошибок: -```kotlin -fun main() { - val empty = emptyList() - - println(empty.any { it > 0 }) // false - println(empty.all { it > 0 }) // true — вакуумная истина! - println(empty.none { it > 0 }) // true - println(empty.sum()) // 0 - println(empty.firstOrNull()) // null - println(empty.maxOrNull()) // null -} -``` -`all` на пустой коллекции возвращает `true` - это математически корректно, но в проверках вида «все элементы валидны» часто неожиданно. - -## Агрегация -```kotlin -fun main() { - val list = listOf(1, 2, 3, 4) - - println(list.sum()) // 10 - println(list.average()) // 2.5 - println(list.maxOrNull()) // 4 - println(list.minOrNull()) // 1 - println(list.sumOf { it * 10 }) // 100 - println(list.maxByOrNull { -it }) // 1 - println(list.maxOfOrNull { it * 2 }) // 8 - println(list.joinToString(prefix = "[", postfix = "]")) // [1, 2, 3, 4] - println(list.joinToString(limit = 2, truncated = "…")) // 1, 2, … -} -``` - -`maxBy` против `maxOf` - частая путаница: -```kotlin -data class User8(val name: String, val age: Int) - -fun main() { - val users = listOf(User8("Иван", 30), User8("Пётр", 25)) - - println(users.maxByOrNull { it.age }) // User8(name=Иван, age=30) — элемент - println(users.maxOfOrNull { it.age }) // 30 — значение -} -``` - -## fold и reduce -```kotlin -fun main() { - val list = listOf(1, 2, 3, 4) - - println(list.fold(0) { acc, x -> acc + x }) // 10 - println(list.fold(1) { acc, x -> acc * x }) // 24 - println(list.reduce { acc, x -> acc + x }) // 10 - println(list.runningFold(0) { acc, x -> acc + x }) // [0, 1, 3, 6, 10] - println(list.foldRight(0) { x, acc -> acc * 10 + x }) // 4321 — справа налево - - println(emptyList().fold(0) { a, b -> a + b }) // 0 - try { - emptyList().reduce { a, b -> a + b } - } catch (e: UnsupportedOperationException) { - println("reduce падает на пустой коллекции") - } -} -``` - -| | `fold` | `reduce` | -|---|---|---| -| начальное значение | задаёте вы | первый элемент | -| тип результата | любой | тип элемента | -| пустая коллекция | вернёт начальное | исключение | - -`fold` умеет менять тип, и это его главное применение: -```kotlin -fun main() { - val words = listOf("раз", "два", "три") - - val lengths: Map = words.fold(mutableMapOf()) { acc, w -> - acc[w] = w.length - acc - } - println(lengths) // {раз=3, два=3, три=3} - - println(words.fold(0) { acc, w -> acc + w.length }) // 9 — List -> Int -} -``` - -## Группировка -```kotlin -fun main() { - val words = listOf("яблоко", "арбуз", "банан", "боб") - - println(words.groupBy { it.first() }) - // {я=[яблоко], а=[арбуз], б=[банан, боб]} - - println(words.groupBy(keySelector = { it.first() }, valueTransform = { it.length })) - // {я=[6], а=[5], б=[5, 3]} - - println(words.groupingBy { it.first() }.eachCount()) - // {я=1, а=1, б=2} - - println(words.groupingBy { it.first() }.fold(0) { acc, w -> acc + w.length }) - // {я=6, а=5, б=8} -} -``` -`groupingBy` отличается от `groupBy` тем, что не создаёт промежуточные списки - он сразу агрегирует. Для подсчёта и суммирования по группам это и быстрее, и короче. - -## associate -```kotlin -fun main() { - val users = listOf("Иван", "Пётр", "Анна") - - println(users.associateWith { it.length }) // {Иван=4, Пётр=4, Анна=4} - println(users.associateBy { it.first() }) // {И=Иван, П=Пётр, А=Анна} - println(users.associate { it.first() to it.length }) // {И=4, П=4, А=4} -} -``` - -| Функция | Ключ | Значение | -|---|---|---| -| `associateWith` | элемент | результат лямбды | -| `associateBy` | результат лямбды | элемент | -| `associate` | из `Pair` | из `Pair` | - -`associateBy` против `groupBy` - важная разница: -```kotlin -fun main() { - val words = listOf("банан", "боб") - - println(words.associateBy { it.first() }) // {б=боб} — потеря! - println(words.groupBy { it.first() }) // {б=[банан, боб]} -} -``` -`associateBy` при дубликате ключа **молча перезаписывает**. Если ключ не уникален, нужен `groupBy`. - -## Сортировка -```kotlin -fun main() { - val list = listOf(3, 1, 2) - - println(list.sorted()) // [1, 2, 3] - println(list.sortedDescending()) // [3, 2, 1] - println(list.reversed()) // [2, 1, 3] — не сортировка! - println(list.shuffled().size) // 3 - - val users = listOf("Пётр" to 25, "Иван" to 30, "Анна" to 25) - println(users.sortedBy { it.second }) // [(Пётр, 25), (Анна, 25), (Иван, 30)] - println(users.sortedWith(compareBy({ it.second }, { it.first }))) - // [(Анна, 25), (Пётр, 25), (Иван, 30)] — по возрасту, потом по имени - println(users.sortedWith(compareByDescending> { it.second } - .thenBy { it.first })) - // [(Иван, 30), (Анна, 25), (Пётр, 25)] -} -``` -Сортировка Kotlin **стабильна**: элементы с равными ключами сохраняют исходный порядок. - -На `MutableList` есть сортировка на месте - без `ed` в имени: -```kotlin -fun main() { - val list = mutableListOf(3, 1, 2) - list.sort() // на месте - println(list) // [1, 2, 3] - list.sortByDescending { it } - println(list) // [3, 2, 1] - - val copy = list.sorted() // новая коллекция - println("$list / $copy") // [3, 2, 1] / [1, 2, 3] -} -``` -Соглашение в stdlib: `sort()` меняет на месте, `sorted()` возвращает копию. То же с `reverse()`/`reversed()`, `shuffle()`/`shuffled()`. - -## Разбиение и объединение -```kotlin -fun main() { - val list = listOf(1, 2, 3, 4, 5) - - println(list.chunked(2)) // [[1, 2], [3, 4], [5]] - println(list.chunked(2) { it.sum() }) // [3, 7, 5] - println(list.windowed(3)) // [[1,2,3], [2,3,4], [3,4,5]] - println(list.windowed(2, step = 2, partialWindows = true)) // [[1,2], [3,4], [5]] - println(list.zipWithNext()) // [(1,2), (2,3), (3,4), (4,5)] - println(list.zipWithNext { a, b -> b - a }) // [1, 1, 1, 1] -} -``` -`windowed` - скользящее окно: удобно для расчёта разниц, средних и сглаживания. - -## Операции на Map -```kotlin -fun main() { - val map = mapOf("a" to 1, "b" to 2, "c" to 3) - - println(map.filter { (_, v) -> v > 1 }) // {b=2, c=3} - println(map.mapValues { it.value * 10 }) // {a=10, b=20, c=30} - println(map.mapKeys { it.key.uppercase() }) // {A=1, B=2, C=3} - println(map.filterKeys { it != "a" }) // {b=2, c=3} - println(map.maxByOrNull { it.value }?.key) // c - println(map.entries.sumOf { it.value }) // 6 - println(map.toList().map { (k, v) -> "$k$v" }) // [a1, b2, c3] -} -``` -Деструктуризация в лямбде (`{ (k, v) -> }`) работает для `Map.Entry` и для `Pair`. - -## Практический пример -```kotlin -data class Sale(val region: String, val product: String, val amount: Long) - -fun main() { - val sales = listOf( - Sale("Север", "хлеб", 100), - Sale("Север", "молоко", 200), - Sale("Юг", "хлеб", 150), - Sale("Юг", "сыр", 300), - ) - - val byRegion = sales.groupBy { it.region }.mapValues { (_, v) -> v.sumOf { it.amount } } - println(byRegion) // {Север=300, Юг=450} - - val top = sales.maxByOrNull { it.amount } - println("${top?.product}: ${top?.amount}") // сыр: 300 - - val products = sales.map { it.product }.distinct().sorted() - println(products) // [молоко, сыр, хлеб] - - val report = byRegion.entries - .sortedByDescending { it.value } - .joinToString("\n") { "${it.key}: ${it.value}" } - println(report) // Юг: 450\nСевер: 300 -} -``` - -> [!info] Отличия от Java -> | Java Stream | Kotlin | -> |---|---| -> | `stream().map(f).collect(toList())` | `map(f)` | -> | `stream().filter(p).collect(toList())` | `filter(p)` | -> | `collect(groupingBy(f))` | `groupBy(f)` | -> | `collect(toMap(k, v))` | `associate { k to v }` | -> | `reduce(0, Integer::sum)` | `fold(0) { a, b -> a + b }` | -> | `mapToInt(f).sum()` | `sumOf(f)` | -> | `flatMap(x -> x.stream())` | `flatMap { it }` | -> | `collect(partitioningBy(p))` | `partition(p)` | -> | `anyMatch(p)` | `any(p)` | -> | нет аналога | `mapNotNull`, `windowed`, `zipWithNext`, `groupingBy` | -> -> **Главное - не нужен `.stream()` и `.collect()`.** В Java любая операция требует входа в стрим и выхода из него; это ~25 символов обвязки на каждую цепочку. В Kotlin операции объявлены прямо на `Iterable`, поэтому короткие цепочки читаются как одно выражение: -> ```kotlin -> fun main() { -> println(listOf(1, 2, 3).filter { it > 1 }.sumOf { it * 10 }) // 50 -> } -> ``` -> ```java -> List.of(1, 2, 3).stream().filter(x -> x > 1).mapToInt(x -> x * 10).sum(); -> ``` -> -> **Цена - энергичность.** Java-стрим ленив: промежуточные операции не создают коллекций. Kotlin-операции на `Iterable` создают новый список на каждом шаге. Для цепочки из 3 операций по списку из 1000 элементов это 3 списка. Ленивый аналог - [Sequence](Sequence.md), и он почти дословно соответствует `Stream`. -> -> **`mapNotNull` и `groupingBy` аналогов в Java не имеют.** В Java фильтрация с преобразованием - это `map` + `filter(Objects::nonNull)` или `flatMap` с `Optional.stream()`. Подсчёт по группам - `groupingBy(f, counting())`, то есть вложенные коллекторы, которые плохо читаются. -> -> **`Optional` в цепочках не нужен.** Java-стримы возвращают `Optional` из `findFirst` и `max`; в Kotlin это nullable-тип, с которым работают обычные `?.` и `?:`: -> ```kotlin -> fun main() { -> println(listOf(1, 2, 3).maxOrNull() ?: 0) // 3 -> } -> ``` -> -> Сравнить: [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md) - -> [!warning] Подводные камни -> **Каждая операция создаёт новую коллекцию:** -> ```kotlin -> fun main() { -> val big = List(100_000) { it } -> val result = big.map { it * 2 }.filter { it % 3 == 0 }.take(5) -> println(result) // [0, 6, 12, 18, 24] -> } -> ``` -> `map` обработал все 100 000 элементов и создал список, `filter` - ещё один, и только потом `take` взял пять. С `asSequence()` работы было бы на порядки меньше. -> -> **`all` на пустой коллекции - `true`:** -> ```kotlin -> fun main() { -> val errors = emptyList() -> println(errors.all { it.isNotBlank() }) // true -> println(errors.isNotEmpty() && errors.all { it.isNotBlank() }) // false -> } -> ``` -> Проверка «все данные валидны» на пустом входе пройдёт. Если пустота недопустима, проверяйте её отдельно. -> -> **`associateBy` теряет дубликаты молча** - при повторе ключа остаётся последний элемент. Для неуникальных ключей нужен `groupBy`. -> -> **`reduce` падает на пустой коллекции**, `fold` - нет. В коде, где коллекция может быть пустой, `reduce` - потенциальное исключение. -> -> **`sortedBy` с nullable-ключом ставит `null` первыми:** -> ```kotlin -> fun main() { -> val list = listOf("b" to 2, "a" to null, "c" to 1) -> println(list.sortedBy { it.second }) // [(a, null), (c, 1), (b, 2)] -> println(list.sortedWith(compareBy(nullsLast()) { it.second })) // [(c, 1), (b, 2), (a, null)] -> } -> ``` -> -> **`reversed()` - не сортировка**, а переворот. `listOf(3, 1, 2).reversed()` даёт `[2, 1, 3]`. -> -> **`sort()` и `sorted()` легко перепутать.** `sort()` молча меняет исходную коллекцию - если она пришла извне, это неожиданная мутация: -> ```kotlin -> fun process(items: MutableList): List { -> items.sort() // испортили аргумент вызывающего -> return items -> } -> -> fun main() { -> val original = mutableListOf(3, 1, 2) -> process(original) -> println(original) // [1, 2, 3] — вызывающий этого не просил -> } -> ``` -> Принимайте `List` вместо `MutableList` - тогда `sort()` просто не скомпилируется. -> -> **`first()` и `single()` бросают, а не возвращают `null`.** Версии с `OrNull` безопасны; `single` вдобавок падает, если элементов больше одного. -> -> **`flatMap { it }` на `List>` работает, но `flatten()` яснее.** -> -> **Цепочка из `filter` после `map` делает лишнюю работу.** `filter` перед `map` обрабатывает меньше элементов: -> ```kotlin -> fun main() { -> val list = List(10) { it } -> println(list.map { it * 2 }.filter { it > 10 }) // [12, 14, 16, 18], 10 преобразований -> println(list.filter { it > 5 }.map { it * 2 }) // [12, 14, 16, 18], 4 преобразования -> } -> ``` -> Здесь результаты совпадают, а работы во втором случае вдвое с лишним меньше. Но совпадают они не всегда: перестановка `filter` и `map` меняет смысл, если предикат проверяет уже преобразованное значение. Сначала убедитесь в эквивалентности, потом выбирайте по стоимости. -> -> **`joinToString` по умолчанию без ограничения длины.** Для списка на миллион элементов он построит огромную строку; в логах используйте `limit`. - -Далее: [Sequence](Sequence.md) diff --git a/Kotlin/Корутины/Channels.md b/Kotlin/Корутины/Channels.md deleted file mode 100644 index 46a7b1b..0000000 --- a/Kotlin/Корутины/Channels.md +++ /dev/null @@ -1,545 +0,0 @@ ---- -title: Channels -description: Передача значений между корутинами: типы буферов, закрытие, fan-out и fan-in, select и когда канал лучше Flow -tags: - - kotlin - - coroutines - - channels ---- - -`Channel` - очередь для передачи значений между корутинами. Если `Flow` описывает, как значения производятся, то `Channel` - это объект, в который одна корутина кладёт значения, а другая забирает. - -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* - -fun main() = runBlocking { - val channel = Channel() - - launch { - for (i in 1..3) { - channel.send(i) // приостановится, пока никто не принял - } - channel.close() - } - - for (value in channel) { // завершится при close() - println("получено: $value") - } - // получено: 1 - // получено: 2 - // получено: 3 -} -``` -`send` и `receive` - `suspend`-функции: отправитель ждёт получателя, получатель ждёт отправителя. Это rendezvous-канал, вариант по умолчанию. - -## Типы каналов -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* - -fun main() = runBlocking { - // RENDEZVOUS (по умолчанию): буфера нет - val rendezvous = Channel() - println("rendezvous принял без получателя: ${rendezvous.trySend(1).isSuccess}") - - // BUFFERED: буфер на n элементов - val buffered = Channel(capacity = 2) - println("в буфер на 2: ${buffered.trySend(1).isSuccess} ${buffered.trySend(2).isSuccess}") - println("третий не влез: ${buffered.trySend(3).isSuccess}") - - // CONFLATED: хранит только последнее - val conflated = Channel(Channel.CONFLATED) - conflated.trySend(1); conflated.trySend(2); conflated.trySend(3) - println("conflated хранит последнее: ${conflated.receive()}") - - // UNLIMITED: растёт без ограничений - val unlimited = Channel(Channel.UNLIMITED) - repeat(1000) { unlimited.trySend(it) } - println("unlimited принял всё: ${unlimited.receive()}") - // rendezvous принял без получателя: false - // в буфер на 2: true true - // третий не влез: false - // conflated хранит последнее: 3 - // unlimited принял всё: 0 -} -``` -| Тип | Поведение `send` при заполнении | -|---|---| -| `RENDEZVOUS` (0) | ждёт, пока кто-то вызовет `receive` | -| `BUFFERED` (по умолчанию 64) | ждёт освобождения места | -| `CONFLATED` | не ждёт, заменяет непрочитанное значение | -| `UNLIMITED` | не ждёт никогда, растёт в памяти | -| `capacity = n` + `onBufferOverflow` | `DROP_OLDEST` / `DROP_LATEST` вместо ожидания | - -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* - -fun main() = runBlocking { - val dropping = Channel(capacity = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST) - repeat(5) { dropping.trySend(it) } - println("осталось последние два: ${dropping.receive()} ${dropping.receive()}") - // осталось последние два: 3 4 -} -``` - -## Закрытие канала -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* - -fun main() = runBlocking { - val channel = Channel(capacity = 3) - channel.send(1) - channel.send(2) - channel.close() - - println("после close ещё можно читать: ${channel.receive()} ${channel.receive()}") - println("отправка запрещена: ${channel.trySend(3).isSuccess}") - println("закрыт для отправки: ${channel.isClosedForSend}") - - val error = runCatching { channel.receive() }.exceptionOrNull() - println("чтение из пустого закрытого: ${error is ClosedReceiveChannelException}") - // после close ещё можно читать: 1 2 - // отправка запрещена: false - // закрыт для отправки: true - // чтение из пустого закрытого: true -} -``` -`close()` - это «больше значений не будет»: уже отправленные элементы дочитываются, новые не принимаются. `for`-цикл по каналу завершается нормально, а `receive()` на исчерпанном канале бросает исключение. Безопасная альтернатива - `receiveCatching`: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* - -fun main() = runBlocking { - val channel = Channel() - channel.close() - - val result = channel.receiveCatching() - println("успех: ${result.isSuccess}, закрыт: ${result.isClosed}") - println("значение или null: ${result.getOrNull()}") - // успех: false, закрыт: true - // значение или null: null -} -``` - -Закрытие с ошибкой передаёт её получателю: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* - -fun main() = runBlocking { - val channel = Channel() - launch { - channel.send(1) - channel.close(IllegalStateException("источник сломался")) - } - - val error = runCatching { - for (v in channel) println("получено: $v") - }.exceptionOrNull() - println("ошибка дошла: ${error?.message}") - // получено: 1 - // ошибка дошла: источник сломался -} -``` - -## produce: канал как результат корутины -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* - -fun CoroutineScope.produceSquares(n: Int): ReceiveChannel = produce { - for (i in 1..n) { - delay(10) - send(i * i) - } -} - -fun main() = runBlocking { - val squares = produceSquares(4) - println(squares.toList()) // [1, 4, 9, 16] -} -``` -`produce` закрывает канал автоматически при завершении тела - это его главное преимущество перед ручным `Channel()` + `launch`. Он помечен `@ObsoleteCoroutinesApi`, но остаётся самым аккуратным способом создать канал-источник. - -## Fan-out: несколько получателей -Канал распределяет элементы между получателями - каждый элемент достаётся **ровно одному**: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* -import java.util.concurrent.ConcurrentHashMap - -fun main() = runBlocking { - val tasks = Channel() - val handled = ConcurrentHashMap() - - repeat(3) { worker -> - launch { - for (task in tasks) { - delay(10) - handled[task] = worker - } - } - } - - repeat(9) { tasks.send(it) } - tasks.close() - delay(100) - - println("обработано задач: ${handled.size}") - println("каждая ровно раз: ${handled.keys.sorted() == (0..8).toList()}") - println("работали несколько воркеров: ${handled.values.toSet().size > 1}") - // обработано задач: 9 - // каждая ровно раз: true - // работали несколько воркеров: true -} -``` -Это главное отличие канала от `SharedFlow`: канал **распределяет**, `SharedFlow` **транслирует** всем. Пул воркеров без канала не сделать. - -## Fan-in: несколько отправителей -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* - -fun main() = runBlocking { - val results = Channel() - - launch { delay(10); results.send("от первого") } - launch { delay(20); results.send("от второго") } - launch { delay(30); results.send("от третьего") } - - repeat(3) { println(results.receive()) } - results.close() - // от первого - // от второго - // от третьего -} -``` - -## Конвейер -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* - -fun CoroutineScope.numbers(): ReceiveChannel = produce { - var i = 1 - while (i <= 5) send(i++) -} - -fun CoroutineScope.squares(input: ReceiveChannel): ReceiveChannel = produce { - for (n in input) send(n * n) -} - -fun CoroutineScope.evens(input: ReceiveChannel): ReceiveChannel = produce { - for (n in input) if (n % 2 == 0) send(n) -} - -fun main() = runBlocking { - val pipeline = evens(squares(numbers())) - println(pipeline.toList()) // [4, 16] - coroutineContext.cancelChildren() -} -``` -Каждая стадия - отдельная корутина, работающая параллельно с остальными. Для такой задачи обычно достаточно `Flow` с операторами, но каналы дают контроль над буферами каждой стадии. - -```mermaid -graph LR - A["produce: 1..5"] -->|"Channel"| B["squares"] - B -->|"Channel"| C["evens"] - C -->|"Channel"| D["collect"] - E["Каждая стадия —
своя корутина"] -.-> B -``` - -## Тикер и таймеры -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* - -fun main() = runBlocking { - val ticks = mutableListOf() - val job = launch { - var i = 0 - while (true) { - delay(20) - ticks += i++ - } - } - delay(70) - job.cancel() - println("тиков примерно 3: ${ticks.size in 2..4}") // тиков примерно 3: true -} -``` -Для периодических задач проще цикл с `delay`, чем `ticker` (который помечен устаревшим). Если нужен именно `Flow`, его даёт `flow { while (true) { delay(x); emit(Unit) } }`. - -## select: ждать первое из нескольких -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* -import kotlinx.coroutines.selects.select - -fun main() = runBlocking { - val fast = Channel() - val slow = Channel() - - launch { delay(10); fast.send("быстрый") } - launch { delay(50); slow.send("медленный") } - - val first = select { - fast.onReceive { "пришло из fast: $it" } - slow.onReceive { "пришло из slow: $it" } - } - println(first) - coroutineContext.cancelChildren() - // пришло из fast: быстрый -} -``` -`select` работает и с `Deferred` - это способ устроить гонку запросов: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.selects.select - -fun main() = runBlocking { - val mirror1 = async { delay(50); "зеркало 1" } - val mirror2 = async { delay(10); "зеркало 2" } - - val winner = select { - mirror1.onAwait { it } - mirror2.onAwait { it } - } - println("ответил первым: $winner") - coroutineContext.cancelChildren() - // ответил первым: зеркало 2 -} -``` -Важно: проигравший `Deferred` продолжает работу - его нужно отменить явно (здесь это делает `cancelChildren`). - -## Channel как мьютекс для состояния -Канал-«актор» - способ изолировать изменяемое состояние в одной корутине: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* - -sealed interface Command { - data class Increment(val by: Int) : Command - data class Read(val reply: CompletableDeferred) : Command -} - -fun main() = runBlocking { - val commands = Channel() - - launch { - var state = 0 // доступно только этой корутине - for (cmd in commands) { - when (cmd) { - is Command.Increment -> state += cmd.by - is Command.Read -> cmd.reply.complete(state) - } - } - } - - repeat(100) { commands.send(Command.Increment(1)) } - val reply = CompletableDeferred() - commands.send(Command.Read(reply)) - println("состояние без блокировок: ${reply.await()}") - commands.close() - // состояние без блокировок: 100 -} -``` -Синхронизации нет вообще: состояние изменяет одна корутина, остальные присылают сообщения. Подробнее: [Разделяемое состояние](Разделяемое%20состояние.md) - -## Channel против Flow -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - // Channel: горячий, значения расходятся по получателям - val channel = Channel(capacity = 3) - channel.send(1); channel.send(2) - channel.close() - println("канал раздал: ${channel.toList()}") - - // Flow: холодный, каждый подписчик получает всё - val flow = flowOf(1, 2) - println("поток повторил: ${flow.toList()} ${flow.toList()}") - // канал раздал: [1, 2] - // поток повторил: [1, 2] [1, 2] -} -``` -| | `Channel` | `Flow` | -|---|---|---| -| природа | горячий, объект-очередь | холодный, описание | -| значение получает | один получатель | каждый подписчик | -| существует без получателей | да, накапливает | нет, не запущен | -| нужно закрывать | да | нет | -| операторы | почти нет | десятки | -| типичное применение | пул воркеров, развязка корутин | данные, события, UI | - -Практическое правило: **начинайте с `Flow`**. Канал нужен, когда значения должны распределяться между получателями (пул воркеров), когда производитель и потребитель должны работать независимо, или когда нужна точная политика буферизации. Для «потока данных из репозитория в UI» канал - неправильный выбор: он одноразовый и легко теряет данные. - -Если канал уже есть, его можно превратить во `Flow`: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - val channel = Channel(capacity = 2) - channel.send(1); channel.send(2) - channel.close() - - println(channel.consumeAsFlow().toList()) // [1, 2] -} -``` -`consumeAsFlow()` можно собрать только один раз - канал одноразовый. Повторный `collect` бросит `IllegalStateException`. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `BlockingQueue` | `Channel` | -> | `ArrayBlockingQueue(n)` | `Channel(capacity = n)` | -> | `LinkedBlockingQueue()` | `Channel(UNLIMITED)` | -> | `SynchronousQueue` | `Channel(RENDEZVOUS)` | -> | `queue.put(x)` (блокирует поток) | `channel.send(x)` (приостанавливает) | -> | `queue.take()` (блокирует поток) | `channel.receive()` (приостанавливает) | -> | «отравленная пилюля» как признак конца | `close()` | -> | `java.util.concurrent.Flow` | `Flow` | -> -> **`BlockingQueue` блокирует поток, канал - нет.** Это главное различие. Пул воркеров на `BlockingQueue` требует по потоку на воркера, и 1000 воркеров - это 1000 потоков. Канал держит их как корутины: -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.channels.* -> import java.util.concurrent.atomic.AtomicInteger -> -> fun main() = runBlocking { -> val tasks = Channel(capacity = 100) -> val done = AtomicInteger(0) -> repeat(1000) { // 1000 воркеров -> launch { for (t in tasks) { delay(1); done.incrementAndGet() } } -> } -> repeat(500) { tasks.send(it) } -> tasks.close() -> delay(200) -> println("обработано: ${done.get()}") // обработано: 500 -> } -> ``` -> В Java это 1000 потоков и около гигабайта стеков. -> -> **Признак конца данных.** `BlockingQueue` не умеет закрываться: чтобы сообщить воркерам «задач больше нет», в очередь кладут специальное значение - «отравленную пилюлю» (`POISON_PILL`), причём по одной на каждого воркера, иначе часть из них зависнет навсегда: -> ```java -> for (int i = 0; i < workers; i++) queue.put(POISON_PILL); -> ``` -> Это хрупкий паттерн: нужно знать точное число получателей, а сама пилюля ломает типизацию (обычно это `null`-объект или специальный экземпляр). `channel.close()` корректно завершает всех получателей, сколько бы их ни было, а `for (x in channel)` просто выходит из цикла. -> -> **Передача ошибки.** В Java исключение из производителя в очередь не положить - его оборачивают в элемент-обёртку (`Either`-подобный класс) или передают через отдельный механизм. `channel.close(exception)` доставляет исключение получателям, и оно выбрасывается в точке `receive`. -> -> **Таймауты и выбор из нескольких очередей.** `BlockingQueue` даёт `poll(timeout, unit)`, но ждать «первое значение из двух очередей» штатно невозможно - приходится опрашивать по очереди с короткими таймаутами. `select` в Kotlin решает это напрямую, причём без активного ожидания. -> -> **`java.util.concurrent.Flow` - это Reactive Streams, а не очередь.** Оно появилось в Java 9 как SPI для реактивных библиотек, с ручным управлением давлением через `Subscription.request(n)`. Ближайший аналог в Kotlin - `Flow`, а не `Channel`. Подробнее: [Flow](Flow.md) -> -> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [JVM](../../Программирование%20на%20Java/JVM/JVM.md) - -> [!warning] Подводные камни -> **Незакрытый канал подвешивает получателя навсегда:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.channels.* -> -> fun main() = runBlocking { -> val channel = Channel() -> launch { -> channel.send(1) -> // close() забыли -> } -> val job = launch { -> for (v in channel) println("получено: $v") -> println("этого не будет") -> } -> delay(50) -> println("получатель всё ещё ждёт: ${job.isActive}") -> coroutineContext.cancelChildren() -> // получено: 1 -> // получатель всё ещё ждёт: true -> } -> ``` -> `for`-цикл по каналу ждёт `close()`. Используйте `produce`, который закрывает канал сам. -> -> **`receive()` на закрытом канале бросает исключение:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.channels.* -> -> fun main() = runBlocking { -> val channel = Channel() -> channel.close() -> val error = runCatching { channel.receive() }.exceptionOrNull() -> println(error is ClosedReceiveChannelException) // true -> } -> ``` -> Для безопасного чтения - `receiveCatching()` или `for`-цикл. -> -> **`send` на закрытом канале тоже бросает:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.channels.* -> -> fun main() = runBlocking { -> val channel = Channel() -> channel.close() -> val error = runCatching { channel.send(1) }.exceptionOrNull() -> println(error is ClosedSendChannelException) // true -> } -> ``` -> `trySend` вместо исключения вернёт неуспешный результат. -> -> **`Channel.UNLIMITED` - это `OutOfMemoryError` под нагрузкой.** Он снимает обратное давление: производитель быстрее потребителя будет накапливать элементы в памяти, пока процесс не упадёт. Безлимитный канал оправдан только при известном конечном числе элементов. -> -> **`trySend` молча теряет данные:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.channels.* -> -> fun main() = runBlocking { -> val channel = Channel(capacity = 2) -> val sent = (1..5).count { channel.trySend(it).isSuccess } -> println("отправлено из 5: $sent") // отправлено из 5: 2 -> } -> ``` -> Результат `trySend` обычно игнорируют - и три элемента исчезают без следа. Проверяйте `isSuccess` или используйте `send`. -> -> **Канал - одноразовый.** Повторный `consumeAsFlow().collect` или второй `toList()` после исчерпания бросит исключение либо вернёт пустой результат. Для многократного чтения нужен `Flow` или `SharedFlow`. -> -> **Канал раздаёт, а не транслирует.** Если нужно, чтобы **все** подписчики получили каждое событие, канал не подойдёт - элемент достаётся одному получателю. Нужен `SharedFlow`. Подробнее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md) -> -> **`CONFLATED` теряет значения по определению.** Он хранит только последнее непрочитанное - для счётчиков и журналов это ошибка, для «актуального состояния» - норма. -> -> **Отмена корутины с `send` оставляет элемент непереданным:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.channels.* -> -> fun main() = runBlocking { -> val channel = Channel() -> val sender = launch { channel.send(1) } // ждёт получателя -> delay(20) -> sender.cancel() -> println("отправитель отменён: ${sender.isCancelled}") -> println("канал пуст: ${channel.tryReceive().isFailure}") -> // отправитель отменён: true -> // канал пуст: true -> } -> ``` -> Элемент не доставлен и не сохранён. Для надёжной доставки нужен буфер или подтверждение. -> -> **Проигравший в `select` продолжает работать.** `select` возвращает первый результат, но остальные `Deferred` и корутины живут дальше, расходуя ресурсы. Отменяйте их явно. -> -> **`select` с `onSend` и несколькими каналами не атомарен между итерациями.** В цикле состояние каналов между проверками меняется - на `select` нельзя строить транзакционную логику. -> -> **`produce` и `actor` помечены устаревшими** (`@ObsoleteCoroutinesApi`): их API может измениться. Для новых источников данных предпочтительнее `Flow`, для состояния - `Mutex` или `StateFlow`. -> -> **`isClosedForReceive`/`isEmpty` - гонка по определению.** Между проверкой и действием состояние изменится. Единственный корректный способ - `receiveCatching()` или `for`-цикл, обрабатывающие закрытие атомарно. - -Далее: [Разделяемое состояние](Разделяемое%20состояние.md) diff --git a/Kotlin/Корутины/Flow.md b/Kotlin/Корутины/Flow.md deleted file mode 100644 index f116f54..0000000 --- a/Kotlin/Корутины/Flow.md +++ /dev/null @@ -1,783 +0,0 @@ ---- -title: Flow -description: Холодный асинхронный поток значений: построение, операторы, контекст, обработка ошибок и backpressure -tags: - - kotlin - - coroutines - - flow ---- - -`Flow` - асинхронная последовательность значений. Если `suspend`-функция возвращает одно значение, то `Flow` - много, причём каждое может приходить с задержкой. - -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun temperatures(): Flow = flow { - for (t in listOf(20, 22, 25)) { - delay(20) // внутри flow можно приостанавливаться - emit(t) - } -} - -fun main() = runBlocking { - temperatures().collect { println("температура: $it") } - // температура: 20 - // температура: 22 - // температура: 25 -} -``` -Сравните с `List`: список нужно вычислить целиком, прежде чем вернуть. `Flow` отдаёт значения по мере готовности. - -## Холодный поток -`Flow` ничего не делает, пока его не начнут собирать. Каждый вызов `collect` запускает тело заново. -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - var starts = 0 - val f = flow { - starts++ - emit(1) - emit(2) - } - - println("создан, запусков: $starts") // создан, запусков: 0 - f.collect { } - f.collect { } - println("после двух collect: $starts") // после двух collect: 2 -} -``` -Это главное свойство: `Flow` - **описание** вычисления, а не запущенный процесс. Горячие потоки, которые работают независимо от подписчиков, - это `StateFlow` и `SharedFlow`. Подробнее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md) - -## Способы создать Flow -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - flowOf(1, 2, 3).collect { print("$it ") } - println() // 1 2 3 - - listOf("a", "b").asFlow().collect { print("$it ") } - println() // a b - - (1..3).asFlow().collect { print("$it ") } - println() // 1 2 3 - - flow { - emit("из билдера") - emitAll(flowOf("и", "вложенного")) - }.collect { print("$it ") } - println() // из билдера и вложенного -} -``` -Для колбэчных источников есть `callbackFlow` - он же мост от слушателей к `Flow`: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.* - -class Sensor { - private var listener: ((Int) -> Unit)? = null - fun subscribe(l: (Int) -> Unit) { listener = l } - fun unsubscribe() { listener = null; println("слушатель снят") } - fun fire(value: Int) = listener?.invoke(value) -} - -fun Sensor.readings(): Flow = callbackFlow { - subscribe { value -> trySend(value) } - awaitClose { unsubscribe() } // вызовется при отмене collect -} - -fun main() = runBlocking { - val sensor = Sensor() - val job = launch { - sensor.readings().collect { println("показание: $it") } - } - delay(20) - sensor.fire(42) - sensor.fire(43) - delay(20) - job.cancelAndJoin() - // показание: 42 - // показание: 43 - // слушатель снят -} -``` -`awaitClose` обязателен: без него `callbackFlow` бросит `IllegalStateException`, потому что иначе слушатель остался бы висеть. - -## Промежуточные операторы -Операторы ленивые: они только оборачивают поток, не запуская его. -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - val result = (1..10).asFlow() - .filter { it % 2 == 0 } - .map { it * it } - .take(3) - .toList() - println(result) // [4, 16, 36] -} -``` -Главное отличие от операторов коллекций - внутри можно приостанавливаться: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -suspend fun enrich(id: Int): String { delay(10); return "запись $id" } - -fun main() = runBlocking { - flowOf(1, 2, 3) - .map { enrich(it) } // suspend внутри map - .collect { println(it) } - // запись 1 - // запись 2 - // запись 3 -} -``` - -| Оператор | Что делает | -|---|---| -| `map`, `filter` | как у коллекций, но с `suspend` внутри | -| `transform` | произвольное число `emit` на элемент | -| `take`, `drop`, `takeWhile` | ограничение по количеству | -| `onEach` | побочный эффект без изменения значений | -| `withIndex`, `runningFold`, `scan` | накопление | -| `distinctUntilChanged` | пропуск подряд идущих дублей | -| `debounce`, `sample` | фильтрация по времени | -| `flatMapConcat`, `flatMapMerge`, `flatMapLatest` | поток из потоков | -| `zip`, `combine` | объединение двух потоков | -| `onStart`, `onCompletion`, `catch` | обработка жизненного цикла | - -`transform` - самый общий: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - flowOf(1, 2, 3) - .transform { value -> - emit("начало $value") - emit("конец $value") - } - .collect { print("$it; ") } - println() - // начало 1; конец 1; начало 2; конец 2; начало 3; конец 3; -} -``` - -## Терминальные операторы -Только они запускают поток, и все они `suspend`: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - val f = flowOf(1, 2, 3, 4) - - println(f.toList()) // [1, 2, 3, 4] - println(f.first()) // 1 - println(f.firstOrNull { it > 10 }) // null - println(f.lastOrNull()) // 4 - println(f.count()) // 4 - println(f.reduce { a, b -> a + b }) // 10 - println(f.fold(100) { a, b -> a + b }) // 110 - println(f.toSet()) // [1, 2, 3, 4] - println(f.filter { it == 2 }.single()) // 2 -} -``` -`first()` отменяет поток сразу после первого значения - этого достаточно, чтобы остановить бесконечный источник: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - var emitted = 0 - val infinite = flow { - var i = 0 - while (true) { - emitted++ - emit(i++) - } - } - println(infinite.first()) // 0 - println("излучено значений: $emitted") // излучено значений: 1 -} -``` - -## Контекст и flowOn -По умолчанию `Flow` выполняется в контексте того, кто его собирает - это называется context preservation: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - flow { - println("flow на: ${Thread.currentThread().name}") - emit(1) - }.collect { - println("collect на: ${Thread.currentThread().name}") - } - // flow на: main - // collect на: main -} -``` -Сменить контекст **производителя** можно только через `flowOn`: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - flow { - val onPool = Thread.currentThread().name.contains("DefaultDispatcher") - println("flow в пуле: $onPool") - emit(1) - } - .flowOn(Dispatchers.IO) - .collect { - println("collect на: ${Thread.currentThread().name}") - } - // flow в пуле: true - // collect на: main -} -``` -`flowOn` действует на всё, что **выше** него по цепочке: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - flowOf(1, 2) - .map { it * 10 } // в IO - .flowOn(Dispatchers.IO) - .map { it + 1 } // в main - .collect { print("$it ") } - println() // 11 21 -} -``` -Менять контекст внутри `flow { }` через `withContext` запрещено: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - val broken = flow { - withContext(Dispatchers.IO) { emit(1) } // нарушение контракта - } - val error = runCatching { broken.collect { } }.exceptionOrNull() - println(error is IllegalStateException) // true - println(error?.message?.contains("Flow invariant is violated")) // true -} -``` -Причина запрета - предсказуемость: собирающий код должен быть уверен, что `collect` вызывается в его контексте (например, в UI-потоке). Для смены контекста есть только `flowOn`. - -## Обработка ошибок -`try/catch` работает, но правильнее оператор `catch`: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - flow { - emit(1) - throw IllegalStateException("источник упал") - } - .catch { e -> println("перехвачено: ${e.message}") } - .collect { println("значение: $it") } - // значение: 1 - // перехвачено: источник упал -} -``` -`catch` ловит ошибки только **выше** себя - это называется прозрачность исключений: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - val error = runCatching { - flowOf(1) - .catch { println("не сработает") } - .collect { throw IllegalStateException("ошибка в collect") } - }.exceptionOrNull() - println("ошибка collect прошла мимо catch: ${error?.message}") - // ошибка collect прошла мимо catch: ошибка в collect -} -``` -Чтобы ловить и ошибки потребителя, нужен `onEach` + `catch` в таком порядке: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - flowOf(1, 2) - .onEach { if (it == 2) error("обработка упала на $it") } - .catch { e -> println("перехвачено: ${e.message}") } - .collect { println("обработано: $it") } - // обработано: 1 - // перехвачено: обработка упала на 2 -} -``` -`catch` может подменить поток значениями по умолчанию: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - flow { error("сеть недоступна") } - .catch { emit("данные из кеша") } - .collect { println(it) } - // данные из кеша -} -``` - -## retry и повторные попытки -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - var attempt = 0 - flow { - attempt++ - if (attempt < 3) error("попытка $attempt не удалась") - emit("успех на попытке $attempt") - } - .retry(3) { e -> - println("ретрай после: ${e.message}") - delay(10) - true - } - .collect { println(it) } - // ретрай после: попытка 1 не удалась - // ретрай после: попытка 2 не удалась - // успех на попытке 3 -} -``` -`retryWhen` даёт доступ к номеру попытки - это нужно для экспоненциальной задержки: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - var calls = 0 - flow { - calls++ - if (calls < 3) error("сбой") - emit("готово") - } - .retryWhen { _, attempt -> - if (attempt >= 3) false - else { - delay(10L * (attempt + 1)) // 10, 20, 30 мс - println("попытка ${attempt + 1}") - true - } - } - .collect { println(it) } - // попытка 1 - // попытка 2 - // готово -} -``` - -## Жизненный цикл: onStart, onCompletion -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - flowOf(1, 2) - .onStart { println("начали") } - .onEach { println("значение $it") } - .onCompletion { cause -> println("завершили, ошибка: $cause") } - .collect { } - // начали - // значение 1 - // значение 2 - // завершили, ошибка: null -} -``` -`onCompletion` вызывается всегда - при успехе, ошибке и отмене, причём получает причину: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - flow { - emit(1) - error("упало") - } - .onCompletion { cause -> println("причина: ${cause?.message}") } - .catch { } - .collect { } - // причина: упало -} -``` -`onStart` может излучать значения - это способ показать «загрузка...» до первого реального элемента: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - flow { delay(20); emit("данные") } - .onStart { emit("загрузка...") } - .collect { println(it) } - // загрузка... - // данные -} -``` - -## buffer: производитель и потребитель параллельно -По умолчанию производитель ждёт потребителя: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import kotlin.system.measureTimeMillis - -fun source() = flow { - repeat(3) { - delay(20) // производство - emit(it) - } -} - -fun main() = runBlocking { - val sequential = measureTimeMillis { - source().collect { delay(20) } // потребление - } - val buffered = measureTimeMillis { - source().buffer().collect { delay(20) } - } - println("без буфера около 120 мс: ${sequential >= 110}") - println("с буфером быстрее: ${buffered < sequential}") - // без буфера около 120 мс: true - // с буфером быстрее: true -} -``` -Без буфера шаги складываются: 3 × (20 + 20) = 120 мс. С буфером производство идёт параллельно с потреблением, и общее время приближается к максимуму из двух конвейеров. - -## conflate и collectLatest -Когда важны только свежие значения, промежуточные можно отбрасывать: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun fast() = flow { - repeat(5) { - delay(10) - emit(it) - } -} - -fun main() = runBlocking { - val all = mutableListOf() - fast().collect { delay(40); all += it } - - val conflated = mutableListOf() - fast().conflate().collect { delay(40); conflated += it } - - println("без conflate получено всё: ${all.size == 5}") - println("с conflate пропущены промежуточные: ${conflated.size < 5}") - println("последнее значение всегда доходит: ${conflated.last() == 4}") - // без conflate получено всё: true - // с conflate пропущены промежуточные: true - // последнее значение всегда доходит: true -} -``` -`collectLatest` работает иначе: он не пропускает значения, а **отменяет** обработку предыдущего: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - val completed = mutableListOf() - flow { - repeat(3) { delay(10); emit(it) } - }.collectLatest { value -> - delay(25) // не успеет для первых двух - completed += value - } - println("доработал только последний: $completed") // доработал только последний: [2] -} -``` - -| Стратегия | Поведение при быстром производителе | -|---|---| -| по умолчанию | производитель ждёт потребителя | -| `buffer(n)` | очередь на `n` элементов, затем ожидание | -| `buffer(onBufferOverflow = DROP_OLDEST)` | выбрасывает старые | -| `conflate()` | хранит только последнее непрочитанное | -| `collectLatest` | отменяет обработку устаревшего значения | -| `debounce(ms)` | пропускает только после паузы | -| `sample(ms)` | берёт последнее раз в интервал | - -## Объединение потоков -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - val letters = flowOf("a", "b", "c") - val numbers = flowOf(1, 2, 3) - - println(letters.zip(numbers) { l, n -> "$l$n" }.toList()) // [a1, b2, c3] - - val slow = flow { emit(1); delay(30); emit(2) } - val fast2 = flow { emit("x"); delay(10); emit("y") } - println(slow.combine(fast2) { a, b -> "$a$b" }.toList()) - // [1x, 1y, 2y] -} -``` -`zip` ждёт пару - элементов будет столько, сколько в коротком потоке. `combine` излучает при каждом изменении любого из потоков, используя последние известные значения другого. Для UI (например, «форма валидна, если валидны все поля») нужен `combine`. - -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - val concatenated = flowOf(1, 2).flatMapConcat { n -> - flow { emit("$n-a"); delay(10); emit("$n-b") } - }.toList() - println(concatenated) // [1-a, 1-b, 2-a, 2-b] - - val latest = flowOf(1, 2, 3).flatMapLatest { n -> - flow { delay(20); emit("результат $n") } - }.toList() - println(latest) // [результат 3] -} -``` -`flatMapLatest` - основной оператор для поиска по вводу: новый запрос отменяет предыдущий. - -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -suspend fun search(query: String): List { - delay(20) - return listOf("результат для «$query»") -} - -fun main() = runBlocking { - val input = flow { - emit("к"); delay(5) - emit("ко"); delay(5) - emit("кот"); delay(100) // пользователь остановился - emit("кота") - } - - input - .debounce(30) // ждём паузу в наборе - .filter { it.length >= 2 } - .flatMapLatest { search(it).asFlow() } - .collect { println(it) } - // результат для «кот» - // результат для «кота» -} -``` -Это каноничный конвейер: `debounce` отсекает промежуточные нажатия, `flatMapLatest` отменяет устаревший запрос. - -```mermaid -graph LR - A["ввод: к, ко, кот"] --> B["debounce(30)"] - B --> C["только 'кот'
после паузы"] - C --> D["flatMapLatest"] - D --> E["запрос к API"] - E --> F["collect: результаты"] - D -.->|"пришёл новый ввод"| G["отмена
предыдущего запроса"] -``` - -## Flow как возвращаемый тип -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -class OrderRepository { - // не suspend: Flow холодный, вызов ничего не запускает - fun observeOrders(): Flow = flow { - repeat(3) { i -> - delay(10) - emit("заказ $i") - } - }.flowOn(Dispatchers.IO) // производитель всегда в IO -} - -fun main() = runBlocking { - OrderRepository().observeOrders().collect { println(it) } - // заказ 0 - // заказ 1 - // заказ 2 -} -``` -Правила проектирования: функция, возвращающая `Flow`, не должна быть `suspend` (создание потока мгновенно), а `flowOn` ставится внутри - вызывающий не обязан знать, где работает источник. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `Stream` (синхронный, одноразовый) | `Sequence` | -> | `Flow` из Reactive Streams | `Flow` | -> | RxJava `Observable`/`Flowable` | `Flow` | -> | Project Reactor `Flux` | `Flow` | -> | `Subscriber.request(n)` | приостановка `emit` | -> | `subscribeOn`/`observeOn` | `flowOn` | -> | `onErrorResume` | `catch` | -> | `BlockingQueue` + поток | `Channel` | -> -> **`java.util.stream.Stream` - это не аналог.** `Stream` синхронный: `stream.map(f)` не может ждать ответа сети, а терминальная операция блокирует поток до конца обработки. Прямой аналог `Stream` в Kotlin - `Sequence`. `Flow` отличается тем, что внутри каждого оператора разрешена приостановка. Подробнее: [Sequence](../Коллекции/Sequence.md) -> -> **Reactive Streams решают ту же задачу, но сложнее.** RxJava и Reactor построены на коллбэках (`onNext`, `onError`, `onComplete`) и требуют явного управления давлением через `request(n)`. В `Flow` backpressure получается бесплатно: `emit` - это `suspend`-функция, поэтому если потребитель не готов, производитель просто приостанавливается: -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.flow.* -> -> fun main() = runBlocking { -> var produced = 0 -> flow { -> repeat(100) { produced++; emit(it) } -> }.take(3).collect { } -> println("произведено ровно столько, сколько взяли: $produced") -> // произведено ровно столько, сколько взяли: 3 -> } -> ``` -> Никакого `request(n)` - producer сам останавливается на `emit`. -> -> **Стектрейсы и отладка.** В RxJava стектрейс ошибки состоит из кадров библиотеки, а место в бизнес-логике приходится искать по `onAssembly`-хукам. `Flow` - обычные `suspend`-функции, поэтому исключение приходит с кадрами вашего кода, а `try/catch`, `for` и `return` работают внутри операторов естественно. -> -> **Количество операторов.** В RxJava их несколько сотен (`flatMapIterable`, `concatMapDelayError`, `switchIfEmpty`...), потому что каждый сценарий требует отдельного оператора - внутри цепочки нельзя писать обычный код. В `Flow` операторов несколько десятков: остальное выражается через `transform` и обычные конструкции языка. -> -> **Отмена подписки.** В Rx нужно хранить `Disposable` и не забыть вызвать `dispose()` - классический источник утечек, для борьбы с которым придумали `CompositeDisposable`. `Flow` собирается внутри корутины, поэтому отмена области автоматически прекращает сбор: -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.flow.* -> -> fun main() = runBlocking { -> val job = launch { -> flow { -> try { -> repeat(100) { delay(10); emit(it) } -> } finally { println("источник закрыт") } -> }.collect { } -> } -> delay(30) -> job.cancelAndJoin() -> // источник закрыт -> } -> ``` -> -> **Context preservation - то, чего нет в Rx.** В RxJava `observeOn` можно поставить в любом месте, и понять, на каком потоке выполнится `subscribe`, можно только прочитав всю цепочку. `Flow` гарантирует, что `collect` всегда выполняется в контексте собирающего, а `withContext` внутри `flow { }` запрещён на уровне рантайма. -> -> Сравнить: [Многопоточность в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md) - -> [!warning] Подводные камни -> **`Flow` не запустится без терминального оператора:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.flow.* -> -> fun main() = runBlocking { -> var side = 0 -> flowOf(1, 2, 3).onEach { side++ } // collect не вызван -> delay(20) -> println("побочных эффектов: $side") // побочных эффектов: 0 -> } -> ``` -> Компилятор предупреждает о неиспользованном результате, но не всегда - например, если значение присвоено переменной. -> -> **`withContext` внутри `flow { }` бросает исключение в рантайме:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.flow.* -> -> fun main() = runBlocking { -> val error = runCatching { -> flow { withContext(Dispatchers.IO) { emit(1) } }.collect { } -> }.exceptionOrNull() -> println(error?.message?.contains("Flow invariant is violated")) // true -> } -> ``` -> Это ошибка времени выполнения, а не компиляции - её легко пропустить в редкой ветке кода. Используйте `flowOn`. -> -> **`flowOn` действует только выше по цепочке.** `flow.map { a }.flowOn(IO).map { b }` выполнит `a` в IO, а `b` - в контексте сборщика. Порядок операторов меняет смысл. -> -> **`catch` не ловит ошибки `collect`:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.flow.* -> -> fun main() = runBlocking { -> val error = runCatching { -> flowOf(1).catch { println("не вызовется") }.collect { error("из collect") } -> }.exceptionOrNull() -> println("ошибка прошла мимо: ${error?.message}") // ошибка прошла мимо: из collect -> } -> ``` -> Перенесите логику в `onEach` перед `catch`. -> -> **`emit` из другой корутины запрещён:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.flow.* -> -> fun main() = runBlocking { -> val broken = flow { -> coroutineScope { -> launch { emit(1) } // emit в другой корутине -> } -> } -> val error = runCatching { broken.collect { } }.exceptionOrNull() -> println(error != null) // true -> } -> ``` -> Сообщение - про нарушение инварианта. Для излучения из нескольких источников нужен `channelFlow` или `callbackFlow`. -> -> **`callbackFlow` без `awaitClose` падает:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.flow.* -> -> fun main() = runBlocking { -> val broken = callbackFlow { -> trySend(1) -> // нет awaitClose -> } -> val error = runCatching { broken.collect { } }.exceptionOrNull() -> println(error is IllegalStateException) // true -> } -> ``` -> Это защита от утечки слушателя: без `awaitClose` библиотека не знает, когда отписываться. -> -> **`trySend` молча теряет значения при полном буфере.** У `callbackFlow` буфер по умолчанию 64 элемента; `trySend` возвращает `ChannelResult`, который обычно игнорируют. Если источник быстрее потребителя, часть событий исчезнет - проверяйте результат или задавайте `buffer(Channel.UNLIMITED)` осознанно. -> -> **Повторный `collect` запускает поток заново.** Если `Flow` делает сетевой запрос, два `collect` - это два запроса. Для разделения результата между подписчиками нужен `shareIn`/`stateIn`. Подробнее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md) -> -> **`Flow` без буфера последователен, а не параллелен:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.flow.* -> import kotlin.system.measureTimeMillis -> -> fun main() = runBlocking { -> val t = measureTimeMillis { -> flowOf(1, 2, 3).map { delay(30); it }.collect { } -> } -> println("последовательно, около 90 мс: ${t >= 85}") // последовательно, около 90 мс: true -> } -> ``` -> `map` с `suspend` внутри обрабатывает элементы по одному. Для параллельной обработки нужен `flatMapMerge` с `concurrency` или `map { async { } }` поверх списка. -> -> **`conflate` и `collectLatest` теряют данные - это их цель.** Для UI-состояния это правильно, для обработки платежей или записи в журнал - нет. Убедитесь, что потеря промежуточных значений допустима. -> -> **`debounce` задерживает даже последнее значение.** Пользователь перестал печатать - результат появится только через указанный интервал. Слишком большое значение ощущается как лаг интерфейса. -> -> **Бесконечный `Flow` в `toList()` повиснет.** Терминальные операторы, собирающие всё (`toList`, `count`, `fold`), на бесконечном источнике не завершатся никогда. Нужен `take(n)` или `first()`. -> -> **`SharingStarted.Eagerly` в `stateIn` держит источник активным всегда** - типичная утечка при переносе холодного `Flow` в горячий. Подробнее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md) -> -> **Исключение в `onCompletion` не перехватывается предыдущим `catch`.** `onCompletion` - не `finally`: он не может подавить ошибку, а своя ошибка внутри него уйдёт потребителю. -> -> **`first()` отменяет источник.** Для `callbackFlow` это значит, что слушатель будет снят сразу после первого значения - если вы ожидали продолжения подписки, её нет. - -Далее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md) diff --git a/Kotlin/Корутины/StateFlow и SharedFlow.md b/Kotlin/Корутины/StateFlow и SharedFlow.md deleted file mode 100644 index c730964..0000000 --- a/Kotlin/Корутины/StateFlow и SharedFlow.md +++ /dev/null @@ -1,618 +0,0 @@ ---- -title: StateFlow и SharedFlow -description: Горячие потоки: хранение состояния, события, replay, stateIn и shareIn, конфляция и выбор между ними -tags: - - kotlin - - coroutines - - flow ---- - -`StateFlow` и `SharedFlow` - **горячие** потоки: они существуют и работают независимо от подписчиков. Обычный `Flow` холодный - он запускается на каждый `collect` и умирает вместе с ним. - -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - val state = MutableStateFlow(0) - - val job = launch { - state.collect { println("подписчик: $it") } - } - delay(20) - state.value = 1 - state.value = 2 - delay(20) - job.cancel() - println("текущее значение без подписчиков: ${state.value}") - // подписчик: 0 - // подписчик: 1 - // подписчик: 2 - // текущее значение без подписчиков: 2 -} -``` -Обратите внимание: подписчик сразу получил текущее значение `0`, а после отмены сбора состояние осталось доступным через `.value`. - -## StateFlow: состояние -`StateFlow` - это наблюдаемое значение. У него всегда есть текущее состояние, и оно всегда одно. - -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -class Counter { - private val _count = MutableStateFlow(0) - val count: StateFlow = _count.asStateFlow() // только чтение снаружи - - fun increment() { _count.value++ } - fun add(n: Int) = _count.update { it + n } // атомарно -} - -fun main() = runBlocking { - val counter = Counter() - val job = launch { counter.count.collect { println("счёт: $it") } } - - delay(20) - counter.increment() - delay(20) - counter.add(10) - delay(20) - job.cancel() - // счёт: 0 - // счёт: 1 - // счёт: 11 -} -``` -Пара «приватный `MutableStateFlow` + публичный `StateFlow`» - основной шаблон: изменять состояние может только владелец, читать - все. - -## StateFlow конфлюирует и дедуплицирует -Два свойства, которые нужно знать наизусть. - -**1. Одинаковые подряд значения не доходят** - `StateFlow` сравнивает через `equals`: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - val state = MutableStateFlow("a") - val received = mutableListOf() - val job = launch { state.collect { received += it } } - - delay(20) - state.value = "a"; delay(20) // то же значение — не придёт - state.value = "b"; delay(20) - state.value = "b"; delay(20) // снова дубль - state.value = "a"; delay(20) - job.cancel() - println(received) // [a, b, a] -} -``` -Задержки между присваиваниями здесь не для красоты: без них подписчик не успел бы обработать значения и сработало бы второе свойство - конфляция (см. ниже), из-за которой он увидел бы только `a` и финальное значение. - -Это бесплатный `distinctUntilChanged`, но он же источник ошибок с изменяемыми объектами: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -class MutableBox(val items: MutableList) - -fun main() = runBlocking { - val box = MutableBox(mutableListOf(1)) - val state = MutableStateFlow(box) - var updates = 0 - val job = launch { state.collect { updates++ } } - - delay(20) - box.items.add(2) // изменили содержимое - state.value = box // тот же объект — equals true - delay(20) - job.cancel() - println("обновлений: $updates") // обновлений: 1 -} -``` -Подписчик не узнал об изменении. Поэтому состояние в `StateFlow` должно быть неизменяемым - `data class` с `val` и неизменяемыми коллекциями. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md) - -**2. Медленный подписчик пропускает промежуточные значения:** -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - val state = MutableStateFlow(0) - val received = mutableListOf() - val job = launch { - state.collect { delay(50); received += it } - } - delay(10) - repeat(10) { state.value = it + 1 } - delay(150) - job.cancel() - println("получено не всё: ${received.size < 10}") - println("последнее значение дошло: ${received.last() == 10}") - // получено не всё: true - // последнее значение дошло: true -} -``` -`StateFlow` - про актуальное состояние, а не про историю. Последнее значение дойдёт всегда, промежуточные - нет. - -## MutableStateFlow.update -Присваивание `value` не атомарно при конкуренции: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - val unsafe = MutableStateFlow(0) - withContext(Dispatchers.Default) { - List(100) { launch { repeat(100) { unsafe.value = unsafe.value + 1 } } } - .forEach { it.join() } - } - println("потеряны обновления: ${unsafe.value < 10_000}") - - val safe = MutableStateFlow(0) - withContext(Dispatchers.Default) { - List(100) { launch { repeat(100) { safe.update { v -> v + 1 } } } } - .forEach { it.join() } - } - println("update атомарен: ${safe.value == 10_000}") - // потеряны обновления: true - // update атомарен: true -} -``` -`update` выполняет CAS-цикл, поэтому при конкурентных изменениях нужен именно он. Подробнее: [Разделяемое состояние](Разделяемое%20состояние.md) - -## SharedFlow: события -`SharedFlow` не хранит «текущее значение» - он транслирует события всем подписчикам. -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - val events = MutableSharedFlow() - - val a = launch { events.collect { println("A: $it") } } - val b = launch { events.collect { println("B: $it") } } - delay(20) // дать подписчикам стартовать - - events.emit("событие 1") - delay(20) - a.cancel(); b.cancel() - // A: событие 1 - // B: событие 1 -} -``` -По умолчанию `replay = 0`: подписчик получает только то, что произошло **после** подписки. Событие, отправленное без подписчиков, исчезает: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - val events = MutableSharedFlow() - events.emit("в пустоту") // подписчиков нет - - val received = mutableListOf() - val job = launch { events.collect { received += it } } - delay(20) - events.emit("услышано") - delay(20) - job.cancel() - println(received) // [услышано] -} -``` - -## replay -`replay` задаёт, сколько последних событий получит новый подписчик: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - val events = MutableSharedFlow(replay = 2) - events.emit(1) - events.emit(2) - events.emit(3) - - val received = mutableListOf() - val job = launch { events.collect { received += it } } - delay(20) - job.cancel() - println(received) // [2, 3] -} -``` -`StateFlow` эквивалентен `SharedFlow(replay = 1)` с дедупликацией и обязательным начальным значением. - -## Буфер и стратегия переполнения -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - val dropping = MutableSharedFlow( - replay = 0, - extraBufferCapacity = 2, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - val received = mutableListOf() - val job = launch { dropping.collect { delay(50); received += it } } - delay(20) - - repeat(10) { dropping.emit(it) } // emit не приостанавливается - delay(200) - job.cancel() - println("часть событий отброшена: ${received.size < 10}") // часть событий отброшена: true -} -``` -| Параметр | Смысл | -|---|---| -| `replay` | сколько событий повторить новому подписчику | -| `extraBufferCapacity` | буфер сверх `replay` | -| `onBufferOverflow` | `SUSPEND` (по умолчанию), `DROP_OLDEST`, `DROP_LATEST` | - -Важно: `DROP_OLDEST`/`DROP_LATEST` работают только при ненулевом буфере (`replay + extraBufferCapacity > 0`). - -`tryEmit` - неприостанавливающаяся отправка: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - val noBuffer = MutableSharedFlow() - println("без буфера и подписчиков: ${noBuffer.tryEmit(1)}") // без буфера и подписчиков: true - - val withSubscriber = MutableSharedFlow() - val job = launch { withSubscriber.collect { delay(1000) } } - delay(20) - println("подписчик занят, буфера нет: ${withSubscriber.tryEmit(2)}") // подписчик занят, буфера нет: false - job.cancel() -} -``` -Тонкость, которая удивляет: без подписчиков `tryEmit` возвращает `true`, хотя событие никто не получил - отправка «удалась», получателей просто не было. - -## stateIn и shareIn -Превращают холодный `Flow` в горячий: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - var starts = 0 - val cold = flow { - starts++ - repeat(3) { delay(20); emit(it) } - } - - val hot = cold.stateIn( - scope = this, - started = SharingStarted.Eagerly, - initialValue = -1, - ) - delay(30) - - val a = launch { hot.take(1).collect { println("A видит: $it") } } - val b = launch { hot.take(1).collect { println("B видит: $it") } } - a.join(); b.join() - println("источник запущен раз: $starts") - // A видит: 0 - // B видит: 0 - // источник запущен раз: 1 -} -``` -Без `stateIn` два подписчика запустили бы источник дважды - два сетевых запроса вместо одного. - -`SharingStarted` определяет жизненный цикл источника: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - var active = 0 - val source = flow { - active++ - try { - repeat(100) { delay(10); emit(it) } - } finally { active-- } - } - - val shared = source.shareIn( - scope = this, - started = SharingStarted.WhileSubscribed(), - replay = 1, - ) - - val job = launch { shared.collect { } } - delay(30) - println("источник активен при подписчике: ${active == 1}") - job.cancelAndJoin() - delay(30) - println("источник остановлен без подписчиков: ${active == 0}") - coroutineContext.cancelChildren() // иначе runBlocking ждёт корутину shareIn - // источник активен при подписчике: true - // источник остановлен без подписчиков: true -} -``` -| `SharingStarted` | Когда источник активен | -|---|---| -| `Eagerly` | сразу и до отмены scope | -| `Lazily` | с первого подписчика, затем навсегда | -| `WhileSubscribed()` | только пока есть подписчики | -| `WhileSubscribed(stopTimeoutMillis = 5000)` | плюс пауза перед остановкой | - -`WhileSubscribed(5000)` - стандартный выбор для Android: при повороте экрана подписчик пересоздаётся за миллисекунды, и таймаут не даёт зря перезапускать запрос. - -## Как выбрать -```mermaid -graph TD - A["Нужен поток значений"] --> B{"Один подписчик,
работа по запросу?"} - B -->|"да"| C["обычный Flow
холодный"] - B -->|"нет, нужно делить"| D{"Есть текущее
значение?"} - D -->|"да, это состояние"| E["StateFlow
initialValue обязателен"] - D -->|"нет, это события"| F{"Нужна история
для новых подписчиков?"} - F -->|"нет"| G["SharedFlow
replay = 0"] - F -->|"да, n последних"| H["SharedFlow
replay = n"] -``` - -| | `StateFlow` | `SharedFlow` | -|---|---|---| -| начальное значение | обязательно | нет | -| `.value` | есть | нет | -| дедупликация | да, по `equals` | нет | -| `replay` | всегда 1 | настраивается | -| потеря промежуточных | да, конфляция | зависит от буфера | -| типичное применение | состояние экрана, настройки | навигация, снекбары, ошибки | - -Практическое правило: состояние (что сейчас на экране) - `StateFlow`; события (что произошло один раз) - `SharedFlow`. Показ сообщения об ошибке через `StateFlow` приводит к повторному показу при пересоздании подписчика. - -## Состояние экрана целиком -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -data class ScreenState( - val loading: Boolean = false, - val items: List = emptyList(), - val error: String? = null, -) - -class ViewModel(private val scope: CoroutineScope) { - private val _state = MutableStateFlow(ScreenState()) - val state: StateFlow = _state.asStateFlow() - - fun load() { - scope.launch { - _state.update { it.copy(loading = true, error = null) } - delay(20) - _state.update { it.copy(loading = false, items = listOf("первый", "второй")) } - } - } -} - -fun main() = runBlocking { - val vm = ViewModel(this) - val job = launch { vm.state.collect { println(it) } } - delay(10) - vm.load() - delay(60) - job.cancel() - // ScreenState(loading=false, items=[], error=null) - // ScreenState(loading=true, items=[], error=null) - // ScreenState(loading=false, items=[первый, второй], error=null) -} -``` -Один `StateFlow` на всё состояние лучше трёх отдельных: подписчик видит согласованный снимок, а не промежуточные комбинации полей. `copy` + `update` делают изменение атомарным. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md) - -## Производные состояния -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - val query = MutableStateFlow("") - val items = MutableStateFlow(listOf("кот", "кофе", "книга")) - - val filtered = combine(query, items) { q, list -> - list.filter { it.startsWith(q) } - }.stateIn(this, SharingStarted.Eagerly, emptyList()) - - delay(20) // Eagerly запускает корутину, но не мгновенно - println(filtered.value) // [кот, кофе, книга] - query.value = "ко" - delay(20) - println(filtered.value) // [кот, кофе] - - coroutineContext.cancelChildren() -} -``` -`combine` + `stateIn` - способ получить вычисляемое состояние из нескольких источников. Важно вызвать `cancelChildren()` или ограничить scope: `stateIn` с `Eagerly` запускает корутину, которая живёт до отмены области. - -## subscriptionCount -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main() = runBlocking { - val events = MutableSharedFlow() - println("подписчиков: ${events.subscriptionCount.value}") // подписчиков: 0 - - val job = launch { events.collect { } } - delay(20) - println("после подписки: ${events.subscriptionCount.value}") // после подписки: 1 - - job.cancelAndJoin() - println("после отмены: ${events.subscriptionCount.value}") // после отмены: 0 -} -``` -Это `StateFlow`, поэтому за числом подписчиков можно наблюдать - именно так `WhileSubscribed` понимает, когда останавливать источник. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `Observable` + `Observer` (паттерн) | `StateFlow` | -> | `PropertyChangeListener` | `StateFlow` | -> | RxJava `BehaviorSubject` | `StateFlow` | -> | RxJava `PublishSubject` | `SharedFlow(replay = 0)` | -> | RxJava `ReplaySubject(n)` | `SharedFlow(replay = n)` | -> | `AtomicReference` + слушатели | `MutableStateFlow` | -> | `CopyOnWriteArrayList` | встроенная многоадресность | -> -> **Паттерн «наблюдатель» вручную.** В Java его пишут заново в каждом проекте: список слушателей, синхронизация при добавлении и удалении, защита от `ConcurrentModificationException` при оповещении, снятие слушателей при закрытии компонента. Классическая реализация: -> ```java -> private final List> listeners = new CopyOnWriteArrayList<>(); -> private volatile int value; -> -> public void setValue(int v) { -> this.value = v; -> for (var l : listeners) l.accept(v); -> } -> public void addListener(Consumer l) { listeners.add(l); } -> public void removeListener(Consumer l) { listeners.remove(l); } -> ``` -> Здесь уже есть проблемы: оповещение идёт в потоке вызывающего, медленный слушатель задерживает остальных, забытый `removeListener` - утечка. `MutableStateFlow` решает всё это за одну строку, а отписка происходит автоматически при отмене корутины-подписчика. -> -> **`BehaviorSubject` против `StateFlow`.** Прямой аналог, но с двумя отличиями. `BehaviorSubject` не дедуплицирует одинаковые значения - в Rx для этого добавляют `distinctUntilChanged()` руками. И `Subject` можно «завершить» через `onComplete`/`onError`, после чего он мёртв; `StateFlow` не завершается никогда - ошибки передают внутри состояния (поле `error`), что для UI удобнее: экран всегда в каком-то состоянии. -> -> **Утечки подписок.** В Rx подписка возвращает `Disposable`, который нужно хранить и вызывать `dispose()`; для борьбы с забывчивостью придумали `CompositeDisposable` и `AutoDispose`. В Kotlin подписчик - корутина, поэтому отмена scope отписывает всех: -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.flow.* -> -> fun main() = runBlocking { -> val state = MutableStateFlow(0) -> val scope = CoroutineScope(Job()) -> scope.launch { state.collect { } } -> delay(20) -> println("подписчиков: ${state.subscriptionCount.value}") -> scope.cancel() -> delay(20) -> println("после cancel: ${state.subscriptionCount.value}") -> // подписчиков: 1 -> // после cancel: 0 -> } -> ``` -> -> **`volatile` поле против `StateFlow`.** `volatile int value` даёт видимость между потоками, но не даёт ни оповещения, ни атомарного обновления (`value++` всё равно гонка - нужен `AtomicInteger`). `MutableStateFlow` совмещает три роли: потокобезопасное хранилище, атомарный `update` и многоадресная рассылка. -> -> **Нет аналога `WhileSubscribed`.** В Java управление жизненным циклом источника при появлении и исчезновении слушателей пишут вручную: счётчик подписчиков, запуск потока при первом, остановка при последнем, таймаут на повторное подключение. `shareIn(started = WhileSubscribed(5000))` - это одна строка. -> -> Сравнить: [Многопоточность в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [Паттерны проектирования](../Функциональный%20Kotlin/Паттерны%20проектирования.md) - -> [!warning] Подводные камни -> **`StateFlow` не доставит одинаковое значение дважды:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.flow.* -> -> fun main() = runBlocking { -> val showError = MutableStateFlow("ошибка сети") -> var shown = 0 -> val job = launch { showError.collect { shown++ } } -> delay(20) -> showError.value = "ошибка сети" // то же самое — не придёт -> delay(20) -> job.cancel() -> println("показов: $shown") // показов: 1 -> } -> ``` -> Для повторяющихся событий (две одинаковые ошибки подряд) нужен `SharedFlow`. -> -> **Изменяемое состояние внутри `StateFlow` ломает оповещение.** Если вы положили `MutableList` и меняете его содержимое, `equals` вернёт `true` и подписчики ничего не узнают. Храните неизменяемые `data class` и обновляйте через `copy`. -> -> **`state.value = state.value + 1` - гонка:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.flow.* -> -> fun main() = runBlocking { -> val state = MutableStateFlow(0) -> withContext(Dispatchers.Default) { -> List(50) { launch { repeat(200) { state.value = state.value + 1 } } } -> .forEach { it.join() } -> } -> println("обновления потеряны: ${state.value < 10_000}") // обновления потеряны: true -> } -> ``` -> Используйте `update { }`. -> -> **`SharedFlow` без подписчиков теряет события.** `emit` при `replay = 0` и нулевом буфере просто завершится, не доставив ничего. Для «событий, которые нельзя потерять» нужен `replay ≥ 1` или `Channel`. Подробнее: [Channels](Channels.md) -> -> **`emit` в `SharedFlow` с `SUSPEND` приостанавливается на медленном подписчике:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.flow.* -> import kotlin.system.measureTimeMillis -> -> fun main() = runBlocking { -> val events = MutableSharedFlow() -> val job = launch { events.collect { delay(100) } } -> delay(20) -> val t = measureTimeMillis { events.emit(1); events.emit(2) } -> println("emit ждал подписчика: ${t >= 90}") // emit ждал подписчика: true -> job.cancel() -> } -> ``` -> Один зависший подписчик блокирует отправку всем. Задавайте `extraBufferCapacity` и стратегию переполнения. -> -> **`tryEmit` возвращает `true` без подписчиков**, хотя событие никто не получил. Проверка `if (tryEmit(x))` не означает «доставлено». -> -> **`DROP_OLDEST` без буфера не работает.** При `replay = 0` и `extraBufferCapacity = 0` стратегия переполнения игнорируется, потому что буфера нет - `emit` будет приостанавливаться. -> -> **`SharingStarted.Eagerly` держит источник навсегда:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.flow.* -> -> fun main() = runBlocking { -> var active = 0 -> val source = flow { -> active++ -> try { repeat(1000) { delay(10); emit(it) } } finally { active-- } -> } -> val hot = source.stateIn(this, SharingStarted.Eagerly, -1) -> delay(30) -> println("работает без единого подписчика: ${active == 1}") -> coroutineContext.cancelChildren() -> // работает без единого подписчика: true -> } -> ``` -> Если источник - опрос сервера, он будет работать и жечь батарею, пока жив scope. Для подписок UI нужен `WhileSubscribed()`. -> -> **`stateIn`/`shareIn` требуют scope, который кто-то отменит.** Передача `GlobalScope` или scope, живущего весь процесс, превращает их в утечку. -> -> **`StateFlow.value` - снимок, а не транзакция:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.flow.* -> -> fun main() = runBlocking { -> val a = MutableStateFlow(1) -> val b = MutableStateFlow(10) -> // между чтениями значения могли измениться -> val sum = a.value + b.value -> println("несогласованный снимок возможен: $sum") // несогласованный снимок возможен: 11 -> } -> ``` -> Два связанных значения нужно держать в одном `StateFlow` как единый объект состояния. -> -> **`collect` на `StateFlow` никогда не завершается.** Это бесконечный поток: `toList()` на нём повиснет, `onCompletion` не вызовется сам. Нужен `take(n)`, `first()` или отмена корутины. -> -> **Подписчик, созданный до `emit`, может его пропустить:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.flow.* -> -> fun main() = runBlocking { -> val events = MutableSharedFlow() -> launch { events.collect { println("не увидит: $it") } } -> events.emit(1) // launch ещё не успел подписаться -> delay(20) -> coroutineContext.cancelChildren() -> // (ничего не напечатано) -> } -> ``` -> `launch` возвращается до начала тела, поэтому подписка ещё не оформлена. В тестах для этого используют `events.subscriptionCount.first { it > 0 }`, в продакшене - `replay ≥ 1`. -> -> **`asStateFlow()` не копирует состояние** - это обёртка только для чтения над тем же объектом. Если внешний код получит доступ к исходному `MutableStateFlow`, он сможет его менять. -> -> **Ошибка в подписчике отменяет его подписку, а не поток.** Исключение в теле `collect` уходит в корутину-подписчик и может уронить её scope, при этом `StateFlow` продолжит жить. Оборачивайте тело `collect` в `try/catch`, если ошибки ожидаемы. - -Далее: [Channels](Channels.md) diff --git a/Kotlin/Корутины/Suspend-функции.md b/Kotlin/Корутины/Suspend-функции.md deleted file mode 100644 index 99e9675..0000000 --- a/Kotlin/Корутины/Suspend-функции.md +++ /dev/null @@ -1,395 +0,0 @@ ---- -title: Suspend-функции -description: Модификатор suspend, Continuation, машина состояний и мост к колбэчным API -tags: - - kotlin - - coroutines ---- - -`suspend` означает: эта функция может приостановиться, поэтому вызывать её разрешено только оттуда, где приостановка возможна. - -```kotlin -import kotlinx.coroutines.* - -suspend fun fetchData(): String { - delay(100) // точка приостановки - return "данные" -} - -fun main() = runBlocking { - println(fetchData()) // данные -} -``` -Вне корутины вызов не скомпилируется: `fun main() { fetchData() }` даёт «Suspend function can only be called within coroutine body». - -## Что добавляет компилятор -`suspend fun fetchData(): String` превращается в метод с дополнительным параметром: -```kotlin -// примерно так это выглядит с точки зрения Java: -// Object fetchData(Continuation continuation) -``` -`Continuation` - это «что делать дальше»: объект с методом `resumeWith`, хранящий сохранённое состояние функции. -```kotlin -import kotlin.coroutines.* - -fun main() { - val continuation = object : Continuation { - override val context = EmptyCoroutineContext - override fun resumeWith(result: Result) { - println("получено: ${result.getOrNull()}") - } - } - continuation.resumeWith(Result.success("значение")) // получено: значение -} -``` -Отсюда и название CPS - continuation-passing style. Компилятор делает это преобразование автоматически, поэтому в исходнике остаётся обычный последовательный код. - -## Машина состояний -Каждая точка приостановки превращается в метку состояния: -```kotlin -import kotlinx.coroutines.* - -suspend fun threeSteps(): Int { - println("шаг 1") // состояние 0 - delay(10) - println("шаг 2") // состояние 1 - delay(10) - println("шаг 3") // состояние 2 - return 42 -} - -fun main() = runBlocking { - println(threeSteps()) - // шаг 1 - // шаг 2 - // шаг 3 - // 42 -} -``` -В байт-коде это один метод с `switch` по полю `label`: при каждом возобновлении он входит в себя заново и прыгает к нужной метке. Локальные переменные, живущие между приостановками, становятся полями объекта-продолжения. - -Практические следствия: -- приостановка стоит дешевле переключения потока, но дороже обычного вызова -- глубоко вложенные `suspend`-вызовы создают по объекту продолжения на кадр -- стектрейс восстанавливается библиотекой, а не JVM - поэтому он может быть короче ожидаемого - -## Функция, которая не приостанавливается -`suspend` не обязывает приостанавливаться: -```kotlin -import kotlinx.coroutines.* - -suspend fun maybeSlow(cached: String?): String { - cached?.let { return it } // быстрый путь: без приостановки - delay(100) - return "загружено" -} - -fun main() = runBlocking { - println(maybeSlow("из кеша")) // из кеша - println(maybeSlow(null)) // загружено -} -``` -Если приостановки не произошло, функция возвращает значение напрямую - без накладных расходов. Это важно для «горячих» путей: `suspend` не означает «всегда медленно». - -Компилятор предупреждает о `suspend` без точек приостановки: -```kotlin -suspend fun noSuspension(): Int = 42 // warning: redundant 'suspend' modifier - -fun main() { - println("нужен вызов из корутины или runBlocking") -} -``` -Такой модификатор стоит убрать - он заставляет вызывающих быть `suspend` без причины. - -## suspend-лямбды и типы -```kotlin -import kotlinx.coroutines.* - -suspend fun retry(times: Int, block: suspend () -> String): String { - repeat(times - 1) { attempt -> - try { - return block() - } catch (e: Exception) { - println("попытка ${attempt + 1}: ${e.message}") - delay(10) - } - } - return block() -} - -fun main() = runBlocking { - var counter = 0 - val result = retry(3) { - counter++ - delay(5) - if (counter < 3) error("не готово") else "успех" - } - println(result) - // попытка 1: не готово - // попытка 2: не готово - // успех -} -``` -Тип `suspend () -> String` - функциональный тип, внутри которого можно приостанавливаться. Без `suspend` в типе параметра `delay` внутри лямбды не скомпилируется. - -Для лямбды с получателем аналогично: `suspend CoroutineScope.() -> Unit` - именно так объявлены `launch` и `runBlocking`. - -## Приостановка без блокировки: suspendCancellableCoroutine -Это мост от колбэчного API к `suspend`-функции - основной способ интегрировать существующие библиотеки. -```kotlin -import kotlinx.coroutines.* -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException - -// колбэчный API, который нужно обернуть -fun legacyLoad(onSuccess: (String) -> Unit, onError: (Throwable) -> Unit) { - Thread { - Thread.sleep(50) - onSuccess("результат из колбэка") - }.start() -} - -suspend fun load(): String = suspendCancellableCoroutine { cont -> - legacyLoad( - onSuccess = { cont.resume(it) }, - onError = { cont.resumeWithException(it) }, - ) -} - -fun main() = runBlocking { - println(load()) // результат из колбэка -} -``` -Правила: `resume` или `resumeWithException` нужно вызвать **ровно один раз**. Повторный вызов бросит `IllegalStateException`. - -Отмену нужно поддержать явно: -```kotlin -import kotlinx.coroutines.* -import java.util.concurrent.CompletableFuture -import kotlin.coroutines.resume - -suspend fun awaitFuture(future: CompletableFuture): String = - suspendCancellableCoroutine { cont -> - future.whenComplete { value, error -> - if (error != null) cont.cancel(error) else cont.resume(value) - } - cont.invokeOnCancellation { future.cancel(true) } // освобождаем ресурс - } - -fun main() = runBlocking { - val future = CompletableFuture.supplyAsync { - Thread.sleep(30) - "из future" - } - println(awaitFuture(future)) // из future -} -``` -`invokeOnCancellation` - то, что отличает корректную обёртку от утечки: при отмене корутины нижележащий запрос тоже нужно отменить. Подробнее: [Отмена и таймауты](Отмена%20и%20таймауты.md) - -## Последовательность и параллельность -`suspend`-функции по умолчанию выполняются последовательно: -```kotlin -import kotlinx.coroutines.* -import kotlin.system.measureTimeMillis - -suspend fun step(name: String): String { delay(100); return name } - -fun main() = runBlocking { - val sequential = measureTimeMillis { - val a = step("a") - val b = step("b") - println("$a$b") // ab - } - println("последовательно около 200 мс: ${sequential >= 200}") // последовательно около 200 мс: true - - val parallel = measureTimeMillis { - coroutineScope { - val a = async { step("a") } - val b = async { step("b") } - println("${a.await()}${b.await()}") // ab - } - } - println("параллельно около 100 мс: ${parallel < 180}") // параллельно около 100 мс: true -} -``` -Это сознательное решение: последовательность - разумное поведение по умолчанию, параллельность требуется явно запросить через `async`. Подробнее: [Билдеры корутин](Билдеры%20корутин.md) - -## Правила проектирования suspend-API -| Правило | Почему | -|---|---| -| `suspend fun` не должна блокировать поток | иначе теряется весь смысл | -| смена диспетчера - внутри функции, а не у вызывающего | вызывающий не должен знать, I/O это или CPU | -| функция должна быть безопасна для отмены | иначе отмена не работает или течёт | -| возвращать значение, а не `Deferred` | параллельность - дело вызывающего | -| `suspend` + возврат `Flow` обычно излишни | `Flow` сам по себе холодный | - -Правило про диспетчер - самое практичное: -```kotlin -import kotlinx.coroutines.* - -// хорошо: вызывающему не нужно думать о потоках -suspend fun readConfig(): String = withContext(Dispatchers.IO) { - "содержимое файла" // здесь был бы блокирующий File.readText -} - -fun main() = runBlocking { - println(readConfig()) // содержимое файла -} -``` -Такая функция называется main-safe: её можно вызвать с любого потока, включая UI. - -## suspend и наследование -```kotlin -import kotlinx.coroutines.* - -interface Repository { - suspend fun find(id: Int): String? -} - -class RemoteRepository : Repository { - override suspend fun find(id: Int): String? { - delay(10) - return if (id == 1) "найдено" else null - } -} - -class CachedRepository : Repository { - private val cache = mapOf(1 to "из кеша") - override suspend fun find(id: Int): String? = cache[id] // без приостановки -} - -fun main() = runBlocking { - println(RemoteRepository().find(1)) // найдено - println(CachedRepository().find(1)) // из кеша - println(CachedRepository().find(2)) // null -} -``` -`suspend` в интерфейсе - правильный способ объявить операцию, которая **может** быть асинхронной. Реализация решает, приостанавливаться ли. Обратное невозможно: не-suspend метод интерфейса нельзя переопределить как `suspend`. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `CompletableFuture f()` | `suspend fun f(): T` | -> | `void f(Callback cb)` | `suspend fun f(): T` | -> | `f().thenApply(x -> ...)` | `val x = f()` | -> | `CompletionStage` в сигнатуре | `suspend` в модификаторе | -> | блокирующий `f()` + пул потоков | `suspend` + `withContext(IO)` | -> | нет аналога | `Continuation` | -> -> **Асинхронность в типе возврата против модификатора.** В Java асинхронная функция возвращает обёртку: `CompletableFuture`. Это заражает тип: `List>` приходится разворачивать через `allOf`, а вложенные вызовы дают `CompletableFuture>` и `thenCompose` вместо `thenApply`. В Kotlin тип возврата остаётся обычным (`User`), а асинхронность выражается модификатором: -> ```kotlin -> import kotlinx.coroutines.* -> -> suspend fun loadOne(id: Int): String { delay(10); return "u$id" } -> -> suspend fun loadMany(ids: List): List = coroutineScope { -> ids.map { async { loadOne(it) } }.awaitAll() -> } -> -> fun main() = runBlocking { println(loadMany(listOf(1, 2, 3))) } // [u1, u2, u3] -> ``` -> В Java это `CompletableFuture.allOf(...)` с ручной сборкой результатов и приведением типов. -> -> **`Continuation` - это обобщённый коллбэк.** В сущности `suspend fun f(): T` компилируется в `f(Continuation)`, то есть в тот же колбэчный стиль, который в Java пишут руками. Разница в том, что преобразование делает компилятор, поэтому исходник остаётся линейным, а в Java программист пишет лямбды сам и получает «callback hell» при вложенности. -> -> **Из Java `suspend`-функцию вызвать неудобно.** Она видна как метод с лишним параметром `Continuation`, возвращающий `Object`. Для интеграции есть готовые мосты: `future { }` из `kotlinx-coroutines-jdk8` возвращает `CompletableFuture`, а `runBlocking` даёт синхронный вызов. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md) -> -> **Виртуальные потоки убирают необходимость в модификаторе.** В Java 21 блокирующий `loadOne(id)` на виртуальном потоке масштабируется без изменения сигнатуры - это главный плюс Loom: существующий код работает как есть. Цена - компилятор не знает, где функция блокируется, поэтому нет ни структурированной отмены по дереву, ни проверки «не блокируй UI-поток» на этапе компиляции. -> -> Сравнить: [Многопоточность в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md) - -> [!warning] Подводные камни -> **Блокировка внутри `suspend` сводит корутины на нет:** -> ```kotlin -> import kotlinx.coroutines.* -> -> // плохо: функция объявлена suspend, но блокирует поток -> suspend fun badRead(): String { -> Thread.sleep(50) -> return "заблокировал поток" -> } -> -> // хорошо: блокирующий код в подходящем диспетчере -> suspend fun goodRead(): String = withContext(Dispatchers.IO) { -> Thread.sleep(50) -> "поток из IO-пула" -> } -> -> fun main() = runBlocking { -> println(badRead()) -> println(goodRead()) -> // заблокировал поток -> // поток из IO-пула -> } -> ``` -> Компилятор это не поймает: `suspend` не запрещает блокировку. Ошибка проявится как «приложение зависает под нагрузкой». -> -> **Повторный `resume` бросает исключение:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlin.coroutines.resume -> -> suspend fun broken(): String = suspendCancellableCoroutine { cont -> -> cont.resume("первый") -> // cont.resume("второй") // IllegalStateException: Already resumed -> } -> -> fun main() = runBlocking { println(broken()) } // первый -> ``` -> Колбэчные API, которые могут вызвать коллбэк дважды (например, при ретраях внутри), требуют защиты флагом или `isActive`-проверки. -> -> **Обёртка без `invokeOnCancellation` течёт.** При отмене корутины нижележащий запрос продолжит работу: соединение останется открытым, таймер - запущенным, файл - читаемым. Всегда освобождайте ресурс в `invokeOnCancellation`. -> -> **`suspendCoroutine` без `Cancellable` игнорирует отмену:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> val job = launch { -> try { -> withTimeout(30) { delay(1000) } -> } catch (e: TimeoutCancellationException) { -> println("таймаут сработал: ${e::class.simpleName}") -> } -> } -> job.join() -> } -> ``` -> `delay` отменяем, поэтому таймаут работает. А вот `suspendCoroutine { }` (без `Cancellable`) приостановится навсегда и таймаут его не прервёт - используйте `suspendCancellableCoroutine`. -> -> **`suspend` поднимается по всему стеку вызовов.** Добавить приостановку в глубоко вложенную функцию означает помечать `suspend` всю цепочку вверх. Если наверху - не-suspend интерфейс (например, `Comparator` или `equals`), сделать это невозможно, и придётся либо менять архитектуру, либо блокировать через `runBlocking`. -> -> **`suspend` нельзя использовать в конструкторе, геттере свойства и `init`:** -> ```kotlin -> import kotlinx.coroutines.* -> -> class Service { -> // suspend init — невозможно -> // val value: String get() = loadValue() // геттер не может быть suspend -> -> suspend fun loadValue(): String { delay(10); return "значение" } -> } -> -> fun main() = runBlocking { println(Service().loadValue()) } // значение -> ``` -> Для асинхронной инициализации используют отдельную `suspend fun init()` или `lazy`-обёртку через `Deferred`. -> -> **Нельзя переопределить обычный метод как `suspend`.** Если интерфейс объявлен в Java (`Runnable`, `Callable`, `Comparator`), асинхронность туда не поместится - только `runBlocking` внутри, что блокирует поток. -> -> **Стектрейсы обрезаны.** Исключение из корутины содержит кадры только текущей точки возобновления, а не всю логическую цепочку. Для отладки включают `-Dkotlinx.coroutines.debug` или добавляют `kotlinx-coroutines-debug`, которые восстанавливают историю приостановок. -> -> **Рекурсивная `suspend`-функция создаёт объект продолжения на каждый кадр:** -> ```kotlin -> import kotlinx.coroutines.* -> -> suspend fun countDown(n: Int): Int = if (n <= 0) 0 else countDown(n - 1) + 1 -> -> fun main() = runBlocking { -> println(countDown(1000)) // 1000 -> } -> ``` -> Глубина ограничена не стеком JVM, а памятью, но на десятках тысяч кадров это заметная аллокация. Хвостовая рекурсия (`tailrec`) с `suspend` не сочетается. -> -> **`suspend` без приостановок - лишний.** Он заставляет всех вызывающих быть `suspend`, не давая ничего взамен. Компилятор предупреждает; предупреждение стоит слушать. - -Далее: [Структурированная конкурентность](Структурированная%20конкурентность.md) diff --git a/Kotlin/Корутины/Билдеры корутин.md b/Kotlin/Корутины/Билдеры корутин.md deleted file mode 100644 index f0a9f8f..0000000 --- a/Kotlin/Корутины/Билдеры корутин.md +++ /dev/null @@ -1,417 +0,0 @@ ---- -title: Билдеры корутин -description: launch, async, runBlocking, withContext, produce и actor - что выбрать и чем они различаются -tags: - - kotlin - - coroutines ---- - -Билдер - функция, которая создаёт корутину. Их немного, и выбор между ними определяется двумя вопросами: нужен ли результат и нужна ли новая корутина вообще. - -| Билдер | Возвращает | Блокирует поток | Нужен результат | -|---|---|---|---| -| `launch` | `Job` | нет | нет | -| `async` | `Deferred` | нет | да | -| `runBlocking` | `T` | да | да | -| `withContext` | `T` | нет | да, смена контекста | -| `produce` | `ReceiveChannel` | нет | поток значений | -| `actor` | `SendChannel` | нет | обработка сообщений | - -## launch: запустить и не ждать результата -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val job = launch { - delay(50) - println("работа выполнена") - } - println("launch вернулся сразу: ${job.isActive}") - job.join() - println("дождались") - // launch вернулся сразу: true - // работа выполнена - // дождались -} -``` -`launch` возвращает `Job` - дескриптор для `join()`, `cancel()` и проверки состояния. Результат тела отбрасывается: тип всегда `Job`, даже если блок что-то вычислил. - -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val job = launch { 42 } // значение 42 теряется - job.join() - println(job::class.simpleName != null) // true -} -``` - -## async: запустить и получить результат -```kotlin -import kotlinx.coroutines.* -import kotlin.system.measureTimeMillis - -suspend fun loadPrice(id: Int): Int { delay(100); return id * 10 } - -fun main() = runBlocking { - val time = measureTimeMillis { - val a = async { loadPrice(1) } - val b = async { loadPrice(2) } - println("сумма: ${a.await() + b.await()}") // сумма: 30 - } - println("параллельно: ${time < 180}") // параллельно: true -} -``` -`Deferred` - это `Job` с результатом: у него есть и `join()`, и `await()`. Вызов `await()` приостанавливает корутину до готовности значения. - -`await` можно вызывать повторно - результат кешируется: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - var calls = 0 - val d = async { calls++; delay(20); "значение" } - println(d.await()) // значение - println(d.await()) // значение - println("тело выполнилось раз: $calls") // тело выполнилось раз: 1 -} -``` - -Ленивый запуск - тело не начнётся до `await()` или `start()`: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val lazy = async(start = CoroutineStart.LAZY) { - println("тело стартовало") - "результат" - } - println("создан, но не запущен") - delay(20) - println(lazy.await()) - // создан, но не запущен - // тело стартовало - // результат -} -``` -Внимание: ленивый `async`, у которого так и не вызвали `await()`, никогда не выполнится и заблокирует завершение области до её отмены. - -## runBlocking: мост из синхронного кода -```kotlin -import kotlinx.coroutines.* - -suspend fun compute(): Int { delay(20); return 42 } - -fun main() { - val result = runBlocking { compute() } // обычная функция вызывает suspend - println(result) // 42 -} -``` -`runBlocking` блокирует текущий поток до завершения корутины и всех её детей. Это единственный билдер, который **не** требует scope - он его создаёт. - -Применимость: -| Место | Уместен ли `runBlocking` | -|---|---| -| функция `main` | да | -| тесты (если нет `runTest`) | да | -| CLI-утилита | да | -| обработчик HTTP-запроса | нет, фреймворк даёт scope | -| Android UI-поток | категорически нет | -| внутри другой корутины | нет, это блокировка потока | - -## withContext: сменить контекст без новой корутины -```kotlin -import kotlinx.coroutines.* - -suspend fun readFile(): String = withContext(Dispatchers.IO) { - Thread.sleep(20) // здесь блокирующий вызов допустим - "содержимое" -} - -fun main() = runBlocking { - println(readFile()) // содержимое -} -``` -`withContext` не создаёт новую корутину - он переключает контекст текущей и возвращает значение блока. Это главный инструмент для «main-safe» функций. - -Разница с `async { }.await()` существенна: -```kotlin -import kotlinx.coroutines.* - -suspend fun viaWithContext(): Int = withContext(Dispatchers.Default) { 1 } - -suspend fun viaAsync(): Int = coroutineScope { - async(Dispatchers.Default) { 1 }.await() // лишняя корутина -} - -fun main() = runBlocking { - println("${viaWithContext()} ${viaAsync()}") // 1 1 -} -``` -Результат одинаков, но `async().await()` создаёт объект `Deferred` и корутину зря. Правило: если результат нужен **сразу**, это `withContext`; `async` оправдан только когда между запуском и `await` есть другая работа. - -## Параллельная декомпозиция -```kotlin -import kotlinx.coroutines.* - -data class Dashboard(val user: String, val orders: List, val balance: Int) - -suspend fun fetchUser(): String { delay(30); return "Иван" } -suspend fun fetchOrders(): List { delay(40); return listOf("заказ 1") } -suspend fun fetchBalance(): Int { delay(20); return 1000 } - -suspend fun loadDashboard(): Dashboard = coroutineScope { - val user = async { fetchUser() } - val orders = async { fetchOrders() } - val balance = async { fetchBalance() } - Dashboard(user.await(), orders.await(), balance.await()) -} - -fun main() = runBlocking { - println(loadDashboard()) - // Dashboard(user=Иван, orders=[заказ 1], balance=1000) -} -``` -Это каноничный шаблон: `coroutineScope` + несколько `async` + сборка результата. Время работы - максимум из трёх, а не сумма; при ошибке любого запроса остальные отменяются. - -## CoroutineStart -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - launch(start = CoroutineStart.DEFAULT) { println("3: обычный, после текущего кода") } - launch(start = CoroutineStart.UNDISPATCHED) { println("1: сразу, до первой приостановки") } - val lazy = launch(start = CoroutineStart.LAZY) { println("4: только после start()") } - println("2: тело main") - lazy.start() - delay(20) - // 1: сразу, до первой приостановки - // 2: тело main - // 3: обычный, после текущего кода - // 4: только после start() -} -``` -| Значение | Поведение | -|---|---| -| `DEFAULT` | запланировать запуск согласно диспетчеру | -| `LAZY` | не запускать до `start()`, `join()` или `await()` | -| `ATOMIC` | запустить, игнорируя отмену до первой точки приостановки | -| `UNDISPATCHED` | выполнять в текущем потоке до первой приостановки | - -`UNDISPATCHED` используют, когда важно выполнить начало тела немедленно (например, зарегистрировать подписку до возврата из функции). Остальные значения нужны редко. - -## produce и actor -`produce` - билдер, возвращающий канал с результатами: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* - -fun CoroutineScope.numbers(): ReceiveChannel = produce { - for (i in 1..3) { - delay(10) - send(i) - } -} - -fun main() = runBlocking { - val channel = numbers() - for (value in channel) print("$value ") - println() // 1 2 3 -} -``` -`actor` - обратное: корутина, принимающая сообщения: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* - -sealed interface Msg { - data class Add(val value: Int) : Msg - data class GetSum(val response: CompletableDeferred) : Msg -} - -fun CoroutineScope.counter() = actor { - var sum = 0 - for (msg in channel) { - when (msg) { - is Msg.Add -> sum += msg.value - is Msg.GetSum -> msg.response.complete(sum) - } - } -} - -fun main() = runBlocking { - val c = counter() - c.send(Msg.Add(10)) - c.send(Msg.Add(5)) - val response = CompletableDeferred() - c.send(Msg.GetSum(response)) - println(response.await()) // 15 - c.close() -} -``` -Оба помечены `@ObsoleteCoroutinesApi` - их планируют заменить. Для потоков значений используйте [Flow](Flow.md), для разделяемого состояния - [Mutex или StateFlow](Разделяемое%20состояние.md). - -## Как выбирать -```mermaid -graph TD - A["Нужна корутина"] --> B{"Вы в suspend-функции
или в scope?"} - B -->|"нет, обычный код"| C["runBlocking"] - B -->|"да"| D{"Нужен результат?"} - D -->|"нет, fire-and-forget"| E["launch"] - D -->|"да"| F{"Нужен прямо сейчас?"} - F -->|"да, просто другой поток"| G["withContext"] - F -->|"нет, сначала другая работа"| H["async + await"] -``` - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `executor.execute(task)` | `launch` | -> | `executor.submit(task)` -> `Future` | `async` -> `Deferred` | -> | `future.get()` (блокирует) | `deferred.await()` (приостанавливает) | -> | `CompletableFuture.supplyAsync` | `async` | -> | `future.get()` в `main` | `runBlocking` | -> | `CompletableFuture.allOf` | `awaitAll` | -> | явная передача пула в каждый вызов | контекст наследуется | -> -> **`await()` приостанавливает, `get()` блокирует.** Это ключевая разница. `Future.get()` занимает поток до готовности результата, поэтому в пуле из 8 потоков 8 ожидающих задач полностью его парализуют - классический deadlock при вложенных `get()`. `Deferred.await()` освобождает поток, поэтому миллион ожидающих корутин живут в нескольких потоках. -> -> **Контекст наследуется, а не передаётся.** В Java пул приходится протаскивать в каждый вызов: `supplyAsync(task, executor)`, и забытый второй аргумент молча отправит задачу в `ForkJoinPool.commonPool`. В Kotlin дочерняя корутина наследует контекст родителя: -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> withContext(Dispatchers.IO) { -> launch { -> val name = Thread.currentThread().name -> val onIo = name.contains("DefaultDispatcher") -> println("ребёнок унаследовал IO: $onIo") -> } -> } -> // ребёнок унаследовал IO: true -> } -> ``` -> -> **`CompletableFuture` комбинируется методами, корутины - кодом.** Java предлагает `thenCombine`, `allOf`, `anyOf`, `thenCompose` - по методу на каждый случай. В Kotlin сбор результатов пишется обычным кодом (`listOf(a.await(), b.await())`), поэтому `if`, циклы и `try/catch` работают там естественно. -> -> **Нет аналога `runBlocking` в Java, потому что он не нужен:** `future.get()` и так блокирует. Обратная ситуация - в Kotlin `runBlocking` нужен именно как явная граница между блокирующим и приостанавливаемым миром, и эта явность полезна: видно, где поток встаёт. -> -> **`launch` против `submit` в обработке ошибок.** Исключение в `submit` попадает в `Future` и теряется, если не вызвать `get()`. Исключение в `launch` уходит родителю и обязательно будет обработано - либо родителем, либо `CoroutineExceptionHandler`, либо уронит приложение. Терять ошибки молча корутины не позволяют. Подробнее: [Исключения в корутинах](Исключения%20в%20корутинах.md) -> -> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md) - -> [!warning] Подводные камни -> **`runBlocking` внутри корутины блокирует поток диспетчера:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> withContext(Dispatchers.Default.limitedParallelism(1)) { -> launch { -> runBlocking { delay(50) } // держит единственный поток -> println("первая закончила") -> } -> launch { println("вторая ждала освобождения потока") } -> } -> // первая закончила -> // вторая ждала освобождения потока -> } -> ``` -> На ограниченном диспетчере это прямой путь к deadlock. Внутри корутины нужен `withContext`, а не `runBlocking`. -> -> **`async` без `await` проглатывает исключение до конца области:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> val result = runCatching { -> coroutineScope { -> async { throw IllegalStateException("ошибка в async") } -> delay(20) -> "тело завершилось" -> } -> } -> println(result.exceptionOrNull()?.message) // ошибка в async -> } -> ``` -> Ошибка не исчезла - она отменила область при завершении. Но заметить её в месте возникновения невозможно, а если `async` висит в `SupervisorJob`, она потеряется совсем. -> -> **Ленивый `async` без `await` подвешивает область навсегда:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> val job = launch { -> async(start = CoroutineStart.LAZY) { "никогда не выполнится" } -> println("тело launch закончилось") -> } -> val finished = withTimeoutOrNull(200) { job.join(); true } ?: false -> println("launch завершился: $finished") -> job.cancel() -> // тело launch закончилось -> // launch завершился: false -> } -> ``` -> Ленивый `Deferred` остаётся незавершённым ребёнком, а родитель обязан дождаться всех детей - поэтому `launch` не завершится никогда, даже после выхода из тела. Без `job.cancel()` программа повиснет. Это тихая утечка: ни ошибки, ни предупреждения. -> -> **`async` последовательно - это не параллельно:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlin.system.measureTimeMillis -> -> suspend fun slow(): Int { delay(100); return 1 } -> -> fun main() = runBlocking { -> val wrong = measureTimeMillis { -> val a = async { slow() }.await() // await сразу — ждём здесь же -> val b = async { slow() }.await() -> println(a + b) // 2 -> } -> println("последовательно: ${wrong >= 200}") // последовательно: true -> } -> ``` -> `await()` сразу после `async` убивает параллельность. Запускайте все `async`, потом ждите. -> -> **`launch` теряет результат молча.** Тип `Job` не содержит значения, поэтому `launch { computeSomething() }` компилируется и ничего не делает с результатом. Компилятор не предупредит. -> -> **`withContext` на каждый мелкий вызов стоит дорого:** -> ```kotlin -> import kotlinx.coroutines.* -> -> suspend fun badLoop(): Int = withContext(Dispatchers.Default) { -> var sum = 0 -> repeat(1000) { sum += it } // одно переключение — правильно -> sum -> } -> -> fun main() = runBlocking { -> println(badLoop()) // 499500 -> } -> ``` -> Переключение контекста - это передача задачи в другой пул с синхронизацией. Выносите `withContext` наружу цикла, а не внутрь. -> -> **`runBlocking` в Android UI-потоке вызывает ANR.** Это самая частая серьёзная ошибка: UI замирает до конца корутины. В Android используйте `lifecycleScope`/`viewModelScope`. -> -> **`produce` и `actor` устарели.** Они помечены `@ObsoleteCoroutinesApi`, их API может измениться. Для новых проектов - `Flow` и `Channel` напрямую, либо `Mutex` для состояния. -> -> **`CoroutineStart.ATOMIC` игнорирует отмену:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> @OptIn(ExperimentalCoroutinesApi::class) -> val job = launch(start = CoroutineStart.ATOMIC) { -> println("тело всё равно начнётся") -> delay(100) -> println("а это уже нет") -> } -> job.cancel() -> job.join() -> // тело всё равно начнётся -> } -> ``` -> Начало тела выполнится даже после `cancel()`. Это нужно для гарантированного освобождения ресурсов, но легко превращается в сюрприз. -> -> **Возврат `Deferred` из публичной функции ломает структурированность** - вызывающий может не вызвать `await()`, и ошибка потеряется. Возвращайте значение из `suspend`-функции. - -Далее: [Диспетчеры и контекст](Диспетчеры%20и%20контекст.md) diff --git a/Kotlin/Корутины/Диспетчеры и контекст.md b/Kotlin/Корутины/Диспетчеры и контекст.md deleted file mode 100644 index 59234fe..0000000 --- a/Kotlin/Корутины/Диспетчеры и контекст.md +++ /dev/null @@ -1,404 +0,0 @@ ---- -title: Диспетчеры и контекст -description: CoroutineContext как набор элементов, выбор между Default, IO и Main, limitedParallelism и наследование контекста -tags: - - kotlin - - coroutines ---- - -`CoroutineContext` - неизменяемый набор элементов, описывающий, как и где выполняется корутина. Диспетчер - один из этих элементов, отвечающий за поток. - -```kotlin -import kotlinx.coroutines.* -import kotlin.coroutines.ContinuationInterceptor - -fun main() = runBlocking { - println(coroutineContext[ContinuationInterceptor] != null) // true - println(coroutineContext[Job] != null) // true - println(coroutineContext[CoroutineName]) // null — имени не задали - - withContext(CoroutineName("загрузка")) { - println(coroutineContext[CoroutineName]?.name) // загрузка - } -} -``` -Элементы складываются оператором `+`, а извлекаются по ключу как из `Map`. Ключ диспетчера называется `ContinuationInterceptor` - это более общее понятие «перехватчик продолжений», частным случаем которого является диспетчер. Ключ `CoroutineDispatcher` тоже существует, но помечен `@ExperimentalStdlibApi`. - -## Из чего состоит контекст -| Элемент | Зачем | -|---|---| -| `Job` | жизненный цикл, иерархия, отмена | -| `CoroutineDispatcher` | на каком потоке выполнять | -| `CoroutineName` | имя для отладки и логов | -| `CoroutineExceptionHandler` | что делать с необработанной ошибкой | - -```kotlin -import kotlinx.coroutines.* -import kotlin.coroutines.ContinuationInterceptor - -fun main() = runBlocking { - val context = Dispatchers.IO + CoroutineName("задача") + SupervisorJob() - - withContext(context) { - println(coroutineContext[CoroutineName]?.name) // задача - println(coroutineContext[ContinuationInterceptor] == Dispatchers.IO) // true - } -} -``` -Сложение не коммутативно по смыслу: при конфликте элементов одного типа побеждает **правый**. -```kotlin -import kotlinx.coroutines.* -import kotlin.coroutines.CoroutineContext - -fun main() { - val base: CoroutineContext = Dispatchers.IO + CoroutineName("первое") - val combined = base + CoroutineName("второе") - println(combined[CoroutineName]?.name) // второе -} -``` -Для двух диспетчеров подряд (`Dispatchers.IO + Dispatchers.Default`) компилятор выдаёт предупреждение о бессмысленности выражения - правило то же, побеждает правый, но писать так незачем. - -## Стандартные диспетчеры -| Диспетчер | Потоков | Для чего | -|---|---|---| -| `Dispatchers.Default` | по числу ядер (минимум 2) | вычисления, обработка данных | -| `Dispatchers.IO` | до 64 (растёт по требованию) | блокирующий I/O: файлы, JDBC, legacy-HTTP | -| `Dispatchers.Main` | один, платформенный | UI (Android, Swing, JavaFX) | -| `Dispatchers.Unconfined` | любой, текущий | редкие случаи, не для продакшена | - -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - println("runBlocking: ${Thread.currentThread().name}") - - withContext(Dispatchers.Default) { - println("Default: ${Thread.currentThread().name.contains("DefaultDispatcher")}") - } - withContext(Dispatchers.IO) { - println("IO: ${Thread.currentThread().name.contains("DefaultDispatcher")}") - } - // runBlocking: main - // Default: true - // IO: true -} -``` -Обратите внимание: и `Default`, и `IO` показывают `DefaultDispatcher-worker-N` - они **делят один пул потоков**. `IO` - это тот же пул с другим лимитом параллелизма, поэтому переключение между `Default` и `IO` дешёвое и не требует смены потока. - -## Default против IO -Выбор определяется одним вопросом: код считает или ждёт? - -```kotlin -import kotlinx.coroutines.* - -// CPU-bound: больше потоков, чем ядер, только вредит -suspend fun compute(): Long = withContext(Dispatchers.Default) { - (1..1_000_000).sumOf { it.toLong() } -} - -// блокирующий I/O: поток простаивает, поэтому их нужно много -suspend fun readBlocking(): String = withContext(Dispatchers.IO) { - Thread.sleep(20) // имитация JDBC или File.readText - "прочитано" -} - -fun main() = runBlocking { - println(compute()) // 500000500000 - println(readBlocking()) // прочитано -} -``` -Логика лимитов: на `Default` потоков столько же, сколько ядер, потому что больше вычислять всё равно некому - лишние потоки только добавят переключений контекста. На `IO` потоки в основном спят в ожидании ответа, поэтому их держат до 64. - -Если API уже `suspend` и не блокирует (Ktor-клиент, R2DBC, `kotlinx-io`), диспетчер менять не нужно вообще - это лишнее переключение. - -## limitedParallelism -Ограничивает число одновременных корутин, не создавая новых потоков: -```kotlin -import kotlinx.coroutines.* -import java.util.concurrent.atomic.AtomicInteger - -fun main() = runBlocking { - val limited = Dispatchers.IO.limitedParallelism(2) - val running = AtomicInteger(0) - val maxRunning = AtomicInteger(0) - - withContext(limited) { - repeat(10) { - launch { - val now = running.incrementAndGet() - maxRunning.updateAndGet { m -> maxOf(m, now) } - Thread.sleep(20) // занимает поток - running.decrementAndGet() - } - } - } - println("одновременно выполнялось не больше 2: ${maxRunning.get() <= 2}") - // одновременно выполнялось не больше 2: true -} -``` -Это замена старому `newFixedThreadPoolContext`: вместо отдельного пула используется общий, а ограничивается только параллелизм. Типичное применение - не перегрузить базу данных или внешний API. - -Важная тонкость: лимит считает корутины, **занимающие поток**, а не существующие. Приостановленная корутина слот освобождает: -```kotlin -import kotlinx.coroutines.* -import java.util.concurrent.atomic.AtomicInteger - -fun main() = runBlocking { - val limited = Dispatchers.IO.limitedParallelism(2) - val inFlight = AtomicInteger(0) - val maxInFlight = AtomicInteger(0) - - withContext(limited) { - repeat(10) { - launch { - val now = inFlight.incrementAndGet() - maxInFlight.updateAndGet { m -> maxOf(m, now) } - delay(20) // слот освобождён - inFlight.decrementAndGet() - } - } - } - println("одновременно в полёте больше 2: ${maxInFlight.get() > 2}") - // одновременно в полёте больше 2: true -} -``` -Поэтому `limitedParallelism` - не семафор на число запросов. Если нужно ограничить именно количество одновременных обращений к внешнему API (включая время ожидания ответа), нужен `Semaphore` из `kotlinx.coroutines.sync`. Подробнее: [Разделяемое состояние](Разделяемое%20состояние.md) - -## Dispatchers.Main -```kotlin -// Android или Swing — требует соответствующей зависимости -// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0") -// -// viewModelScope.launch { // уже на Main -// val data = withContext(Dispatchers.IO) { repository.load() } -// textView.text = data // снова на Main -// } -``` -Главный шаблон UI-программирования: корутина стартует на `Main`, тяжёлую работу уводит в `withContext`, результат обрабатывает снова на `Main` - без коллбэков и `runOnUiThread`. Без платформенной зависимости `Dispatchers.Main` бросит `IllegalStateException` при первом обращении. - -## Unconfined -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - withContext(Dispatchers.Unconfined) { - println("до delay: ${Thread.currentThread().name}") - delay(10) - println("после delay на потоке таймера: ${Thread.currentThread().name.contains("DefaultExecutor")}") - } - // до delay: main - // после delay на потоке таймера: true -} -``` -`Unconfined` не привязывает корутину к потоку: до первой приостановки она идёт в текущем, а после возобновляется в том потоке, который её разбудил. Это ломает любые предположения о потоках, поэтому в продакшене не используется - только в тестах и специальных случаях. - -## Наследование контекста -Дочерняя корутина наследует контекст родителя, заменяя только `Job`: -```kotlin -import kotlinx.coroutines.* -import kotlin.coroutines.ContinuationInterceptor - -fun main() = runBlocking { - withContext(Dispatchers.IO + CoroutineName("родитель")) { - launch { - println("имя: ${coroutineContext[CoroutineName]?.name}") // имя: родитель - val inherited = coroutineContext[ContinuationInterceptor] == Dispatchers.IO - println("диспетчер унаследован: $inherited") // диспетчер унаследован: true - } - launch(CoroutineName("ребёнок")) { - println("переопределено: ${coroutineContext[CoroutineName]?.name}") // переопределено: ребёнок - } - } -} -``` -`Job` ребёнка всегда новый и становится дочерним по отношению к `Job` родителя - именно так строится дерево. Подробнее: [Структурированная конкурентность](Структурированная%20конкурентность.md) - -```mermaid -graph TD - A["Контекст родителя:
Job#1 + Dispatchers.IO + Name"] --> B["launch { }"] - B --> C["Контекст ребёнка:
Job#2 (parent = Job#1)
+ Dispatchers.IO + Name"] - A --> D["launch(Dispatchers.Default) { }"] - D --> E["Контекст ребёнка:
Job#3 (parent = Job#1)
+ Dispatchers.Default + Name"] -``` - -## Свои элементы контекста -Элемент контекста - способ передать данные через всё дерево корутин: -```kotlin -import kotlinx.coroutines.* -import kotlin.coroutines.AbstractCoroutineContextElement -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.coroutineContext - -class RequestId(val value: String) : AbstractCoroutineContextElement(RequestId) { - companion object Key : CoroutineContext.Key -} - -suspend fun logWithId(message: String) { - val id = coroutineContext[RequestId]?.value ?: "нет id" - println("[$id] $message") -} - -fun main() = runBlocking { - withContext(RequestId("req-42")) { - logWithId("начало обработки") - launch { logWithId("из дочерней корутины") } - } - logWithId("вне контекста") - // [req-42] начало обработки - // [req-42] из дочерней корутины - // [нет id] вне контекста -} -``` -Это корутинный аналог `ThreadLocal`, но работающий правильно при переключении потоков. Применяется для трассировки запросов, идентификаторов пользователя, локали. - -## ThreadLocal и корутины -```kotlin -import kotlinx.coroutines.* - -val threadLocal = ThreadLocal() - -fun main() = runBlocking { - threadLocal.set("главный") - println("до: ${threadLocal.get()}") - - withContext(Dispatchers.Default) { - println("на другом потоке: ${threadLocal.get()}") // null — другой поток - } - - withContext(Dispatchers.Default + threadLocal.asContextElement("перенесённый")) { - println("через asContextElement: ${threadLocal.get()}") // перенесённый - } - // до: главный - // на другом потоке: null - // через asContextElement: перенесённый -} -``` -`ThreadLocal` привязан к потоку, а корутина потоки меняет - поэтому напрямую он не работает. `asContextElement()` переносит значение при каждом переключении. Это важно для MDC в логировании и Spring Security. - -## Проверка потока в отладке -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking(CoroutineName("корень")) { - println(coroutineContext[CoroutineName]?.name) // корень - launch(CoroutineName("дочерняя")) { - println(coroutineContext[CoroutineName]?.name) // дочерняя - }.join() -} -``` -Запуск с `-Dkotlinx.coroutines.debug` добавляет к имени потока идентификатор корутины (`main @корень#1`), что сильно упрощает чтение логов. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `ExecutorService` передаётся аргументом | диспетчер наследуется из контекста | -> | `Executors.newFixedThreadPool(n)` | `Dispatchers.Default` / `limitedParallelism(n)` | -> | `Executors.newCachedThreadPool()` | `Dispatchers.IO` | -> | `SwingUtilities.invokeLater`, `runOnUiThread` | `Dispatchers.Main` | -> | `ThreadLocal` | элемент контекста | -> | `MDC` в логировании | `MDCContext` или свой элемент | -> | пул = потоки | диспетчер = политика, потоки общие | -> -> **Пул потоков против диспетчера.** В Java каждый `ExecutorService` владеет своими потоками: четыре пула по 10 потоков - это 40 реальных потоков, даже если 35 из них простаивают. `Dispatchers.Default` и `Dispatchers.IO` делят один пул, а `limitedParallelism` ограничивает только число одновременных задач. Поэтому в Kotlin «ограничить параллелизм до 2 для работы с БД» не стоит двух простаивающих потоков. -> -> **Контекст наследуется, пул - нет.** В Java пул нужно передавать в каждый асинхронный вызов, и забытый аргумент молча уводит задачу в `ForkJoinPool.commonPool`: -> ```java -> CompletableFuture.supplyAsync(this::load); // commonPool! -> CompletableFuture.supplyAsync(this::load, myExecutor); // правильно -> ``` -> Эти две строки отличаются одним аргументом и дают совершенно разное поведение под нагрузкой. В Kotlin дочерняя корутина получает диспетчер родителя автоматически, а переопределение - явное и видимое. -> -> **`ThreadLocal` в асинхронном коде в Java не работает так же** - и там это обходят сложнее. При передаче задачи в пул `ThreadLocal` теряется; Spring решает это через `TaskDecorator` и копирование `SecurityContext`/MDC в каждом месте. В Kotlin элемент контекста путешествует вместе с корутиной по всему дереву, а `asContextElement()` переносит и настоящий `ThreadLocal`, когда он нужен библиотеке. -> -> **Переключение потока явное и дешёвое.** В Java чтобы вернуться в UI-поток, нужен `invokeLater` или `Handler.post` с коллбэком, который разрывает линейность кода. `withContext(Dispatchers.Main)` - это обычный блок, после которого выполнение продолжается на нужном потоке: -> ```kotlin -> import kotlinx.coroutines.* -> -> suspend fun loadAndRender(): String { -> val data = withContext(Dispatchers.IO) { Thread.sleep(10); "данные" } -> return "отрисовано: $data" // снова на исходном диспетчере -> } -> -> fun main() = runBlocking { println(loadAndRender()) } // отрисовано: данные -> ``` -> -> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [JVM](../../Программирование%20на%20Java/JVM/JVM.md) - -> [!warning] Подводные камни -> **Блокирующий вызов на `Dispatchers.Default` исчерпывает пул:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlin.system.measureTimeMillis -> -> fun main() = runBlocking { -> val t = measureTimeMillis { -> withContext(Dispatchers.Default.limitedParallelism(2)) { -> List(6) { launch { Thread.sleep(50) } }.forEach { it.join() } -> } -> } -> println("сериализовалось вместо параллели: ${t >= 150}") // сериализовалось вместо параллели: true -> } -> ``` -> Шесть задач по 50 мс на двух потоках заняли 150 мс вместо 50. На реальном `Default` (число ядер) блокирующий код точно так же съедает все потоки, и вся программа встаёт - включая несвязанные корутины. -> -> **`Dispatchers.IO` не делает блокирующий код неблокирующим.** Он лишь даёт больше потоков. 64 одновременных JDBC-запроса всё равно упрутся в размер пула соединений, а 1000 корутин на `IO` встанут в очередь. -> -> **`Dispatchers.Main` падает без платформенной зависимости:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() { -> val error = runCatching { runBlocking { withContext(Dispatchers.Main) { } } } -> .exceptionOrNull() -> println(error != null) // true -> } -> ``` -> Сообщение - «Module with the Main dispatcher is missing». Нужен `kotlinx-coroutines-android`, `-javafx` или `-swing`. -> -> **`newSingleThreadContext` создаёт настоящий поток, который нужно закрывать:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> @OptIn(DelicateCoroutinesApi::class) -> val dispatcher = newSingleThreadContext("мой-поток") -> try { -> withContext(dispatcher) { -> println(Thread.currentThread().name) // мой-поток -> } -> } finally { -> dispatcher.close() // без этого поток живёт до конца процесса -> } -> } -> ``` -> API помечено `@DelicateCoroutinesApi` именно из-за этой утечки. Почти всегда вместо него нужен `limitedParallelism(1)`. -> -> **`Unconfined` возобновляется в неизвестном потоке.** Код после `delay` окажется на потоке таймера, после `Channel.receive` - на потоке отправителя. Любая работа с UI или `ThreadLocal` при этом ломается непредсказуемо. -> -> **`withContext` внутри цикла - лишние переключения:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlin.system.measureTimeMillis -> -> fun main() = runBlocking { -> val inside = measureTimeMillis { -> repeat(10_000) { withContext(Dispatchers.Default) { it } } -> } -> val outside = measureTimeMillis { -> withContext(Dispatchers.Default) { repeat(10_000) { } } -> } -> println("снаружи быстрее: ${outside < inside}") // снаружи быстрее: true -> } -> ``` -> -> **Передача `Job` в `withContext` рвёт структурированность.** `withContext(Dispatchers.IO + Job())` создаёт корутину без родителя - отмена сверху до неё не дойдёт. Передавайте только диспетчер и имя. -> -> **Контекст неизменяем, `+` создаёт новый.** `context + CoroutineName("x")` не меняет исходный объект; забытое присваивание результата - тихая ошибка. -> -> **`ThreadLocal` без `asContextElement` теряется при любом переключении**, в том числе при возобновлении после `delay` на другом потоке. Для MDC в логах это значит, что идентификатор запроса исчезнет из части записей - используйте `MDCContext` из `kotlinx-coroutines-slf4j`. -> -> **Число потоков `IO` настраивается только через системное свойство** (`kotlinx.coroutines.io.parallelism`) и влияет на весь процесс. Для ограничения конкретного места используйте `limitedParallelism`. -> -> **Переключение диспетчера для неблокирующего API - чистая потеря.** Если библиотека уже `suspend` и не блокирует, `withContext(Dispatchers.IO)` вокруг неё добавляет переключение контекста и ничего не даёт. - -Далее: [Отмена и таймауты](Отмена%20и%20таймауты.md) diff --git a/Kotlin/Корутины/Исключения в корутинах.md b/Kotlin/Корутины/Исключения в корутинах.md deleted file mode 100644 index 31e4abd..0000000 --- a/Kotlin/Корутины/Исключения в корутинах.md +++ /dev/null @@ -1,529 +0,0 @@ ---- -title: Исключения в корутинах -description: Как ошибки идут по дереву, разница launch и async, SupervisorJob, CoroutineExceptionHandler и CancellationException -tags: - - kotlin - - coroutines ---- - -Обработка ошибок в корутинах отличается от обычной именно потому, что корутина - не вызов функции: исключение возникает не там, где его ждёт `try`, а внутри другой корутины. - -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val result = runCatching { - coroutineScope { - launch { throw IllegalStateException("ошибка внутри") } - } - } - println(result.exceptionOrNull()?.message) // ошибка внутри -} -``` -Ошибка из `launch` не выбрасывается в месте вызова `launch` - она идёт **вверх по дереву** и проявляется на границе области. - -## Два механизма: launch и async -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - // launch: ошибка идёт родителю сразу - val fromLaunch = runCatching { - coroutineScope { launch { error("из launch") } } - }.exceptionOrNull()?.message - - // async: ошибка хранится в Deferred и выбрасывается при await - val fromAsync = runCatching { - coroutineScope { - val d = async { error("из async") } - d.await() - } - }.exceptionOrNull()?.message - - println("$fromLaunch | $fromAsync") // из launch | из async -} -``` -Разница в том, **когда** вы увидите ошибку: - -| | `launch` | `async` | -|---|---|---| -| куда идёт ошибка | сразу родителю | сохраняется в `Deferred` | -| где ловить | `try` вокруг области, обработчик | `try` вокруг `await()` | -| если не обработать | падает область | при `coroutineScope` - тоже падает область | -| если `async` висит в supervisor | - | ошибка потеряется совсем | - -Ключевой момент: `async` в обычной области **всё равно** передаёт ошибку родителю, даже без `await()`. Исключение в этом случае попадает в родителя при завершении области. - -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val result = runCatching { - coroutineScope { - async { error("await не вызывали") } - delay(20) - "тело дошло до конца" - } - } - println(result.exceptionOrNull()?.message) // await не вызывали -} -``` -А вот в supervisor-области непрочитанная ошибка `async` исчезает: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - supervisorScope { - async { error("никто не узнает") } // await не вызван - delay(20) - println("ни падения, ни лога") - } - // ни падения, ни лога -} -``` -Это самый тихий способ потерять ошибку в корутинах. - -## Как ошибка идёт по дереву -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val result = runCatching { - coroutineScope { - launch { // уровень 1 - launch { // уровень 2 - launch { // уровень 3 - throw IllegalStateException("с третьего уровня") - } - } - } - launch { - try { delay(1000) } finally { println("соседнюю ветку отменили") } - } - } - } - println("дошло до корня: ${result.exceptionOrNull()?.message}") - // соседнюю ветку отменили - // дошло до корня: с третьего уровня -} -``` -Механика: корутина, получившая ошибку, отменяет себя, передаёт исключение родителю, тот отменяет **всех остальных детей** и повторяет процедуру выше. Промежуточные уровни не могут «не пропустить» ошибку - её нужно ловить `try/catch` внутри самой корутины. - -```mermaid -graph BT - D["Ребёнок уровня 3
throw"] -->|"ошибка наверх"| C["Ребёнок уровня 2"] - C -->|"ошибка наверх"| B["Ребёнок уровня 1"] - B -->|"ошибка наверх"| A["Корень области"] - A -->|"отменить"| E["Соседняя ветка"] - A --> F{"есть обработчик?"} - F -->|"да"| G["CoroutineExceptionHandler"] - F -->|"нет"| H["Thread.uncaughtExceptionHandler
падение"] -``` - -## Где try/catch работает -Внутри самой корутины - как обычно: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - launch { - try { - error("локальная ошибка") - } catch (e: IllegalStateException) { - println("обработали на месте: ${e.message}") - } - }.join() - println("родитель не затронут") - // обработали на месте: локальная ошибка - // родитель не затронут -} -``` -А вокруг `launch` - нет: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val job = launch(SupervisorJob()) { // отдельный Job, чтобы не уронить main - try { - launch { error("из вложенного launch") } - } catch (e: Exception) { - println("этого не будет") - } - } - job.join() - println("родитель выжил: ${job.isCancelled}") - // родитель выжил: true -} -``` -`launch` возвращается мгновенно, до того как тело начало выполняться - ловить там нечего. - -## SupervisorJob -Супервизор меняет направление: ошибка ребёнка не идёт вверх. -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val handler = CoroutineExceptionHandler { _, e -> - println("обработчик: ${e.message}") - } - val scope = CoroutineScope(SupervisorJob() + handler) - - scope.launch { error("первая задача упала") } - delay(20) - scope.launch { println("вторая работает как ни в чём не бывало") } - delay(20) - println("scope жив: ${scope.isActive}") - scope.cancel() - // обработчик: первая задача упала - // вторая работает как ни в чём не бывало - // scope жив: true -} -``` -Сравните с обычным `Job`: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val handler = CoroutineExceptionHandler { _, e -> println("обработчик: ${e.message}") } - val scope = CoroutineScope(Job() + handler) - - scope.launch { error("первая задача упала") } - delay(20) - val second = scope.launch { println("не запустится") } - delay(20) - println("scope мёртв: ${!scope.isActive}, вторая отменена: ${second.isCancelled}") - // обработчик: первая задача упала - // scope мёртв: true, вторая отменена: true -} -``` -Правило: долгоживущий scope компонента - `SupervisorJob`; группа задач внутри одной операции - обычный `Job` через `coroutineScope`. - -## supervisorScope против coroutineScope -```kotlin -import kotlinx.coroutines.* - -suspend fun loadAllOrFail(): String = coroutineScope { - val a = async { delay(20); "A" } - val b = async { error("B упал"); "B" } - "${a.await()}${b.await()}" -} - -suspend fun loadWhatCan(): List = supervisorScope { - val a = async { delay(20); "A" } - val b = async { error("B упал") } - listOf( - runCatching { a.await() }.getOrElse { "?" }, - runCatching { b.await() }.getOrElse { "?" }, - ) -} - -fun main() = runBlocking { - println(runCatching { loadAllOrFail() }.exceptionOrNull()?.message) // B упал - println(loadWhatCan()) // [A, ?] -} -``` -Первый вариант - «нужны все данные», второй - «покажем что есть». В supervisor-области каждый `await()` нужно обрабатывать отдельно, иначе ошибка улетит в обработчик по умолчанию. - -## CoroutineExceptionHandler -Это «последняя линия» - обработчик необработанных ошибок, аналог `Thread.UncaughtExceptionHandler`. -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val handler = CoroutineExceptionHandler { context, e -> - println("необработанная ошибка в ${context[CoroutineName]?.name}: ${e.message}") - } - val scope = CoroutineScope(SupervisorJob() + handler) - - scope.launch(CoroutineName("загрузчик")) { error("сеть недоступна") } - delay(20) - scope.cancel() - // необработанная ошибка в загрузчик: сеть недоступна -} -``` -Три правила, которые не очевидны: - -**1. Обработчик работает только в корневой корутине.** В дочерней он игнорируется: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val handler = CoroutineExceptionHandler { _, e -> println("вызван: ${e.message}") } - val scope = CoroutineScope(SupervisorJob()) - - scope.launch { - launch(handler) { error("ошибка в ребёнке") } // handler проигнорирован - } - delay(50) - scope.cancel() - // Exception in thread ... IllegalStateException: ошибка в ребёнке -} -``` -Ошибка ушла наверх, а наверху обработчика нет. Ставить `handler` нужно в контекст scope или корневого билдера. - -**2. `async` обработчик не использует** - его ошибка принадлежит `Deferred`: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val handler = CoroutineExceptionHandler { _, e -> println("не будет вызван") } - val scope = CoroutineScope(SupervisorJob() + handler) - - val d = scope.async { error("ошибка в async") } - delay(20) - println("ошибка ждёт в Deferred: ${runCatching { d.await() }.isFailure}") - scope.cancel() - // ошибка ждёт в Deferred: true -} -``` - -**3. `runBlocking` обработчик не использует** - он выбрасывает исключение наружу, в вызывающий поток. - -## CancellationException - не ошибка -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val job = launch { - launch { - delay(1000) - }.cancel() // отмена ребёнка - delay(20) - println("родитель продолжает работу") - } - job.join() - println("родитель завершился нормально: ${!job.isCancelled}") - // родитель продолжает работу - // родитель завершился нормально: true -} -``` -`CancellationException`, выброшенное корутиной, трактуется как «я завершилась по запросу» и наверх не передаётся. Именно поэтому `withTimeout` внутри одной корутины не роняет соседей. Подробнее: [Отмена и таймауты](Отмена%20и%20таймауты.md) - -Обратная сторона: `CancellationException` нельзя использовать как обычное исключение - оно будет молча проигнорировано: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val job = launch(SupervisorJob()) { - throw CancellationException("это не ошибка, а отмена") - } - job.join() - println("отменён, а не упал: ${job.isCancelled}") // отменён, а не упал: true -} -``` - -## Агрегация ошибок -Родитель получает **первое** исключение. Ошибки, возникшие уже во время отмены - например, в блоке `finally` соседней корутины - не теряются, а попадают в `suppressed`: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val error = runCatching { - coroutineScope { - launch { delay(10); throw IllegalStateException("первая") } - launch { - try { delay(1000) } finally { throw IllegalArgumentException("из finally") } - } - } - }.exceptionOrNull() - - println("основная: ${error?.message}") - println("подавленных: ${error?.suppressed?.size}") - println("подавленная: ${error?.suppressed?.firstOrNull()?.message}") - // основная: первая - // подавленных: 1 - // подавленная: из finally -} -``` -Важно, чего здесь **нет**: если бы вторая корутина просто бросила исключение через 30 мс, подавленной ошибки не появилось бы - её отменили бы на 10-й миллисекунде, и она никогда не дошла бы до `throw`. Fail-fast означает именно это: вторая ошибка обычно не возникает, потому что работу остановили. В `suppressed` попадают только сбои очистки. - -Практический вывод для логирования: печатать нужно полный стектрейс (`printStackTrace()` или `logger.error(msg, e)`), а не `e.message` - иначе сбои в `finally` останутся невидимыми. - -## Повторные попытки -```kotlin -import kotlinx.coroutines.* - -suspend fun retry(times: Int, delayMs: Long, block: suspend () -> T): T { - var last: Throwable? = null - repeat(times) { attempt -> - try { - return block() - } catch (e: CancellationException) { - throw e // отмену не ретраим! - } catch (e: Exception) { - last = e - println("попытка ${attempt + 1} не удалась: ${e.message}") - delay(delayMs) - } - } - throw last ?: IllegalStateException("нет попыток") -} - -fun main() = runBlocking { - var attempts = 0 - val result = retry(3, 10) { - attempts++ - if (attempts < 3) error("сеть недоступна") else "данные получены" - } - println(result) - // попытка 1 не удалась: сеть недоступна - // попытка 2 не удалась: сеть недоступна - // данные получены -} -``` -Строка `catch (e: CancellationException) { throw e }` обязательна в любом обобщённом обработчике ошибок в корутинах - без неё функция станет неотменяемой. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | исключение в `submit` → в `Future` | ошибка `launch` → родителю | -> | `Future.get()` → `ExecutionException` | `await()` → исходное исключение | -> | `CompletableFuture.exceptionally` | `try/catch` вокруг `await` | -> | `Thread.UncaughtExceptionHandler` | `CoroutineExceptionHandler` | -> | `InterruptedException` (проверяемое) | `CancellationException` (непроверяемое) | -> | ошибка задачи не влияет на пул | ошибка отменяет область | -> | нет аналога | `SupervisorJob` | -> -> **Ошибку в Java легко потерять молча.** `executor.submit(task)` складывает исключение в `Future`, и если `get()` никто не вызовет, об ошибке не узнает никто - ни логов, ни падения: -> ```java -> executor.submit(() -> { throw new RuntimeException("исчезнет"); }); -> // тишина -> ``` -> Корутины этого не допускают: ошибка `launch` обязательно дойдёт до родителя, а если никто не обработал - до `CoroutineExceptionHandler` или `Thread.uncaughtExceptionHandler`. Исключение одно - непрочитанный `async` в supervisor-области, и это считается ошибкой программиста. -> -> **`get()` оборачивает исключение, `await()` - нет.** В Java настоящая причина лежит внутри `ExecutionException`, поэтому обработка выглядит так: -> ```java -> try { -> future.get(); -> } catch (ExecutionException e) { -> if (e.getCause() instanceof IOException io) { /* ... */ } -> } -> ``` -> В Kotlin `await()` бросает то же исключение, что было выброшено внутри, поэтому `catch (e: IOException)` работает напрямую: -> ```kotlin -> import kotlinx.coroutines.* -> import java.io.IOException -> -> fun main() = runBlocking { -> val caught = try { -> coroutineScope { async { throw IOException("диск") }.await() } -> } catch (e: IOException) { -> "поймали ${e::class.simpleName}: ${e.message}" -> } -> println(caught) // поймали IOException: диск -> } -> ``` -> -> **`exceptionally`/`handle` против `try/catch`.** В Java обработка ошибок в асинхронной цепочке - отдельный метод с лямбдой, и правила распространения ошибки по цепочке приходится помнить (`exceptionally` ловит, `thenApply` пропускает, `whenComplete` не меняет результат). В Kotlin работает обычный `try/catch`, с теми же правилами, что в синхронном коде. -> -> **Отмена пула против отмены области.** В Java ошибка в одной задаче никак не влияет на остальные - пул продолжает работу. Это и плюс (изоляция), и минус (бессмысленное продолжение работы, часть которой уже провалилась). Kotlin даёт выбор явно: `coroutineScope` - fail-fast, `supervisorScope` - изоляция как в Java. -> -> **`CancellationException` непроверяемое, `InterruptedException` - нет.** В Java прерывание приходится обрабатывать в каждом блокирующем вызове, и типичная ошибка - проглотить его, не восстановив флаг. В Kotlin отмену не нужно объявлять, но проглотить её так же легко через `catch (e: Exception)`. -> -> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md), [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md) - -> [!warning] Подводные камни -> **`try/catch` вокруг `launch` ничего не ловит:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> val job = launch(SupervisorJob()) { -> try { -> launch { error("внутри") } -> } catch (e: Exception) { -> println("не напечатается") -> } -> delay(50) -> } -> job.join() -> println("ошибка ушла наверх, а не в catch") -> // ошибка ушла наверх, а не в catch -> } -> ``` -> `launch` возвращается до выполнения тела. Ловить нужно **внутри** лямбды или вокруг `coroutineScope`. -> -> **`catch (e: Exception)` перехватывает отмену:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> val job = launch { -> try { -> delay(1000) -> } catch (e: Exception) { -> println("отмена попала в общий catch: ${e is CancellationException}") -> } -> println("и работа продолжилась") -> } -> delay(20) -> job.cancelAndJoin() -> // отмена попала в общий catch: true -> // и работа продолжилась -> } -> ``` -> Всегда ставьте `catch (e: CancellationException) { throw e }` первым. -> -> **`runCatching` в корутинах опасен** по той же причине - он ловит `Throwable`, включая `CancellationException`. В `suspend`-коде это делает блок неотменяемым, и `cancel()` перестаёт работать. Используйте явный `try/catch` с пробросом отмены. -> -> **`CoroutineExceptionHandler` в дочерней корутине игнорируется.** Он учитывается только у корневой корутины области. Передавать его нужно при создании scope или в корневой `launch`. -> -> **`CoroutineExceptionHandler` не работает с `async`** - его ошибка принадлежит `Deferred` и ждёт `await()`. Если `await()` не вызван в supervisor-области, ошибка исчезает навсегда. -> -> **`SupervisorJob`, переданный в `launch`, не делает детей независимыми:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> val job = launch(SupervisorJob()) { // supervisor — для ЭТОЙ корутины -> launch { delay(10); error("первый ребёнок") } -> launch { -> try { delay(1000) } finally { println("второго тоже отменили") } -> } -> } -> job.join() -> // второго тоже отменили -> } -> ``` -> `SupervisorJob` в контексте билдера становится родителем самой корутины, но внутри её тела работает обычная область. Нужен `supervisorScope { }`. -> -> **`supervisorScope` не защищает код своего тела:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> val caught = runCatching { -> supervisorScope { -> launch { delay(1000) } -> error("ошибка в теле блока") -> } -> }.exceptionOrNull()?.message -> println(caught) // ошибка в теле блока -> } -> ``` -> Supervisor меняет поведение только для детей. -> -> **Ошибка внутри `CoroutineExceptionHandler` роняет процесс.** Обработчик - последняя линия, выше его некуда. Логирование внутри него должно быть защищено от собственных сбоев. -> -> **`finally` может не выполниться при падении JVM.** Если необработанная ошибка дошла до `Thread.uncaughtExceptionHandler` и процесс завершается, часть блоков очистки в других корутинах может не успеть. -> -> **Подавленные исключения легко не заметить.** При нескольких одновременных ошибках `e.message` покажет только первую. В логах нужен полный `printStackTrace()` или `e.suppressed`. -> -> **Проброс через несколько уровней нельзя перехватить в середине.** Промежуточный `launch` не видит ошибку ребёнка как исключение - он просто отменяется. Чтобы обработать её на этом уровне, нужен `try/catch` вокруг `coroutineScope` внутри промежуточной корутины. -> -> **`CancellationException` как бизнес-ошибка не работает:** -> ```kotlin -> import kotlinx.coroutines.* -> -> class ValidationError(message: String) : CancellationException(message) -> -> fun main() = runBlocking { -> val job = launch(SupervisorJob()) { throw ValidationError("неверные данные") } -> job.join() -> println("ошибка проигнорирована: отменён=${job.isCancelled}") -> // ошибка проигнорирована: отменён=true -> } -> ``` -> Наследование от `CancellationException` превращает исключение в «тихую отмену»: ни обработчика, ни падения. Для бизнес-ошибок наследуйтесь от `Exception`. - -Далее: [Flow](Flow.md) diff --git a/Kotlin/Корутины/Корутины против потоков.md b/Kotlin/Корутины/Корутины против потоков.md deleted file mode 100644 index a64178f..0000000 --- a/Kotlin/Корутины/Корутины против потоков.md +++ /dev/null @@ -1,344 +0,0 @@ ---- -title: Корутины против потоков -description: Реальная стоимость потока и корутины, сравнение с виртуальными потоками Java и случаи, когда нужны именно потоки -tags: - - kotlin - - coroutines ---- - -Поток - ресурс ОС: стек в памяти, запись в планировщике ядра, переключение через системный вызов. Корутина - объект в куче, переключение которого - обычный вызов метода. - -```kotlin -import kotlinx.coroutines.* -import kotlin.system.measureTimeMillis - -fun main() = runBlocking { - val coroutines = measureTimeMillis { - List(100_000) { launch { delay(100) } }.forEach { it.join() } - } - println("100 000 корутин: уложились в секунду: ${coroutines < 1000}") - // 100 000 корутин: уложились в секунду: true -} -``` -Сто тысяч потоков создать нельзя - JVM упадёт с `OutOfMemoryError` ещё на нескольких десятках тысяч. - -## Стоимость в цифрах -| Характеристика | Поток ОС | Корутина | -|---|---|---| -| память | около 1 МБ стека (`-Xss`) | десятки-сотни байт | -| создание | системный вызов, ~50-100 мкс | аллокация объекта, ~100 нс | -| переключение | через ядро, 1-10 мкс | вызов метода, ~10 нс | -| разумный максимум | тысячи | миллионы | -| планировщик | ядро ОС | библиотека, в пользовательском коде | - -```kotlin -import kotlinx.coroutines.* -import kotlin.system.measureTimeMillis - -fun main() = runBlocking { - val threadTime = measureTimeMillis { - List(1000) { Thread { Thread.sleep(10) } }.onEach { it.start() }.forEach { it.join() } - } - val coroutineTime = measureTimeMillis { - List(1000) { launch { delay(10) } }.forEach { it.join() } - } - println("1000 потоков: ${threadTime} мс, 1000 корутин: ${coroutineTime} мс") - println("корутины быстрее: ${coroutineTime < threadTime}") - // корутины быстрее: true -} -``` -Разница здесь не в самом ожидании (оба спят по 10 мс), а в накладных расходах на создание тысячи объектов ядра. - -## Где именно экономия -Корутины не делают работу быстрее - они устраняют простой потоков. - -```kotlin -import kotlinx.coroutines.* -import kotlin.system.measureTimeMillis - -fun main() = runBlocking { - // блокировка: 4 задачи на 1 потоке = 4 × 100 мс - val blocking = measureTimeMillis { - withContext(Dispatchers.Default.limitedParallelism(1)) { - List(4) { launch { Thread.sleep(100) } }.forEach { it.join() } - } - } - - // приостановка: 4 задачи на 1 потоке = 100 мс - val suspending = measureTimeMillis { - withContext(Dispatchers.Default.limitedParallelism(1)) { - List(4) { launch { delay(100) } }.forEach { it.join() } - } - } - - println("блокировка около 400 мс: ${blocking >= 380}") - println("приостановка около 100 мс: ${suspending < 200}") - // блокировка около 400 мс: true - // приостановка около 100 мс: true -} -``` -Тот же один поток, та же работа - разница в четыре раза. Именно это окупает корутины на сервере, обслуживающем много одновременных запросов: поток не стоит в ожидании ответа базы, а берёт следующий запрос. - -## Корутины не помогают при счёте -```kotlin -import kotlinx.coroutines.* -import kotlin.system.measureTimeMillis - -fun heavy(): Long = (1..20_000_000L).sum() - -fun main() = runBlocking { - val sequential = measureTimeMillis { repeat(4) { heavy() } } - - val parallel = measureTimeMillis { - withContext(Dispatchers.Default) { - List(4) { async { heavy() } }.awaitAll() - } - } - - val oneThread = measureTimeMillis { - withContext(Dispatchers.Default.limitedParallelism(1)) { - List(4) { async { heavy() } }.awaitAll() - } - } - - println("параллельно на ядрах быстрее: ${parallel < sequential}") - println("на одном потоке выигрыша нет: ${oneThread >= parallel}") - // параллельно на ядрах быстрее: true - // на одном потоке выигрыша нет: true -} -``` -Вывод: для вычислений выигрыш даёт только параллелизм по ядрам, а не количество корутин. Тысяча корутин на четырёх ядрах считает ровно столько же, сколько четыре. Корутины здесь удобны как способ организации кода, а не как ускорение. - -## Что происходит при блокировке -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val cores = Runtime.getRuntime().availableProcessors() - println("ядер в системе: ${cores > 0}") - - val finished = withTimeoutOrNull(300) { - withContext(Dispatchers.Default) { - // занимаем все потоки Default блокирующими вызовами - List(cores) { launch { Thread.sleep(500) } } - launch { println("эта корутина не получит поток вовремя") } - } - true - } - println("диспетчер оказался парализован: ${finished == null}") - // ядер в системе: true - // диспетчер оказался парализован: true -} -``` -Это главный практический риск корутин: один блокирующий вызов в неправильном диспетчере останавливает несвязанные задачи. Поток, заблокированный на `Thread.sleep` или JDBC, не может выполнять другие корутины. Подробнее: [Диспетчеры и контекст](Диспетчеры%20и%20контекст.md) - -## Виртуальные потоки Java -Project Loom решает ту же задачу иначе: делает блокировку дешёвой. - -```kotlin -fun main() { - println("JVM: ${Runtime.version().feature()}") - // Java 21+: Thread.ofVirtual().start { ... } — виртуальный поток, - // который при блокирующем вызове освобождает несущий поток ОС. - // Thread.sleep(1000) на нём стоит почти столько же, сколько delay(1000) в корутине. -} -``` -Сравнение подходов: - -| | Корутины | Виртуальные потоки | -|---|---|---| -| где видно асинхронность | в типе: `suspend` | нигде, код обычный | -| существующий блокирующий код | нужно переписать | работает как есть | -| отмена | по дереву, встроена | `interrupt`, вручную | -| структурированность | обязательна | `StructuredTaskScope`, опционально | -| потоки данных | `Flow` | нет аналога, нужен Reactive Streams | -| платформы | JVM, Android, Native, JS, Wasm | только JVM 21+ | -| `ThreadLocal` | нужен `asContextElement` | работает (но дорог при миллионах) | -| готовность экосистемы | библиотеки с `suspend` API | любая блокирующая библиотека | - -**Главный плюс Loom** - обратная совместимость: миллионы строк блокирующего кода и библиотек (JDBC, Apache HttpClient, старые ORM) внезапно масштабируются без изменений. - -**Главный плюс корутин** - асинхронность видна компилятору. Из `suspend` в сигнатуре вырастают структурированная конкурентность, отмена по дереву, `Flow` с backpressure и проверка «это нельзя вызвать из UI-потока» на этапе компиляции. С виртуальными потоками компилятор не знает, где функция заблокируется, поэтому такие гарантии недоступны. - -```kotlin -import kotlinx.coroutines.* - -// в сигнатуре видно, что функция может приостановиться -suspend fun loadUser(id: Int): String { - delay(10) - return "пользователь $id" -} - -// а здесь не видно ничего: может считать 1 нс, может ждать сеть 10 с -fun loadUserBlocking(id: Int): String { - Thread.sleep(10) - return "пользователь $id" -} - -fun main() = runBlocking { - println(loadUser(1)) - println(loadUserBlocking(2)) - // пользователь 1 - // пользователь 2 -} -``` -Второй вариант нельзя безопасно вызвать из UI-потока, и ни компилятор, ни тип этого не подскажут. - -Для Android вопрос не стоит: виртуальных потоков там нет, корутины - стандарт. Для серверного Kotlin на JVM 21+ оба подхода рабочие, и выбор определяется экосистемой: Ktor и Exposed дают `suspend` API, Spring MVC и JDBC - блокирующий. - -```mermaid -graph TD - A["Нужна конкурентность"] --> B{"Платформа?"} - B -->|"Android, Native, JS"| C["корутины"] - B -->|"JVM 21+"| D{"Код и библиотеки?"} - D -->|"блокирующие: JDBC,
Spring MVC"| E["виртуальные потоки
или корутины + IO"] - D -->|"suspend API: Ktor,
R2DBC"| C - A --> F{"Нужны потоки данных,
отмена по дереву?"} - F -->|"да"| C -``` - -## Когда нужны именно потоки -Корутины не универсальны. - -**1. Блокирующий код, который нельзя переписать.** JDBC, `java.io`, нативные библиотеки через JNI. Их место - `Dispatchers.IO` или отдельный пул: -```kotlin -import kotlinx.coroutines.* - -suspend fun queryDatabase(): String = withContext(Dispatchers.IO) { - Thread.sleep(20) // здесь мог быть JDBC - "строка из БД" -} - -fun main() = runBlocking { - println(queryDatabase()) // строка из БД -} -``` - -**2. Код, требующий привязки к потоку.** Некоторые библиотеки используют `ThreadLocal` или требуют вызовов из одного и того же потока (OpenGL, часть UI-тулкитов, нативные SDK): -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - @OptIn(DelicateCoroutinesApi::class) - val dedicated = newSingleThreadContext("нативный-поток") - try { - withContext(dedicated) { - println("всегда один и тот же поток: ${Thread.currentThread().name}") - } - } finally { - dedicated.close() - } - // всегда один и тот же поток: нативный-поток -} -``` - -**3. Приоритеты и реальное время.** `Thread.setPriority`, потоки реального времени, привязка к ядру - у корутин этого нет вообще. - -**4. Бесконечный цикл без точек приостановки.** Корутина, которая никогда не приостанавливается, занимает поток диспетчера полностью, и её нельзя отменить. Для такой задачи нужен выделенный поток. - -## Измерение: сколько живёт корутина -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val before = Runtime.getRuntime().let { it.totalMemory() - it.freeMemory() } - - val jobs = List(200_000) { launch { delay(5000) } } - - val after = Runtime.getRuntime().let { it.totalMemory() - it.freeMemory() } - val perCoroutine = (after - before) / 200_000 - - println("200 000 корутин создано: ${jobs.size}") - println("байт на корутину (порядок величины): ${perCoroutine in 1..5000}") - jobs.forEach { it.cancel() } - // 200 000 корутин создано: 200000 - // байт на корутину (порядок величины): true -} -``` -Измерение грубое (GC работает параллельно), но порядок виден: сотни байт против мегабайта у потока. Именно поэтому «корутина на каждый запрос» или «корутина на каждое соединение» - нормальная архитектура, а «поток на каждый запрос» перестаёт работать на нескольких тысячах. - -## Выбор на практике -| Задача | Решение | -|---|---| -| много одновременных сетевых запросов | корутины | -| обработка HTTP-запросов на сервере | корутины или виртуальные потоки | -| асинхронность в Android | корутины, других вариантов нет | -| параллельные вычисления | `Dispatchers.Default`, корутин по числу ядер | -| JDBC и блокирующие драйверы | корутины + `Dispatchers.IO`, либо виртуальные потоки | -| нативная библиотека с привязкой к потоку | выделенный поток | -| задача с приоритетом ОС | поток | -| поток событий с backpressure | `Flow` | - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `new Thread()` | `launch` | -> | `ExecutorService` | `CoroutineScope` + диспетчер | -> | `Thread.sleep` (блокирует) | `delay` (приостанавливает) | -> | `Thread.ofVirtual()` (Java 21+) | корутины | -> | `StructuredTaskScope` (Java 21+) | `coroutineScope` | -> | `ThreadLocal` | элемент контекста корутины | -> | «поток на запрос» не масштабируется | «корутина на запрос» - норма | -> -> **Модель «поток на запрос» и её предел.** Классический Java-сервер выделяет поток на каждое соединение. При 10 000 одновременных соединений это 10 000 потоков, около 10 ГБ стеков и планировщик ядра, занятый переключениями. Именно отсюда выросли NIO, Netty и реактивное программирование - с их колбэками и нечитаемыми стектрейсами. Корутины дают масштабируемость NIO, сохраняя линейный код: -> ```kotlin -> import kotlinx.coroutines.* -> -> suspend fun handleRequest(id: Int): String { -> delay(10) // здесь мог быть запрос к БД -> return "ответ $id" -> } -> -> fun main() = runBlocking { -> val responses = List(10_000) { async { handleRequest(it) } }.awaitAll() -> println("обработано запросов: ${responses.size}") // обработано запросов: 10000 -> } -> ``` -> Десять тысяч «обработчиков запросов» на несколько потоков - в Java до Loom это требовало полностью асинхронного стека. -> -> **Виртуальные потоки - ответ Java на ту же проблему.** `Thread.ofVirtual().start { }` создаёт поток, который при блокирующем вызове освобождает несущий поток ОС. По стоимости это сравнимо с корутиной, а главное преимущество - существующий блокирующий код работает без изменений. Цена - отсутствие информации об асинхронности в типах, из-за чего нет ни отмены по дереву, ни `Flow`, ни проверок на этапе компиляции. Loom добирает часть этого отдельными API (`StructuredTaskScope`), но они опциональны и появились на 8 лет позже корутин. -> -> **`CompletableFuture` - промежуточное решение, которое проиграло оба раза.** Он даёт неблокирующий код, но ценой разрыва линейности: лямбды, протаскивание переменных через замыкания, неудобные `if` и циклы. Корутины и виртуальные потоки решают задачу так, что код остаётся последовательным - просто разными способами. -> -> **`ThreadLocal` стоит дорого при миллионе потоков.** Это ограничение Loom, о котором говорят меньше: `ThreadLocal` на миллион виртуальных потоков - это миллион копий значения. Для корутин элемент контекста наследуется деревом и копий не создаёт. -> -> **Отладка: стектрейсы.** Поток даёт полный стектрейс бесплатно - это заметный плюс и потоков, и виртуальных потоков. У корутины стектрейс содержит кадры только от последней точки возобновления, а историю приостановок восстанавливает библиотека `kotlinx-coroutines-debug` или флаг `-Dkotlinx.coroutines.debug`. Без них отладка асинхронного кода труднее, чем у потоков. -> -> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [JVM](../../Программирование%20на%20Java/JVM/JVM.md) - -> [!warning] Подводные камни -> **Блокирующий вызов отменяет всю выгоду:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlin.system.measureTimeMillis -> -> fun main() = runBlocking { -> val t = measureTimeMillis { -> withContext(Dispatchers.Default.limitedParallelism(2)) { -> List(10) { launch { Thread.sleep(50) } }.forEach { it.join() } -> } -> } -> println("10 задач по 50 мс на 2 потоках: ${t >= 240}") // 10 задач по 50 мс на 2 потоках: true -> } -> ``` -> Корутины превратились в обычный пул потоков с очередью. Весь смысл - в `delay` вместо `Thread.sleep`, то есть в приостановке вместо блокировки. -> -> **«Корутин больше - значит быстрее» неверно для вычислений.** На четырёх ядрах сорок счётных корутин работают не быстрее четырёх, а переключений становится больше. Число параллельных вычислительных задач должно соответствовать числу ядер. -> -> **Стектрейсы хуже, чем у потоков.** Исключение из корутины содержит кадры от точки возобновления, а не всю логическую цепочку вызовов. Это реальная цена корутин, которую частично компенсирует `kotlinx-coroutines-debug`. -> -> **Корутина без точек приостановки не отменяется и занимает поток целиком.** Бесконечный счётный цикл в корутине хуже, чем в потоке: поток можно хотя бы прервать средствами ОС, а корутину - нет. -> -> **`ThreadLocal` не работает так, как ожидается.** Любая библиотека, полагающаяся на `ThreadLocal` (MDC в логах, `SecurityContext` в Spring, транзакции Hibernate), в корутинах требует `asContextElement` или специальной интеграции. С виртуальными потоками такой проблемы нет. -> -> **Профилировщики и инструменты знают о потоках, а не о корутинах.** В дампе потоков вы увидите `DefaultDispatcher-worker-1`, а не свои корутины; счётчики JMX считают потоки. Для корутин нужны отдельные инструменты - `kotlinx-coroutines-debug` или поддержка в IDE. -> -> **Переход на корутины не бесплатен архитектурно.** `suspend` поднимается по всему стеку вызовов до точки входа. Если наверху - не-suspend интерфейс (`Comparator`, `equals`, Java-колбэк), приостановку туда не поместить, и придётся либо менять архитектуру, либо блокировать поток через `runBlocking`. -> -> **Смешивание корутин и блокирующего кода даёт худшее из двух миров.** Половина кода на `suspend`, половина блокирующая, `runBlocking` в середине - такая система и не масштабируется, и сложна в отладке. Границу между блокирующим и приостанавливаемым кодом нужно держать явной и единственной. -> -> **Корутины не отменяют гонок и deadlock'ов.** Разделяемое изменяемое состояние требует синхронизации точно так же, как в потоках, а два `Mutex` в разном порядке дают взаимную блокировку. Подробнее: [Разделяемое состояние](Разделяемое%20состояние.md) -> -> **Измерения на микробенчмарках обманчивы.** JIT-компиляция, прогрев и GC искажают результаты в десятки раз. Цифры в этой заметке - порядки величин, а не точные значения; для реальных измерений нужен JMH. - -Далее: [Взаимодействие с Java](../Взаимодействие%20с%20Java/Взаимодействие%20с%20Java.md) diff --git a/Kotlin/Корутины/Корутины.md b/Kotlin/Корутины/Корутины.md deleted file mode 100644 index 6d266c4..0000000 --- a/Kotlin/Корутины/Корутины.md +++ /dev/null @@ -1,277 +0,0 @@ ---- -title: Корутины -description: Что такое приостанавливаемое вычисление, зачем оно нужно и как устроен раздел -tags: - - kotlin - - coroutines ---- - -Корутина - это вычисление, которое можно **приостановить** и продолжить позже, не блокируя поток. Это главный инструмент Kotlin для асинхронного кода: вместо коллбэков и `CompletableFuture` вы пишете последовательный код, а компилятор превращает его в машину состояний. - -```kotlin -import kotlinx.coroutines.* - -suspend fun loadUser(id: Int): String { - delay(100) // приостановка, а не блокировка - return "пользователь $id" -} - -suspend fun loadOrders(userId: Int): List { - delay(100) - return listOf("заказ 1", "заказ 2") -} - -fun main() = runBlocking { - val user = loadUser(1) - val orders = loadOrders(1) - println("$user: $orders") // пользователь 1: [заказ 1, заказ 2] -} -``` -Код читается как последовательный, но на `delay` поток освобождается и может выполнять другие корутины. Это и есть вся идея. - -## Почему не потоки -Поток ОС стоит около мегабайта стека и переключается через ядро. Корутина - это объект в куче, её «переключение» - обычный вызов метода. - -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val jobs = List(50_000) { - launch { delay(1000) } - } - println("запущено корутин: ${jobs.size}") // запущено корутин: 50000 - jobs.forEach { it.join() } - println("все завершились") // все завершились -} -``` -Пятьдесят тысяч потоков повалили бы JVM с `OutOfMemoryError`; пятьдесят тысяч корутин спокойно живут в нескольких потоках. Подробнее: [Корутины против потоков](Корутины%20против%20потоков.md) - -## Приостановка вместо блокировки -Разница принципиальная. Блокировка занимает поток, ничего не делая: -```kotlin -import kotlinx.coroutines.* -import kotlin.system.measureTimeMillis - -fun main() = runBlocking { - val blocking = measureTimeMillis { - withContext(Dispatchers.Default) { - List(4) { launch { Thread.sleep(500) } }.forEach { it.join() } - } - } - - val suspending = measureTimeMillis { - List(4) { launch { delay(500) } }.forEach { it.join() } - } - - println("блокировка дольше: ${blocking > suspending}") // блокировка дольше: true - println("приостановка около 500 мс: ${suspending < 900}") // приостановка около 500 мс: true -} -``` -`Thread.sleep` держит поток; `delay` возвращает его в пул. Из-за этого четыре `delay(500)` на одном потоке занимают 500 мс, а четыре `Thread.sleep(500)` - столько, сколько позволит число потоков. - -## Как это работает под капотом -Компилятор превращает `suspend`-функцию в машину состояний с объектом-продолжением (`Continuation`). Каждая точка приостановки - это метка, куда вернётся выполнение. - -```mermaid -graph LR - A["вызов suspend-функции"] --> B["выполнение до delay"] - B --> C{"нужна приостановка?"} - C -->|"нет, результат готов"| G["вернуть значение"] - C -->|"да"| D["сохранить состояние
в Continuation"] - D --> E["вернуть COROUTINE_SUSPENDED
поток свободен"] - E --> F["внешнее событие:
таймер, I/O, другая корутина"] - F --> H["resume: восстановить состояние"] - H --> B -``` - -Практическое следствие: приостановка возможна **только** в `suspend`-функции, потому что только у неё есть скрытый параметр `Continuation`. Подробнее: [Suspend-функции](Suspend-функции.md) - -## Параллелизм -Последовательный код легко превратить в параллельный: -```kotlin -import kotlinx.coroutines.* -import kotlin.system.measureTimeMillis - -suspend fun fetchA(): Int { delay(200); return 1 } -suspend fun fetchB(): Int { delay(200); return 2 } - -fun main() = runBlocking { - val sequential = measureTimeMillis { - val sum = fetchA() + fetchB() - println("последовательно: $sum") // последовательно: 3 - } - - val parallel = measureTimeMillis { - val a = async { fetchA() } - val b = async { fetchB() } - println("параллельно: ${a.await() + b.await()}") // параллельно: 3 - } - - println("ускорение примерно вдвое: ${sequential > parallel + 100}") // ускорение примерно вдвое: true -} -``` -Разница - в `async`: он запускает корутину и сразу возвращает `Deferred`, а `await` ждёт результат. Подробнее: [Билдеры корутин](Билдеры%20корутин.md) - -## Структурированная конкурентность -Главное архитектурное решение библиотеки: у каждой корутины есть родитель, и родитель не завершится раньше детей. - -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - println("начало") - coroutineScope { - launch { delay(100); println("ребёнок 1") } - launch { delay(50); println("ребёнок 2") } - } - println("конец — после всех детей") - // начало - // ребёнок 2 - // ребёнок 1 - // конец — после всех детей -} -``` -Из этого бесплатно получаются: ожидание завершения, распространение отмены вниз и ошибок вверх, отсутствие «потерянных» задач. Это то, чего в Java приходится добиваться вручную. Подробнее: [Структурированная конкурентность](Структурированная%20конкурентность.md) - -## Карта раздела -| Заметка | О чём | -|---|---| -| [Suspend-функции](Suspend-функции.md) | `suspend`, `Continuation`, машина состояний | -| [Структурированная конкурентность](Структурированная%20конкурентность.md) | `CoroutineScope`, иерархия, жизненный цикл | -| [Билдеры корутин](Билдеры%20корутин.md) | `launch`, `async`, `runBlocking`, `withContext` | -| [Диспетчеры и контекст](Диспетчеры%20и%20контекст.md) | на каком потоке выполняется код | -| [Отмена и таймауты](Отмена%20и%20таймауты.md) | кооперативная отмена, `withTimeout`, `NonCancellable` | -| [Исключения в корутинах](Исключения%20в%20корутинах.md) | распространение ошибок, `SupervisorJob`, обработчики | -| [Flow](Flow.md) | асинхронный поток значений, операторы, backpressure | -| [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md) | горячие потоки и состояние | -| [Channels](Channels.md) | передача значений между корутинами | -| [Разделяемое состояние](Разделяемое%20состояние.md) | `Mutex`, атомарные типы, конфайнмент | -| [Корутины против потоков](Корутины%20против%20потоков.md) | цифры, стоимость, когда нужны потоки | - -## Подключение -Корутины - не часть stdlib, это отдельная библиотека: -```kotlin -// build.gradle.kts -dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0") -} -``` -В языке есть только `suspend`, `Continuation` и `suspendCoroutine` - минимальная основа. `launch`, `async`, `Flow`, диспетчеры и каналы живут в `kotlinx.coroutines`. Подробнее: [Gradle и Kotlin DSL](../Инструментарий/Gradle%20и%20Kotlin%20DSL.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `Thread`, `ExecutorService` | корутины на пуле потоков | -> | `CompletableFuture` | `Deferred` или просто `suspend fun` | -> | `.thenApply().thenCompose()` | последовательный код | -> | `CountDownLatch`, `Future.get()` | `join()`, `await()` | -> | виртуальные потоки (Java 21+) | корутины | -> | `Flow` из Reactive Streams | `kotlinx.coroutines.Flow` | -> | отмена через `Future.cancel` и флаги | отмена по иерархии | -> -> **Главное отличие - асинхронный код выглядит последовательным.** В Java до виртуальных потоков асинхронность означала коллбэки или цепочки `CompletableFuture`: -> ```java -> loadUser(id) -> .thenCompose(user -> loadOrders(user.getId())) -> .thenApply(orders -> format(orders)) -> .exceptionally(e -> "ошибка: " + e.getMessage()); -> ``` -> Каждый шаг - лямбда, переменные приходится протаскивать через замыкания, а `try/catch`, циклы и `if` в такой цепочке выражаются неудобно. В Kotlin то же самое пишется обычным кодом: -> ```kotlin -> import kotlinx.coroutines.* -> -> suspend fun loadUser2(id: Int): String { delay(10); return "user$id" } -> suspend fun loadOrders2(user: String): List { delay(10); return listOf("$user-заказ") } -> -> suspend fun report(id: Int): String = try { -> val user = loadUser2(id) -> val orders = loadOrders2(user) -> orders.joinToString() -> } catch (e: Exception) { -> "ошибка: ${e.message}" -> } -> -> fun main() = runBlocking { println(report(1)) } // user1-заказ -> ``` -> Работают обычные `try/catch`, `for`, `return` и локальные переменные. -> -> **Виртуальные потоки (Java 21) решают ту же задачу иначе.** Project Loom делает блокировку дешёвой: виртуальный поток при блокирующем вызове освобождает несущий поток ОС, поэтому старый блокирующий код внезапно масштабируется. Разница в подходе: Loom прячет асинхронность от программиста (`Thread.sleep` просто становится дешёвым), корутины делают её видимой в типах (`suspend` в сигнатуре). Плюс Loom - работает весь существующий код; плюс корутин - компилятор знает, где функция может приостановиться, и из этого вырастают структурированная конкурентность, `Flow` и отмена. Корутины также работают на Android и в Kotlin/Native, где Loom недоступен. -> -> **Отмена в Java не распространяется.** `Future.cancel(true)` ставит флаг прерывания одному потоку; задачи, которые он породил, продолжат работу. В Kotlin отмена идёт по дереву корутин автоматически. Для Java это пришлось бы строить руками поверх `ExecutorService`. Подробнее: [Отмена и таймауты](Отмена%20и%20таймауты.md) -> -> **Реактивные библиотеки закрывают часть задач.** RxJava и Project Reactor дают потоки данных и операторы, то есть примерный аналог `Flow`. Но у них свой стиль (всё - цепочка операторов), высокий порог входа и плохие стектрейсы. `Flow` при этом - обычные `suspend`-функции внутри, поэтому в нём можно писать `for` и `try`. -> -> Сравнить: [Многопоточность в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [JVM](../../Программирование%20на%20Java/JVM/JVM.md) - -> [!warning] Подводные камни -> **Корутины не ускоряют вычисления на CPU.** Они экономят потоки при ожидании (сеть, диск, таймеры). Если код считает, а не ждёт, выгоды от корутин нет - там нужен параллелизм по числу ядер: -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> val result = withContext(Dispatchers.Default) { // пул по числу ядер -> (1..4).map { async { (1..1_000_000).sumOf { x -> x.toLong() } } }.awaitAll() -> } -> println(result.size) // 4 -> } -> ``` -> На `Dispatchers.IO` такой код только создаст лишние потоки. Подробнее: [Диспетчеры и контекст](Диспетчеры%20и%20контекст.md) -> -> **Блокирующий вызов внутри корутины блокирует поток целиком:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlin.system.measureTimeMillis -> -> fun main() = runBlocking { -> val t = measureTimeMillis { -> withContext(Dispatchers.Default.limitedParallelism(1)) { -> List(3) { launch { Thread.sleep(100) } }.forEach { it.join() } -> } -> } -> println("последовательно, не 100 мс: ${t >= 300}") // последовательно, не 100 мс: true -> } -> ``` -> `Thread.sleep`, `JDBC`, старый `HttpURLConnection`, `File.readText` - всё это блокирует. Такие вызовы нужно оборачивать в `withContext(Dispatchers.IO)`. -> -> **`runBlocking` блокирует вызывающий поток** - это мост из обычного кода в корутины, а не способ запускать их в продакшене. В Android и серверных фреймворках у вас уже есть scope; `runBlocking` уместен в `main`, тестах и утилитах. -> -> **Корутина без scope теряется.** `GlobalScope.launch` не привязан ни к чему: его никто не отменит и никто не узнает об ошибке. Это главный антипаттерн в корутинах. Подробнее: [Структурированная конкурентность](Структурированная%20конкурентность.md) -> -> **Исключение в `launch` не ловится снаружи:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> try { -> launch { throw IllegalStateException("внутри launch") } -> } catch (e: Exception) { -> println("не поймается здесь") -> } -> delay(50) -> println("а это выполнится") -> } -> ``` -> Этот код напечатает `а это выполнится`, а затем уронит программу: `launch` передаёт ошибку родителю, а не выбрасывает её в месте вызова. Подробнее: [Исключения в корутинах](Исключения%20в%20корутинах.md) -> -> **Отмена кооперативная.** Корутина, которая не вызывает `suspend`-функций и не проверяет `isActive`, не отменяется: -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> val job = launch(Dispatchers.Default) { -> var i = 0 -> while (i < 100_000_000) i++ // не проверяет отмену -> println("досчитал несмотря на отмену") -> } -> delay(20) -> job.cancelAndJoin() -> println("cancel вернулся") -> } -> ``` -> Цикл без точек приостановки игнорирует `cancel`. -> -> **`suspend` в сигнатуре - это заразно.** Вызвать `suspend`-функцию можно только из другой `suspend`-функции или из билдера. Поэтому `suspend` поднимается вверх по стеку до точки входа, и добавить его в середину существующего синхронного кода не получится без переделки всей цепочки вызовов. -> -> **Версия библиотеки имеет значение.** API корутин развивается: `limitedParallelism`, `Flow`-операторы, `SupervisorJob` поведение менялись между версиями. Код из статей 2019 года может не компилироваться или вести себя иначе. - -Далее: [Suspend-функции](Suspend-функции.md) diff --git a/Kotlin/Корутины/Отмена и таймауты.md b/Kotlin/Корутины/Отмена и таймауты.md deleted file mode 100644 index 0a7ca33..0000000 --- a/Kotlin/Корутины/Отмена и таймауты.md +++ /dev/null @@ -1,525 +0,0 @@ ---- -title: Отмена и таймауты -description: Кооперативная отмена, CancellationException, withTimeout, NonCancellable и освобождение ресурсов -tags: - - kotlin - - coroutines ---- - -Отмена корутин **кооперативная**: библиотека не может остановить код насильно, она лишь сообщает корутине, что её просят завершиться. Код должен это проверять - сам или через `suspend`-функции. - -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val job = launch { - repeat(100) { i -> - delay(10) - println("шаг $i") - } - } - delay(35) - job.cancelAndJoin() - println("отменено") - // шаг 0 - // шаг 1 - // шаг 2 - // отменено -} -``` -Все `suspend`-функции из `kotlinx.coroutines` (`delay`, `join`, `await`, `withContext`) проверяют отмену и бросают `CancellationException`. - -## Как работает отмена -`cancel()` переводит `Job` в состояние Cancelling и бросает `CancellationException` в точке приостановки: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val job = launch { - try { - delay(1000) - } catch (e: CancellationException) { - println("поймали: ${e::class.simpleName}") - throw e // обязательно пробросить! - } - } - delay(20) - job.cancelAndJoin() - println("состояние: отменён=${job.isCancelled}, завершён=${job.isCompleted}") - // поймали: JobCancellationException - // состояние: отменён=true, завершён=true -} -``` -`CancellationException` - особое исключение: оно не считается ошибкой и не приводит к падению родителя. Подробнее: [Исключения в корутинах](Исключения%20в%20корутинах.md) - -## Код без точек приостановки не отменяется -```kotlin -import kotlinx.coroutines.* -import kotlin.system.measureTimeMillis - -fun main() = runBlocking { - val t = measureTimeMillis { - val job = launch(Dispatchers.Default) { - var sum = 0L - repeat(200_000_000) { sum += it } // ни одной точки приостановки - println("досчитал несмотря на отмену: ${sum > 0}") - } - delay(20) - job.cancelAndJoin() - } - println("cancel ждал окончания цикла: ${t > 100}") - // досчитал несмотря на отмену: true - // cancel ждал окончания цикла: true -} -``` -Два способа сделать такой код отменяемым: - -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - // 1. проверять isActive - val job1 = launch(Dispatchers.Default) { - var i = 0 - while (isActive && i < 200_000_000) i++ - println("вышел по isActive: ${i < 200_000_000}") - } - delay(20) - job1.cancelAndJoin() - - // 2. вызывать ensureActive() — бросит CancellationException - val job2 = launch(Dispatchers.Default) { - try { - repeat(200_000_000) { - if (it % 100_000 == 0) ensureActive() - } - } catch (e: CancellationException) { - println("вышел по ensureActive") - throw e - } - } - delay(20) - job2.cancelAndJoin() - // вышел по isActive: true - // вышел по ensureActive -} -``` -Есть и третий вариант - `yield()`: он и проверяет отмену, и даёт другим корутинам шанс выполниться. - -| Способ | Бросает исключение | Когда применять | -|---|---|---| -| `isActive` | нет, возвращает `Boolean` | условие цикла | -| `ensureActive()` | да, `CancellationException` | внутри тела, быстрый выход | -| `yield()` | да | ещё и уступает поток другим | - -## Освобождение ресурсов -`finally` работает как обычно: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val job = launch { - try { - println("ресурс открыт") - delay(1000) - } finally { - println("ресурс закрыт") - } - } - delay(20) - job.cancelAndJoin() - println("готово") - // ресурс открыт - // ресурс закрыт - // готово -} -``` -Но внутри `finally` отменённой корутины **нельзя приостанавливаться**: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val job = launch { - try { - delay(1000) - } finally { - val failed = runCatching { delay(50) }.isFailure - println("delay в finally упал: $failed") // delay в finally упал: true - } - } - delay(20) - job.cancelAndJoin() -} -``` -Корутина уже отменена, поэтому любая приостановка немедленно бросает `CancellationException`. Если в очистке нужна приостановка - оберните её в `NonCancellable`: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val job = launch { - try { - delay(1000) - } finally { - withContext(NonCancellable) { - delay(20) - println("асинхронная очистка выполнена") - } - } - } - delay(20) - job.cancelAndJoin() - println("готово") - // асинхронная очистка выполнена - // готово -} -``` -`NonCancellable` нужен именно и только для этого. Использовать его, чтобы «защитить» обычную работу от отмены - ошибка: такая корутина станет неостановимой. - -Для `Closeable`-ресурсов проще `use`: -```kotlin -import kotlinx.coroutines.* - -class Connection : AutoCloseable { - fun query() = "результат" - override fun close() = println("соединение закрыто") -} - -fun main() = runBlocking { - val job = launch { - Connection().use { conn -> - println(conn.query()) - delay(1000) - } - } - delay(20) - job.cancelAndJoin() - // результат - // соединение закрыто -} -``` - -## withTimeout -```kotlin -import kotlinx.coroutines.* - -suspend fun slowOperation(): String { - delay(1000) - return "готово" -} - -fun main() = runBlocking { - try { - withTimeout(50) { slowOperation() } - } catch (e: TimeoutCancellationException) { - println("не успели: ${e.message}") - } - // не успели: Timed out waiting for 50 ms -} -``` -`withTimeoutOrNull` вместо исключения возвращает `null` - обычно это удобнее: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val fast = withTimeoutOrNull(100) { delay(20); "успели" } - val slow = withTimeoutOrNull(20) { delay(100); "не успели" } - - println(fast) // успели - println(slow) // null -} -``` -`TimeoutCancellationException` - подкласс `CancellationException`, поэтому он отменяет только свой блок, а не всю область. - -## Таймаут и ресурсы -Опасное место: таймаут может сработать между получением ресурса и входом в `try`. -```kotlin -import kotlinx.coroutines.* - -class Resource2 { - var closed = false - fun close() { closed = true } -} - -fun main() = runBlocking { - var resource: Resource2? = null - withTimeoutOrNull(30) { - resource = Resource2() // создали - try { - delay(1000) // здесь таймаут - } finally { - resource?.close() // закрыли - } - } - println("ресурс закрыт: ${resource?.closed}") // ресурс закрыт: true -} -``` -Правило: открывайте ресурс **внутри** блока и закрывайте в `finally` или через `use`. Открытие снаружи с закрытием внутри - источник утечек. - -## Отмена распространяется вниз -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val parent = launch { - launch { - try { delay(1000) } finally { println("ребёнок 1 завершён") } - } - launch { - try { delay(1000) } finally { println("ребёнок 2 завершён") } - } - try { delay(1000) } finally { println("родитель завершён") } - } - delay(20) - parent.cancelAndJoin() - println("всё дерево остановлено") - // ребёнок 1 завершён - // ребёнок 2 завершён - // родитель завершён - // всё дерево остановлено -} -``` -Это бесплатное следствие структурированной конкурентности: отменив одну корутину, вы останавливаете всё поддерево. Подробнее: [Структурированная конкурентность](Структурированная%20конкурентность.md) - -```mermaid -sequenceDiagram - participant В as Вызывающий - participant Р as Родитель - participant Д as Дети - В->>Р: cancel() - Р->>Р: состояние Cancelling - Р->>Д: отменить всех детей - Д->>Д: CancellationException
в точке приостановки - Д->>Д: выполнить finally - Д-->>Р: ребёнок Cancelled - Р->>Р: выполнить finally - Р-->>В: состояние Cancelled, join() вернулся -``` - -## cancel против cancelAndJoin -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val job1 = launch { try { delay(1000) } finally { println("1: очистка") } } - job1.cancel() // не ждём завершения - println("после cancel: завершён=${job1.isCompleted}") - - val job2 = launch { try { delay(1000) } finally { println("2: очистка") } } - job2.cancelAndJoin() // ждём - println("после cancelAndJoin: завершён=${job2.isCompleted}") - // после cancel: завершён=false - // 1: очистка - // 2: очистка - // после cancelAndJoin: завершён=true -} -``` -`cancel()` только отправляет сигнал; `cancelAndJoin()` дожидается, пока корутина действительно остановится и выполнит очистку. Для корректного освобождения ресурсов нужен второй вариант. - -## Отмена и async -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val deferred = async { delay(1000); "результат" } - delay(20) - deferred.cancel() - - val error = runCatching { deferred.await() }.exceptionOrNull() - println("await после cancel бросил: ${error is CancellationException}") // await после cancel бросил: true -} -``` -Отменённый `Deferred` при `await()` бросает `CancellationException`, а не возвращает `null`. Для «результат или ничего» используйте `withTimeoutOrNull`. - -## Таймаут на весь scope -```kotlin -import kotlinx.coroutines.* - -suspend fun loadWithDeadline(): List? = withTimeoutOrNull(60) { - coroutineScope { - val a = async { delay(20); "быстрый" } - val b = async { delay(1000); "медленный" } - listOf(a.await(), b.await()) - } -} - -fun main() = runBlocking { - println(loadWithDeadline()) // null — медленный не успел, отменились оба -} -``` -Таймаут поверх `coroutineScope` даёт дедлайн на всю группу задач: не успел один - отменяются все. Это правильное поведение для запроса с общим SLA. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `Thread.interrupt()` | `job.cancel()` | -> | `Thread.isInterrupted()` | `isActive` | -> | `InterruptedException` | `CancellationException` | -> | `Future.cancel(true)` - одна задача | отмена всего поддерева | -> | `ExecutorService.shutdownNow()` | `scope.cancel()` | -> | `future.get(50, MILLISECONDS)` | `withTimeout(50)` | -> | `ScheduledExecutorService` для дедлайна | `withTimeout` | -> | флаг `volatile boolean stopped` | `isActive` | -> -> **Прерывание в Java тоже кооперативное, но работает хуже.** `Thread.interrupt()` ставит флаг и прерывает только блокирующие вызовы, которые умеют его замечать (`sleep`, `wait`, `join`, часть NIO). Проблемы знакомые: `InterruptedException` - проверяемое исключение, которое приходится ловить в каждом блокирующем вызове, а при перехвате легко потерять флаг: -> ```java -> try { -> Thread.sleep(1000); -> } catch (InterruptedException e) { -> // забыли Thread.currentThread().interrupt() — прерывание потеряно -> } -> ``` -> В Kotlin `CancellationException` непроверяемое, поэтому его не нужно ловить везде, а `catch (e: Exception)` без проброса ломает отмену ровно так же - разница в том, что IDE и детекторы это подсказывают. -> -> **Отмена по дереву против отмены по задаче.** `Future.cancel(true)` прерывает один поток. Если задача породила подзадачи через другой `submit`, они продолжат работу, и отследить их можно только вручную - списком `Future`. В Kotlin `cancel()` на родителе гарантированно останавливает всё поддерево, включая задачи, запущенные тремя уровнями ниже. -> -> **Таймаут не требует отдельного планировщика.** В Java `future.get(50, MILLISECONDS)` прерывает **ожидание**, но не саму задачу - она продолжит выполняться в пуле. Чтобы остановить и её, нужен `cancel(true)` плюс поддержка прерывания в коде задачи. `withTimeout` отменяет сам блок: -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> var finished = false -> withTimeoutOrNull(30) { -> try { delay(1000); finished = true } finally { println("задача остановлена") } -> } -> println("тело доработало: $finished") -> // задача остановлена -> // тело доработало: false -> } -> ``` -> -> **`shutdownNow()` против `scope.cancel()`.** `shutdownNow` возвращает список незапущенных задач и прерывает потоки работающих - результат зависит от того, умеет ли код реагировать на прерывание. `scope.cancel()` отменяет корутины в известных точках приостановки, а `join()` даёт дождаться завершения очистки. -> -> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md) - -> [!warning] Подводные камни -> **Перехват `CancellationException` без проброса ломает отмену:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> val job = launch { -> repeat(5) { -> try { -> delay(20) -> } catch (e: CancellationException) { -> println("проглотили отмену на шаге $it") -> // нет throw — цикл продолжается! -> } -> } -> println("корутина доработала до конца") -> } -> delay(30) -> job.cancelAndJoin() -> // проглотили отмену на шаге 1 -> // проглотили отмену на шаге 2 -> // проглотили отмену на шаге 3 -> // проглотили отмену на шаге 4 -> // корутина доработала до конца -> } -> ``` -> Корутина стала неотменяемой. Та же беда возникает от `catch (e: Exception)` - `CancellationException` в него попадает. -> -> **`try/catch (Exception)` вокруг `suspend`-вызова перехватывает отмену:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> val job = launch { -> try { -> delay(1000) -> } catch (e: Exception) { -> println("перехвачено как обычная ошибка: ${e is CancellationException}") -> } -> println("продолжили работу после отмены") -> } -> delay(20) -> job.cancelAndJoin() -> // перехвачено как обычная ошибка: true -> // продолжили работу после отмены -> } -> ``` -> Правильно: сначала `catch (e: CancellationException) { throw e }`, затем `catch (e: Exception)`. -> -> **`runCatching` глотает `CancellationException`** по той же причине - он ловит `Throwable`. В корутинном коде это регулярная проблема: `runCatching { suspendCall() }` делает блок неотменяемым. Для `suspend`-кода нужен явный `try/catch` с пробросом отмены. -> -> **Приостановка в `finally` отменённой корутины не работает:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> val job = launch { -> try { -> delay(1000) -> } finally { -> val ok = runCatching { delay(50); "очистка" }.isSuccess -> println("приостановка в finally удалась: $ok") // приостановка в finally удалась: false -> } -> } -> delay(20) -> job.cancelAndJoin() -> } -> ``` -> Нужен `withContext(NonCancellable)`. -> -> **`NonCancellable` вокруг обычной работы делает корутину неостановимой:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlin.system.measureTimeMillis -> -> fun main() = runBlocking { -> val t = measureTimeMillis { -> val job = launch { -> withContext(NonCancellable) { -> delay(200) // отмена проигнорирована -> println("доработал несмотря на cancel") -> } -> } -> delay(20) -> job.cancelAndJoin() -> } -> println("cancel ждал 200 мс: ${t >= 190}") -> // доработал несмотря на cancel -> // cancel ждал 200 мс: true -> } -> ``` -> `NonCancellable` оправдан только для короткой очистки в `finally`. -> -> **`cancel()` без `join()` не ждёт очистки.** Если после `cancel()` сразу закрыть ресурс или выйти из процесса, `finally` может не успеть выполниться. Используйте `cancelAndJoin()`. -> -> **Чистый вычислительный цикл игнорирует отмену.** Любой `while`/`repeat` без `suspend`-вызовов нужно дополнять `isActive` или `ensureActive()` - иначе корутина отработает до конца, а `cancelAndJoin()` будет её ждать. -> -> **`withTimeout` бросает исключение, которое легко не заметить:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> val result = runCatching { withTimeout(20) { delay(100); "значение" } } -> println("упало с таймаутом: ${result.exceptionOrNull() is TimeoutCancellationException}") -> // упало с таймаутом: true -> } -> ``` -> `TimeoutCancellationException` - подкласс `CancellationException`, поэтому обычный `catch (e: CancellationException) { throw e }` его пробросит, и таймаут «не сработает» там, где вы ждали обработки. Используйте `withTimeoutOrNull` или ловите `TimeoutCancellationException` до отмены. -> -> **Ресурс, открытый до `withTimeout`, может утечь:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> var opened = 0 -> var closed = 0 -> withTimeoutOrNull(30) { -> opened++ // открыли внутри — хорошо -> try { delay(1000) } finally { closed++ } -> } -> println("открыто $opened, закрыто $closed") // открыто 1, закрыто 1 -> } -> ``` -> Если бы `opened++` стоял перед `withTimeoutOrNull`, а `closed++` внутри - при срабатывании таймаута до входа в блок ресурс остался бы открытым. -> -> **Отменённый scope нельзя переиспользовать** - `launch` в нём сразу возвращает отменённый `Job`. Для перезапуска создавайте новый scope. -> -> **`await()` на отменённом `Deferred` бросает исключение, а не возвращает `null`.** Код, ожидающий `null` при отмене, получит падение. -> -> **Отмена не мгновенна.** Между `cancel()` и фактическим завершением проходит время: корутина должна дойти до точки приостановки и выполнить `finally`. Проверка `isCompleted` сразу после `cancel()` обычно даёт `false`. - -Далее: [Исключения в корутинах](Исключения%20в%20корутинах.md) diff --git a/Kotlin/Корутины/Разделяемое состояние.md b/Kotlin/Корутины/Разделяемое состояние.md deleted file mode 100644 index 2257ba4..0000000 --- a/Kotlin/Корутины/Разделяемое состояние.md +++ /dev/null @@ -1,516 +0,0 @@ ---- -title: Разделяемое состояние -description: Mutex, Semaphore, атомарные типы, конфайнмент потока и подход «не разделять состояние вообще» -tags: - - kotlin - - coroutines ---- - -Корутины не отменяют гонок. Если несколько корутин меняют одну переменную, нужна синхронизация - причём такая, которая приостанавливает, а не блокирует поток. - -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - var counter = 0 - withContext(Dispatchers.Default) { - List(100) { launch { repeat(1000) { counter++ } } }.forEach { it.join() } - } - println("потеряно инкрементов: ${counter < 100_000}") // потеряно инкрементов: true -} -``` -`counter++` - это чтение, увеличение и запись: три операции, между которыми может вклиниться другая корутина. На одном потоке проблемы не было бы, но `Dispatchers.Default` использует все ядра. - -## Способы, по возрастанию цены -| Подход | Стоимость | Когда применять | -|---|---|---| -| не разделять состояние | нулевая | всегда, когда возможно | -| неизменяемые данные | нулевая | состояние целиком заменяется | -| атомарные типы | очень низкая | счётчики, флаги, одна переменная | -| `MutableStateFlow.update` | низкая | наблюдаемое состояние | -| конфайнмент в один поток | низкая | группа связанных переменных | -| `Mutex` | средняя | составная операция, вызов `suspend` внутри | -| `Semaphore` | средняя | ограничение числа одновременных операций | - -Порядок не случаен: первые два варианта убирают проблему, остальные её решают. - -## Атомарные типы -```kotlin -import kotlinx.coroutines.* -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.atomic.AtomicReference - -fun main() = runBlocking { - val counter = AtomicInteger(0) - withContext(Dispatchers.Default) { - List(100) { launch { repeat(1000) { counter.incrementAndGet() } } }.forEach { it.join() } - } - println("точный результат: ${counter.get()}") // точный результат: 100000 - - val ref = AtomicReference(listOf()) - withContext(Dispatchers.Default) { - List(10) { i -> launch { ref.updateAndGet { it + "элемент $i" } } }.forEach { it.join() } - } - println("добавлено без потерь: ${ref.get().size}") // добавлено без потерь: 10 -} -``` -Атомарные типы работают через CAS (compare-and-set) - никаких блокировок, только повтор при конфликте. Это самый дешёвый способ, но он покрывает только операции с **одной** переменной. - -## Mutex -`Mutex` - взаимное исключение для корутин. В отличие от `synchronized`, он приостанавливает, а не блокирует поток. -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock - -fun main() = runBlocking { - val mutex = Mutex() - var counter = 0 - - withContext(Dispatchers.Default) { - List(100) { launch { repeat(1000) { mutex.withLock { counter++ } } } } - .forEach { it.join() } - } - println("результат точный: ${counter == 100_000}") // результат точный: true -} -``` -Главное преимущество - внутри `withLock` можно вызывать `suspend`-функции: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock - -class Cache { - private val mutex = Mutex() - private val data = mutableMapOf() - - suspend fun get(id: Int): String = mutex.withLock { - data[id] ?: load(id).also { data[id] = it } - } - - private suspend fun load(id: Int): String { - delay(10) // suspend внутри критической секции - return "значение $id" - } -} - -fun main() = runBlocking { - val cache = Cache() - val results = List(5) { async { cache.get(1) } }.awaitAll() - println("все получили одно значение: ${results.toSet().size == 1}") // все получили одно значение: true -} -``` -С `synchronized` так нельзя: `suspend`-вызов внутри `synchronized`-блока не компилируется, потому что приостановка освободила бы поток, а монитор остался бы захваченным. - -## Mutex не реентерабелен -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock - -fun main() = runBlocking { - val mutex = Mutex() - - val deadlocked = withTimeoutOrNull(100) { - mutex.withLock { - mutex.withLock { // вторичный захват тем же владельцем - "недостижимо" - } - } - } - println("повторный захват повис: ${deadlocked == null}") // повторный захват повис: true -} -``` -Это осознанное решение: реентерабельность в корутинах определить корректно нельзя - «тот же владелец» для корутины понятие нечёткое, ведь она может перемещаться между потоками. Практический вывод: функция под `withLock` не должна вызывать другую функцию, которая берёт тот же мьютекс. Приватные методы, вызываемые из критической секции, не должны блокироваться сами. - -## Semaphore -Ограничивает число одновременных операций: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit -import java.util.concurrent.atomic.AtomicInteger - -fun main() = runBlocking { - val semaphore = Semaphore(permits = 3) - val inFlight = AtomicInteger(0) - val maxInFlight = AtomicInteger(0) - - withContext(Dispatchers.Default) { - List(20) { - launch { - semaphore.withPermit { - val now = inFlight.incrementAndGet() - maxInFlight.updateAndGet { m -> maxOf(m, now) } - delay(20) // ожидание ответа API - inFlight.decrementAndGet() - } - } - }.forEach { it.join() } - } - println("одновременно не больше 3: ${maxInFlight.get() <= 3}") // одновременно не больше 3: true -} -``` -Важное отличие от `limitedParallelism`: семафор считает операции **в полёте**, включая приостановленные. Именно он нужен, чтобы не превысить лимит внешнего API. Подробнее: [Диспетчеры и контекст](Диспетчеры%20и%20контекст.md) - -## Конфайнмент в один поток -Если состояние меняется только в одном потоке, синхронизация не нужна вовсе: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val single = Dispatchers.Default.limitedParallelism(1) - var counter = 0 - - withContext(Dispatchers.Default) { - List(100) { - launch { - repeat(1000) { - withContext(single) { counter++ } // все изменения в одном потоке - } - } - }.forEach { it.join() } - } - println("точный результат: ${counter == 100_000}") // точный результат: true -} -``` -Работает, но дорого: 100 000 переключений контекста. Правильная версия конфайнмента - **грубая**, когда весь блок работы уходит в один поток: -```kotlin -import kotlinx.coroutines.* -import kotlin.system.measureTimeMillis - -fun main() = runBlocking { - val single = Dispatchers.Default.limitedParallelism(1) - var counter = 0 - - val coarse = measureTimeMillis { - withContext(single) { // переключение один раз - repeat(100_000) { counter++ } - } - } - println("результат: $counter") - println("грубый конфайнмент быстр: ${coarse < 500}") - // результат: 100000 - // грубый конфайнмент быстр: true -} -``` - -## Актор: состояние внутри корутины -Самый чистый подход - состояние не разделяется вообще, им владеет одна корутина: -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel - -sealed interface AccountCommand { - data class Deposit(val amount: Int) : AccountCommand - data class Withdraw(val amount: Int, val ok: CompletableDeferred) : AccountCommand - data class Balance(val reply: CompletableDeferred) : AccountCommand -} - -fun main() = runBlocking { - val commands = Channel() - - launch { - var balance = 0 // никто больше не видит эту переменную - for (cmd in commands) { - when (cmd) { - is AccountCommand.Deposit -> balance += cmd.amount - is AccountCommand.Withdraw -> { - val enough = balance >= cmd.amount - if (enough) balance -= cmd.amount - cmd.ok.complete(enough) - } - is AccountCommand.Balance -> cmd.reply.complete(balance) - } - } - } - - repeat(100) { commands.send(AccountCommand.Deposit(10)) } - - val ok = CompletableDeferred() - commands.send(AccountCommand.Withdraw(500, ok)) - println("списание прошло: ${ok.await()}") - - val tooMuch = CompletableDeferred() - commands.send(AccountCommand.Withdraw(10_000, tooMuch)) - println("превышение отклонено: ${!tooMuch.await()}") - - val balance = CompletableDeferred() - commands.send(AccountCommand.Balance(balance)) - println("баланс: ${balance.await()}") - commands.close() - // списание прошло: true - // превышение отклонено: true - // баланс: 500 -} -``` -Проверка «хватает ли денег» и списание происходят атомарно без всякой блокировки - просто потому, что их выполняет одна корутина. Подробнее: [Channels](Channels.md) - -## StateFlow для наблюдаемого состояния -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update - -fun main() = runBlocking { - val state = MutableStateFlow(0) - withContext(Dispatchers.Default) { - List(100) { launch { repeat(1000) { state.update { v -> v + 1 } } } } - .forEach { it.join() } - } - println("результат точный: ${state.value == 100_000}") // результат точный: true -} -``` -`update` - это CAS-цикл, поэтому он и атомарен, и даёт оповещение подписчикам. Если состояние нужно наблюдать, это лучший вариант. Подробнее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md) - -## Как выбрать -```mermaid -graph TD - A["Состояние меняют
несколько корутин"] --> B{"Можно вообще
не разделять?"} - B -->|"да"| C["локальная переменная
или актор"] - B -->|"нет"| D{"За состоянием
наблюдают?"} - D -->|"да"| E["MutableStateFlow
+ update"] - D -->|"нет"| F{"Одна переменная?"} - F -->|"да"| G["AtomicInteger / AtomicReference"] - F -->|"нет, составная операция"| H{"Нужен suspend
внутри секции?"} - H -->|"да"| I["Mutex.withLock"] - H -->|"нет"| J["Mutex или
конфайнмент в поток"] -``` - -## Неизменяемость убирает проблему -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update - -data class Stats(val requests: Int = 0, val errors: Int = 0) { - val successRate: Double - get() = if (requests == 0) 0.0 else (requests - errors).toDouble() / requests -} - -fun main() = runBlocking { - val stats = MutableStateFlow(Stats()) - - withContext(Dispatchers.Default) { - List(100) { i -> - launch { - stats.update { it.copy(requests = it.requests + 1, errors = it.errors + i % 10 / 9) } - } - }.forEach { it.join() } - } - val final = stats.value - println("запросов: ${final.requests}, ошибок: ${final.errors}") - println("согласованный снимок: ${final.requests == 100}") - // запросов: 100, ошибок: 10 - // согласованный снимок: true -} -``` -Два связанных поля обновляются одним атомарным `copy`, поэтому несогласованного состояния («запросов 5, ошибок 7») не возникает никогда. Это главный довод за неизменяемые `data class` в качестве состояния. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md) - -## Коллекции -```kotlin -import kotlinx.coroutines.* -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import java.util.concurrent.ConcurrentHashMap - -fun main() = runBlocking { - // обычный MutableList не потокобезопасен - val unsafe = mutableListOf() - withContext(Dispatchers.Default) { - List(100) { i -> launch { repeat(100) { unsafe.add(i) } } }.forEach { it.join() } - } - println("размер может быть неверным: ${unsafe.size <= 10_000}") - - // вариант 1: Mutex - val mutex = Mutex() - val guarded = mutableListOf() - withContext(Dispatchers.Default) { - List(100) { i -> launch { repeat(100) { mutex.withLock { guarded.add(i) } } } } - .forEach { it.join() } - } - println("с Mutex точно: ${guarded.size == 10_000}") - - // вариант 2: потокобезопасная коллекция из JDK - val concurrent = ConcurrentHashMap() - withContext(Dispatchers.Default) { - List(100) { i -> launch { concurrent[i] = i } }.forEach { it.join() } - } - println("ConcurrentHashMap точно: ${concurrent.size == 100}") - // размер может быть неверным: true - // с Mutex точно: true - // ConcurrentHashMap точно: true -} -``` -В Kotlin нет своих потокобезопасных коллекций для JVM - используются `java.util.concurrent`. Но учтите: `ConcurrentHashMap` безопасен для отдельных операций, а составные («прочитать, изменить, записать») всё равно требуют `compute`/`merge` или мьютекса. Подробнее: [Изменяемость](../Коллекции/Изменяемость.md) - -## Что не нужно делать -```kotlin -import kotlinx.coroutines.* -import kotlin.system.measureTimeMillis - -fun main() = runBlocking { - var counter = 0 - val lock = Any() - - val time = measureTimeMillis { - withContext(Dispatchers.Default) { - List(100) { launch { repeat(1000) { synchronized(lock) { counter++ } } } } - .forEach { it.join() } - } - } - println("результат верный: ${counter == 100_000}") - println("но поток блокировался: ${time >= 0}") - // результат верный: true - // но поток блокировался: true -} -``` -`synchronized` работает и даёт правильный результат, но блокирует поток диспетчера: пока корутина ждёт монитор, поток не может выполнять другие корутины. На ограниченном диспетчере это прямой путь к deadlock, а `suspend`-вызов внутри `synchronized` вообще не скомпилируется. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `synchronized` (блокирует поток) | `Mutex.withLock` (приостанавливает) | -> | `ReentrantLock` | `Mutex` (не реентерабелен) | -> | `Semaphore` из `j.u.c.` (блокирует) | `kotlinx.coroutines.sync.Semaphore` | -> | `AtomicInteger` | он же, или `MutableStateFlow` | -> | `volatile` | `@Volatile` или `StateFlow` | -> | `ThreadLocal` для конфайнмента | элемент контекста корутины | -> | `ConcurrentHashMap` | он же | -> | нет аналога | актор на `Channel` | -> -> **`synchronized` занимает поток, `Mutex` - нет.** Это принципиальное различие. В пуле из 8 потоков восемь корутин, ждущих монитор, парализуют весь диспетчер - включая корутины, которым этот монитор не нужен. `Mutex` приостанавливает корутину и освобождает поток: -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.sync.Mutex -> import kotlinx.coroutines.sync.withLock -> -> fun main() = runBlocking { -> val mutex = Mutex() -> val single = Dispatchers.Default.limitedParallelism(1) -> var progress = "" -> withContext(single) { -> launch { mutex.withLock { delay(30); progress += "A" } } -> launch { mutex.withLock { progress += "B" } } -> launch { progress += "C" } // не ждёт мьютекс -> } -> println("порядок выполнения: $progress") -> // порядок выполнения: CAB -> } -> ``` -> На одном потоке корутина `C` выполнилась первой, пока `A` держала мьютекс и ждала на `delay`. С `synchronized` и `Thread.sleep` поток был бы занят и `C` стояла бы в очереди. -> -> **`suspend` внутри `synchronized` не компилируется.** Это не ограничение, а защита: приостановка освободила бы поток, а монитор остался бы захваченным - потенциальный deadlock, который JVM не заметит. `Mutex` снимает запрет, потому что владение не привязано к потоку. -> -> **`ReentrantLock` реентерабелен, `Mutex` - нет.** В Java повторный захват тем же потоком разрешён, что делает рекурсивные вызовы под локом безопасными. В корутинах понятие «тот же владелец» не определено: корутина мигрирует между потоками, а `withLock` в дочерней корутине - это уже другой владелец. Поэтому `Mutex` принципиально не реентерабелен, и рекурсию под локом приходится исключать архитектурно. -> -> **`volatile` против `StateFlow`.** `@Volatile` в Kotlin есть и работает так же: гарантирует видимость записи между потоками, но не атомарность составных операций. `MutableStateFlow` даёт и видимость, и атомарный `update`, и оповещение - три вещи вместо одной. -> -> **Актор не имеет аналога в Java.** Паттерн «состояние внутри потока, снаружи только сообщения» в Java требует `BlockingQueue` + выделенный поток + «отравленную пилюлю» для остановки + ручную реализацию ответов (обычно через `CompletableFuture` в сообщении). В Kotlin это `Channel` + `launch` + `CompletableDeferred` - примерно двадцать строк и никаких потоков. -> -> **Проблемы те же.** Корутины не отменяют ни гонок, ни deadlock'ов, ни инверсии приоритетов. Два мьютекса, захваченные в разном порядке, дают взаимную блокировку точно так же, как два `synchronized`: -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.sync.Mutex -> import kotlinx.coroutines.sync.withLock -> -> fun main() = runBlocking { -> val a = Mutex() -> val b = Mutex() -> val result = withTimeoutOrNull(100) { -> coroutineScope { -> launch { a.withLock { delay(20); b.withLock { } } } -> launch { b.withLock { delay(20); a.withLock { } } } -> } -> } -> println("классический deadlock: ${result == null}") // классический deadlock: true -> coroutineContext.cancelChildren() -> } -> ``` -> Разница только в том, что корутинный deadlock можно разорвать таймаутом, а поток, стоящий на мониторе, прервать нельзя. -> -> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md), [JVM](../../Программирование%20на%20Java/JVM/JVM.md) - -> [!warning] Подводные камни -> **`Mutex` не реентерабелен - повторный захват это deadlock:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.sync.Mutex -> import kotlinx.coroutines.sync.withLock -> -> class Service { -> private val mutex = Mutex() -> suspend fun outer() = mutex.withLock { inner() } -> suspend fun inner() = mutex.withLock { "никогда" } // тот же мьютекс -> } -> -> fun main() = runBlocking { -> val result = withTimeoutOrNull(100) { Service().outer() } -> println("повисло: ${result == null}") // повисло: true -> } -> ``` -> Публичный метод, берущий лок, не должен вызывать другой публичный метод того же класса. Выносите логику в приватные методы без блокировки. -> -> **`synchronized` в корутине блокирует поток диспетчера.** Результат будет верным, но пропускная способность упадёт, а на `limitedParallelism(1)` возможен deadlock. Используйте `Mutex`. -> -> **`suspend` внутри `synchronized` не компилируется** - и это хорошо. Попытка обойти запрет через `runBlocking` внутри `synchronized` создаёт именно тот deadlock, от которого защищает компилятор. -> -> **Отмена внутри `withLock` освобождает лок, но работа может остаться незавершённой:** -> ```kotlin -> import kotlinx.coroutines.* -> import kotlinx.coroutines.sync.Mutex -> import kotlinx.coroutines.sync.withLock -> -> fun main() = runBlocking { -> val mutex = Mutex() -> var step = 0 -> val job = launch { -> mutex.withLock { -> step = 1 -> delay(1000) // здесь отмена -> step = 2 // не выполнится -> } -> } -> delay(20) -> job.cancelAndJoin() -> println("лок свободен: ${!mutex.isLocked}, состояние на середине: ${step == 1}") -> // лок свободен: true, состояние на середине: true -> } -> ``` -> Критическая секция прервалась на середине, оставив несогласованное состояние. Либо делайте секцию атомарной (одна операция `copy`), либо восстанавливайте инвариант в `finally`. -> -> **`AtomicInteger` защищает одну переменную, а не инвариант:** -> ```kotlin -> import kotlinx.coroutines.* -> import java.util.concurrent.atomic.AtomicInteger -> -> fun main() = runBlocking { -> val total = AtomicInteger(0) -> val errors = AtomicInteger(0) -> withContext(Dispatchers.Default) { -> List(100) { launch { total.incrementAndGet(); errors.incrementAndGet() } } -> .forEach { it.join() } -> } -> println("каждый атомарен: ${total.get()} ${errors.get()}") -> // каждый атомарен: 100 100 -> } -> ``` -> Итог верный, но в любой момент между двумя инкрементами снимок несогласован: читатель может увидеть `total = 50, errors = 49`. Для связанных значений нужен один объект состояния. -> -> **`ConcurrentHashMap` не делает составные операции атомарными.** `if (!map.containsKey(k)) map[k] = v` - гонка. Нужны `computeIfAbsent`, `putIfAbsent` или `merge`. -> -> **Обычный `mutableListOf` в корутинах небезопасен**, даже если кажется, что обращения редки. `ArrayList.add` при конкурентном вызове может потерять элемент или бросить `ArrayIndexOutOfBoundsException` из-за состояния расширения массива. -> -> **Мелкозернистый конфайнмент убивает производительность.** `withContext(single) { counter++ }` внутри цикла - это переключение контекста на каждую операцию. Выносите весь блок работы в один `withContext`. -> -> **Два мьютекса в разном порядке - deadlock**, как и в Java. Правило то же: фиксированный порядок захвата или единый лок. -> -> **`Semaphore` с утечкой разрешений исчерпывается.** Ручная пара `acquire()`/`release()` без `try/finally` при исключении потеряет разрешение навсегда. Используйте `withPermit`. -> -> **`limitedParallelism(1)` - не замена `Mutex`.** Он гарантирует один поток для **своих** задач, но состояние может меняться и из других контекстов. Конфайнмент работает только если **все** обращения идут через этот диспетчер. -> -> **`@Volatile` не делает `++` атомарным.** Он даёт только видимость записи. `@Volatile var counter = 0` с `counter++` из нескольких корутин теряет инкременты точно так же, как обычное поле. -> -> **Актор сериализует все операции.** Если команд много, одна корутина станет узким местом - её очередь будет расти. Для высокой нагрузки нужны шардирование по ключу или атомарные типы. - -Далее: [Корутины против потоков](Корутины%20против%20потоков.md) diff --git a/Kotlin/Корутины/Структурированная конкурентность.md b/Kotlin/Корутины/Структурированная конкурентность.md deleted file mode 100644 index 45e0c11..0000000 --- a/Kotlin/Корутины/Структурированная конкурентность.md +++ /dev/null @@ -1,478 +0,0 @@ ---- -title: Структурированная конкурентность -description: CoroutineScope, иерархия Job, coroutineScope и supervisorScope, жизненный цикл и GlobalScope -tags: - - kotlin - - coroutines ---- - -Структурированная конкурентность - главное архитектурное решение корутин: каждая корутина принадлежит области видимости (scope) и имеет родителя. Из этого следуют три гарантии: - -1. родитель не завершится, пока не завершатся дети -2. отмена родителя отменяет всех детей -3. ошибка ребёнка доходит до родителя - -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - println("начало") - coroutineScope { - launch { delay(100); println("медленный ребёнок") } - launch { delay(20); println("быстрый ребёнок") } - } - println("конец — только после всех детей") - // начало - // быстрый ребёнок - // медленный ребёнок - // конец — только после всех детей -} -``` -Без этого свойства асинхронный код неизбежно порождает «потерянные» задачи: никто не знает, сколько их работает и когда они закончатся. - -## CoroutineScope -Scope - это объект с одним свойством `coroutineContext`, в котором есть `Job`. Билдеры (`launch`, `async`) - расширения на `CoroutineScope`, поэтому без scope их просто не вызвать. - -```kotlin -import kotlinx.coroutines.* - -class DataLoader : CoroutineScope { - private val job = SupervisorJob() - override val coroutineContext = job + Dispatchers.Default - - fun load(id: Int) = launch { - delay(20L * id) // разные задержки — порядок предсказуем - println("загружено $id") - } - - fun close() { - job.cancel() // отменяет всё, что запущено этим scope - } -} - -fun main() = runBlocking { - val loader = DataLoader() - loader.load(1) - loader.load(2) - delay(80) - loader.close() - println("загрузчик закрыт") - // загружено 1 - // загружено 2 - // загрузчик закрыт -} -``` -Такой класс владеет своими корутинами: `close()` гарантированно останавливает всё. Это основной способ связать корутины с жизненным циклом компонента. - -Чаще используют не наследование, а поле - так интерфейс класса не загрязняется методами scope: -```kotlin -import kotlinx.coroutines.* - -class Service2 { - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - - fun start() { - scope.launch { - delay(20) - println("работа выполнена") - } - } - - fun stop() = scope.cancel() -} - -fun main() = runBlocking { - val s = Service2() - s.start() - delay(50) - s.stop() - println("остановлено") - // работа выполнена - // остановлено -} -``` -Функция `CoroutineScope(context)` - фабрика, а не конструктор интерфейса. Обратите внимание: в `SupervisorJob()` - именно скобки, иначе получится ссылка на конструктор. - -## coroutineScope: вложенная область -`coroutineScope { }` - `suspend`-функция, которая создаёт дочернюю область и ждёт её завершения: -```kotlin -import kotlinx.coroutines.* - -suspend fun loadAll(): List = coroutineScope { - val a = async { delay(50); "первый" } - val b = async { delay(30); "второй" } - listOf(a.await(), b.await()) -} - -fun main() = runBlocking { - println(loadAll()) // [первый, второй] -} -``` -Это стандартный способ сделать `suspend`-функцию, которая внутри работает параллельно: снаружи она выглядит как обычная функция, возвращающая значение, и гарантированно не оставляет фоновых задач после возврата. - -Ошибка внутри отменяет остальных детей: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - try { - coroutineScope { - launch { - try { - delay(1000) - println("не напечатается") - } finally { - println("ребёнка 1 отменили") - } - } - launch { - delay(20) - throw IllegalStateException("ошибка во втором") - } - } - } catch (e: IllegalStateException) { - println("поймано: ${e.message}") - } - // ребёнка 1 отменили - // поймано: ошибка во втором -} -``` -Это свойство «всё или ничего»: если часть параллельной работы провалилась, продолжать остальную обычно бессмысленно. - -## supervisorScope: независимые дети -Когда дети не должны зависеть друг от друга, нужен `supervisorScope`: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - supervisorScope { - launch { - delay(20) - throw IllegalStateException("первый упал") - } - launch { - delay(50) - println("второй выжил") - } - } - println("область завершилась") - // Exception in thread ... IllegalStateException: первый упал - // второй выжил - // область завершилась -} -``` -Ошибка первого не затронула второго. Обратите внимание: исключение всё равно печатается - его нужно обработать, иначе оно уйдёт в обработчик по умолчанию. Подробнее: [Исключения в корутинах](Исключения%20в%20корутинах.md) - -| | `coroutineScope` | `supervisorScope` | -|---|---|---| -| ошибка ребёнка | отменяет остальных детей и область | не затрагивает остальных | -| ошибка пробрасывается наверх | да, из блока | нет, идёт в `CoroutineExceptionHandler` | -| отмена сверху | отменяет всех детей | отменяет всех детей | -| когда применять | части одной задачи | независимые подзадачи | - -## Иерархия Job -`Job` - дескриптор корутины: её состояние и связи с родителем и детьми. -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val parent = launch { - val child1 = launch { delay(1000) } - val child2 = launch { delay(1000) } - println("детей у родителя: ${coroutineContext[Job]!!.children.count()}") - child1.cancel() - println("child1 отменён: ${child1.isCancelled}") - println("child2 активен: ${child2.isActive}") - child2.cancel() - } - parent.join() - println("родитель завершён: ${parent.isCompleted}") - // детей у родителя: 2 - // child1 отменён: true - // child2 активен: true - // родитель завершён: true -} -``` - -Состояния `Job` и переходы между ними: - -```mermaid -stateDiagram-v2 - [*] --> New: "launch(start = LAZY)" - [*] --> Active: "launch / async" - New --> Active: "start() / join()" - Active --> Completing: "тело завершилось" - Completing --> Completed: "все дети завершились" - Active --> Cancelling: "cancel() / ошибка" - Completing --> Cancelling: "cancel() / ошибка ребёнка" - Cancelling --> Cancelled: "все дети отменены" - Completed --> [*] - Cancelled --> [*] -``` - -Флаги состояния: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val job = launch { delay(1000) } - println("${job.isActive} ${job.isCompleted} ${job.isCancelled}") // true false false - - job.cancel() - job.join() - println("${job.isActive} ${job.isCompleted} ${job.isCancelled}") // false true true -} -``` -Важная тонкость: у отменённой корутины `isCompleted` тоже `true` - завершение включает и успешное, и отменённое. Различать их нужно через `isCancelled`. - -## Job как часть контекста -`Job` можно передать явно, и тогда корутина станет ребёнком именно его: -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - val group = Job() - - launch(group) { delay(1000); println("не напечатается 1") } - launch(group) { delay(1000); println("не напечатается 2") } - val independent = launch { delay(30); println("независимая выполнилась") } - - delay(10) - group.cancel() // отменяет только своих детей - independent.join() - println("готово") - // независимая выполнилась - // готово -} -``` -Так делают «группы» задач, отменяемые вместе. Но осторожно: при явном `Job` корутина перестаёт быть ребёнком `runBlocking`, и тот не будет её ждать. - -## GlobalScope: почему его не надо -```kotlin -import kotlinx.coroutines.* - -fun main() = runBlocking { - @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch { - delay(1000) - println("этого вы не увидите") - } - delay(20) - println("main завершился, не дождавшись") - // main завершился, не дождавшись -} -``` -Корутина в `GlobalScope` живёт до конца процесса, её никто не отменит и никто не узнает об ошибке. Аннотация `@DelicateCoroutinesApi` поставлена специально - API помечено как опасное. - -Правильные альтернативы: -| Вместо `GlobalScope` | Используйте | -|---|---| -| фоновая задача в сервисе | свой `CoroutineScope` с явным `cancel()` | -| задача на время запроса | `coroutineScope` внутри обработчика | -| задача в Android ViewModel | `viewModelScope` | -| задача в Android UI | `lifecycleScope` | -| задача в тесте | `runTest` | -| утилита или `main` | `runBlocking` | - -Единственный оправданный случай - задача, которая действительно должна жить весь процесс (например, периодическая отправка метрик), и даже тогда лучше завести явный `applicationScope`. - -## Утечка через внешний launch -```kotlin -import kotlinx.coroutines.* - -// плохо: функция возвращается, а работа продолжается неизвестно где -fun CoroutineScope.badLoad(): Unit { - launch { delay(100); println("ещё работаю") } -} - -// хорошо: функция suspend и не оставляет хвостов -suspend fun goodLoad(): String = coroutineScope { - val d = async { delay(20); "результат" } - d.await() -} - -fun main() = runBlocking { - println(goodLoad()) // результат - badLoad() - println("badLoad вернулся, но задача ещё жива") - delay(150) - // результат - // badLoad вернулся, но задача ещё жива - // ещё работаю -} -``` -Правило: публичная функция, выполняющая асинхронную работу, должна быть `suspend` и возвращать результат. Расширение на `CoroutineScope`, запускающее `launch`, оправдано только там, где «запустить и забыть» - это и есть контракт. - -## Ожидание нескольких результатов -```kotlin -import kotlinx.coroutines.* - -suspend fun loadPage(n: Int): String { delay(20L * n); return "страница $n" } - -fun main() = runBlocking { - val pages = coroutineScope { - (1..3).map { async { loadPage(it) } }.awaitAll() - } - println(pages) // [страница 1, страница 2, страница 3] - - val results = coroutineScope { - (1..3).map { async { loadPage(it) } }.map { it.await() } - } - println(results) // [страница 1, страница 2, страница 3] -} -``` -`awaitAll` отличается от `map { it.await() }` поведением при ошибке: `awaitAll` падает сразу, как только упал любой из `Deferred`, а последовательные `await` дождутся своей очереди. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `ExecutorService` + `shutdown()` | `CoroutineScope` + `cancel()` | -> | `CountDownLatch`, `invokeAll` | `coroutineScope`, `awaitAll` | -> | `Future.cancel` по одной задаче | отмена по дереву | -> | `StructuredTaskScope` (Java 21+) | `coroutineScope` | -> | ошибка в `Runnable` теряется | ошибка идёт родителю | -> | задачи не знают о родителе | иерархия `Job` | -> -> **В Java задачи плоские.** `ExecutorService` - это пул: вы отправляете задачу и получаете `Future`. Если задача запустит ещё три, пул о них ничего не узнает, `shutdown()` их не дождётся, а `Future.cancel` на родителе их не остановит. Отслеживать такие связи приходится вручную - списками `Future`, `CountDownLatch` и флагами. -> -> **`StructuredTaskScope` в Java 21 - прямой аналог `coroutineScope`.** Он появился в том же Project Loom именно потому, что проблема была признана фундаментальной: -> ```java -> try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { -> var user = scope.fork(() -> loadUser(id)); -> var orders = scope.fork(() -> loadOrders(id)); -> scope.join().throwIfFailed(); -> return new Result(user.get(), orders.get()); -> } -> ``` -> Сравните с Kotlin: -> ```kotlin -> import kotlinx.coroutines.* -> -> suspend fun loadUser3(id: Int): String { delay(10); return "u$id" } -> suspend fun loadOrders3(id: Int): List { delay(10); return listOf("o$id") } -> -> suspend fun result(id: Int): String = coroutineScope { -> val user = async { loadUser3(id) } -> val orders = async { loadOrders3(id) } -> "${user.await()} ${orders.await()}" -> } -> -> fun main() = runBlocking { println(result(1)) } // u1 [o1] -> ``` -> Идея одна и та же: блок не завершится, пока не завершатся подзадачи. `ShutdownOnFailure` соответствует `coroutineScope`, `ShutdownOnSuccess` - гонке через `select`. Отличие в том, что в Kotlin это единственный способ работы, а в Java - новый опциональный API поверх существующих плоских пулов. -> -> **Ошибка в `Runnable` исчезает.** В `executor.submit(task)` исключение складывается в `Future`, и если `get()` никто не вызовет, об ошибке не узнает никто. В `executor.execute(task)` оно уйдёт в `UncaughtExceptionHandler` потока. Корутины гарантируют, что ошибка дойдёт до родителя и, если не обработана, до обработчика области. -> -> **Завершение пула против отмены области.** `shutdown()` ждёт текущих задач, `shutdownNow()` прерывает потоки, а прерывание работает только на блокирующих вызовах. `scope.cancel()` отменяет всё дерево кооперативно и предсказуемо. Подробнее: [Отмена и таймауты](Отмена%20и%20таймауты.md) -> -> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md) - -> [!warning] Подводные камни -> **`GlobalScope` - утечка по определению.** Задача не привязана ни к чему, не отменяется при закрытии компонента и может переживать его. В Android это классическая причина обращения к уничтоженному экрану. -> -> **`CoroutineScope(...)` без `cancel()` - та же утечка:** -> ```kotlin -> import kotlinx.coroutines.* -> -> class Leaky { -> private val scope = CoroutineScope(Dispatchers.Default) -> fun start() { scope.launch { delay(10_000) } } -> // нет метода stop — корутина живёт до конца процесса -> } -> -> fun main() = runBlocking { -> Leaky().start() -> println("объект больше не нужен, а корутина работает") -> delay(20) -> // объект больше не нужен, а корутина работает -> } -> ``` -> Если класс создаёт scope, у него обязан быть метод освобождения. -> -> **`Job()` вместо `SupervisorJob()` в собственном scope роняет весь scope при первой ошибке:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> val scope = CoroutineScope(Job()) -> scope.launch { throw IllegalStateException("первая задача") } -> delay(20) -> val second = scope.launch { println("не запустится") } -> delay(20) -> println("scope отменён: ${!scope.isActive}, вторая отменена: ${second.isCancelled}") -> // scope отменён: true, вторая отменена: true -> } -> ``` -> Для долгоживущего scope почти всегда нужен `SupervisorJob`, иначе одна ошибка выводит из строя весь компонент. -> -> **Передача `Job` в билдер разрывает связь с родителем:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> launch(Job()) { // свой Job — runBlocking НЕ ждёт -> delay(200) -> println("не напечатается") -> } -> println("main вышел") -> // main вышел -> } -> ``` -> `launch(Job())` выглядит безобидно, но делает корутину сиротой. Чтобы просто сменить диспетчер, передавайте только диспетчер. -> -> **`supervisorScope` не защищает от ошибки в своём теле:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> try { -> supervisorScope { -> launch { delay(1000) } -> throw IllegalStateException("ошибка в теле, не в ребёнке") -> } -> } catch (e: IllegalStateException) { -> println("поймано: ${e.message}") // поймано: ошибка в теле, не в ребёнке -> } -> } -> ``` -> Supervisor меняет поведение только для **детей**; код самого блока ведёт себя обычно и отменяет область. -> -> **`coroutineScope` внутри `launch` не делает дочерние ошибки локальными.** `coroutineScope` пробрасывает исключение наверх; если наверху `launch`, оно уйдёт родителю и может уронить всю область. Локализовать нужно через `try/catch` вокруг `coroutineScope` или через `supervisorScope`. -> -> **Наследование `CoroutineScope` классом публикует `launch` в API:** -> ```kotlin -> import kotlinx.coroutines.* -> -> class Exposed : CoroutineScope { -> override val coroutineContext = SupervisorJob() + Dispatchers.Default -> } -> -> fun main() = runBlocking { -> val e = Exposed() -> e.launch { println("любой может запустить корутину в чужом scope") } -> delay(20) -> e.cancel() -> // любой может запустить корутину в чужом scope -> } -> ``` -> Внешний код получает возможность запускать задачи в вашем scope. Поле `private val scope` предпочтительнее наследования. -> -> **`cancel()` на scope делает его непригодным навсегда:** -> ```kotlin -> import kotlinx.coroutines.* -> -> fun main() = runBlocking { -> val scope = CoroutineScope(SupervisorJob()) -> scope.cancel() -> val job = scope.launch { println("не выполнится") } -> delay(20) -> println("корутина сразу отменена: ${job.isCancelled}") // корутина сразу отменена: true -> } -> ``` -> Отменённый scope не «перезапускается» - нужно создавать новый. В Android это причина ошибок при повторном использовании ViewModel-scope. -> -> **`awaitAll` и `map { await() }` ведут себя по-разному при ошибке.** `awaitAll` падает при первой ошибке любого элемента; последовательные `await()` сначала дождутся предыдущих. Для fail-fast нужен `awaitAll`. -> -> **Возврат `Deferred` из публичной функции ломает структурированность.** Вызывающий может не вызвать `await()`, и ошибка потеряется, либо вызвать его после закрытия scope. Возвращайте готовое значение из `suspend`-функции. - -Далее: [Билдеры корутин](Билдеры%20корутин.md) diff --git a/Kotlin/ООП/Абстрактные классы.md b/Kotlin/ООП/Абстрактные классы.md deleted file mode 100644 index a84ca4b..0000000 --- a/Kotlin/ООП/Абстрактные классы.md +++ /dev/null @@ -1,306 +0,0 @@ ---- -title: Абстрактные классы -description: abstract против open, шаблонный метод и выбор между абстрактным классом и интерфейсом -tags: - - kotlin - - oop ---- - -Абстрактный класс нельзя создать - только унаследовать. - -```kotlin -abstract class Shape4 { - abstract val area: Double // без реализации - abstract fun name(): String - - fun describe() = "${name()}: площадь %.2f".format(java.util.Locale.US, area) -} - -class Circle3(val radius: Double) : Shape4() { - override val area get() = Math.PI * radius * radius - override fun name() = "круг" -} - -class Square3(val side: Double) : Shape4() { - override val area get() = side * side - override fun name() = "квадрат" -} - -fun main() { - val shapes = listOf(Circle3(1.0), Square3(2.0)) - shapes.forEach { println(it.describe()) } - // круг: площадь 3.14 - // квадрат: площадь 4.00 - // println(Shape4()) // ошибка: нельзя создать абстрактный класс -} -``` - -## abstract и open -`abstract` подразумевает `open` - писать его не нужно: -```kotlin -abstract class Base32 { - abstract fun required() // open не нужен, реализации нет - open fun optional() = "по умолчанию" // есть реализация, но переопределяема - fun fixed() = "не переопределить" // final -} - -class Impl6 : Base32() { - override fun required() = Unit - override fun optional() = "своё" -} - -fun main() { - val i = Impl6() - println("${i.optional()} / ${i.fixed()}") // своё / не переопределить -} -``` - -| Модификатор | Реализация | Нужно переопределять | -|---|---|---| -| `abstract` | нет | обязательно | -| `open` | есть | можно | -| нет модификатора | есть | нельзя | - -Абстрактный класс может не иметь ни одного абстрактного члена - тогда он просто запрещает создание экземпляра: -```kotlin -abstract class Utils { - fun helper() = "общая логика" -} - -class Concrete : Utils() - -fun main() { println(Concrete().helper()) } // общая логика -``` - -## Абстрактный класс может иметь состояние -Это главное отличие от интерфейса: -```kotlin -abstract class Repository2(protected val tableName: String) { - private val cache = mutableMapOf() // состояние - - protected abstract fun loadFromDb(id: Int): String? - - fun find(id: Int): String? = cache.getOrPut(id) { - loadFromDb(id) ?: return null - } - - fun cacheSize() = cache.size -} - -class UserRepository2 : Repository2("users") { - override fun loadFromDb(id: Int) = if (id == 1) "Иван" else null -} - -fun main() { - val repo = UserRepository2() - println(repo.find(1)) // Иван - println(repo.find(1)) // Иван — из кеша - println(repo.cacheSize()) // 1 - println(repo.find(99)) // null -} -``` -Конструктор, приватные поля, инициализация - всё доступно. У интерфейса этого нет. - -## Шаблонный метод -Самое частое применение: алгоритм зафиксирован в базовом классе, шаги переопределяются. -```kotlin -abstract class Importer { - fun import(data: String): String { - val parsed = parse(data) - val valid = parsed.filter { validate(it) } - return "загружено ${valid.size} из ${parsed.size}: $valid" - } - - protected abstract fun parse(data: String): List - protected open fun validate(item: String) = item.isNotBlank() -} - -class CsvImporter : Importer() { - override fun parse(data: String) = data.split(",").map { it.trim() } -} - -class StrictCsvImporter : Importer() { - override fun parse(data: String) = data.split(",").map { it.trim() } - override fun validate(item: String) = item.length >= 3 -} - -fun main() { - println(CsvImporter().import("a, b, , c")) - // загружено 3 из 4: [a, b, c] - println(StrictCsvImporter().import("abc, b, defg")) - // загружено 2 из 3: [abc, defg] -} -``` -`import` не `open` - порядок шагов менять нельзя. `parse` обязателен, `validate` имеет разумное поведение по умолчанию. - -## protected для членов, нужных подклассам -```kotlin -abstract class Connection { - protected var isOpen = false - private set - - protected abstract fun doConnect(): Boolean - - fun connect(): String { - isOpen = doConnect() - return if (isOpen) "подключено" else "ошибка" - } -} - -class TcpConnection(private val host: String) : Connection() { - override fun doConnect() = host.isNotBlank() - fun status() = if (isOpen) "$host: активно" else "$host: закрыто" -} - -fun main() { - val c = TcpConnection("example.com") - println(c.connect()) // подключено - println(c.status()) // example.com: активно -} -``` - -## Абстрактный класс или интерфейс -```kotlin -interface Drawable { fun draw(): String } // контракт - -abstract class Widget(val id: String) : Drawable { // + состояние и общая логика - protected var visible = true - fun hide() { visible = false } - override fun draw() = if (visible) render() else "" - protected abstract fun render(): String -} - -class Button(id: String, val label: String) : Widget(id) { - override fun render() = "[$label]" -} - -fun main() { - val b = Button("b1", "ОК") - println(b.draw()) // [ОК] - b.hide() - println("'${b.draw()}'") // '' -} -``` - -| | Интерфейс | Абстрактный класс | -|---|---|---| -| сколько можно реализовать | много | один | -| состояние (свойство с полем) | нет | да | -| конструктор | нет | да | -| `init` | нет | да | -| `private`-члены | да (с реализацией) | да | -| `protected`-члены | **нет** | да | -| видимость членов | только `public` или `private` | любая | - -Правило выбора: -- **интерфейс** - когда описываете возможность («умеет рисоваться», «сравним»), особенно если её могут захотеть добавить к существующим классам -- **абстрактный класс** - когда у наследников общее состояние или нужен шаблонный метод с `protected`-шагами - -На практике часто сочетают: публичный интерфейс для клиентов и абстрактный класс как база для реализаций. - -Подробнее: [Интерфейсы](Интерфейсы.md) - -## Абстрактный класс и sealed -```kotlin -sealed class Result4 { - abstract val isSuccess: Boolean -} - -class Ok2(val data: String) : Result4() { - override val isSuccess = true -} - -class Fail(val error: String) : Result4() { - override val isSuccess = false -} - -fun handle(r: Result4) = when (r) { // else не нужен - is Ok2 -> "данные: ${r.data}" - is Fail -> "ошибка: ${r.error}" -} - -fun main() { - println(handle(Ok2("готово"))) // данные: готово - println(Fail("сбой").isSuccess) // false -} -``` -`sealed` класс абстрактен по определению. Разница: `abstract` допускает наследников где угодно, `sealed` - только в своём модуле и пакете, зато даёт исчерпывающий `when`. Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `abstract class A` | то же | -> | `abstract void f();` | `abstract fun f()` | -> | поля `protected` | свойства `protected` | -> | абстрактное поле невозможно | `abstract val` | -> | `default`-методы в интерфейсе (Java 8+) | реализация в интерфейсе без ключевого слова | -> -> **`abstract val` - то, чего в Java нет.** Java позволяет объявить абстрактным только метод, поэтому «абстрактное свойство» выражается парой геттер/сеттер: -> ```java -> abstract class Shape { -> abstract double getArea(); // абстрактный геттер -> } -> ``` -> В Kotlin `abstract val area: Double` оставляет наследнику выбор: хранимое свойство, геттер или параметр конструктора. Клиент разницы не видит. -> -> **`abstract` не требует `open`**, потому что отсутствие реализации уже означает необходимость переопределения. В Java `final abstract` - ошибка компиляции по той же причине. -> -> **Доля абстрактных классов в Kotlin ниже.** Часть их задач закрывают другие средства: `sealed` для закрытых иерархий, делегирование для переиспользования кода, функции-расширения для добавления поведения без наследования. -> -> Сравнить: [Абстрактные классы в Java](../../Программирование%20на%20Java/ООП/Абстрактные%20классы.md) - -> [!warning] Подводные камни -> **Вызов абстрактного метода из `init` - гарантированная ловушка:** -> ```kotlin -> abstract class Base33 { -> init { println("из init: ${describe()}") } -> abstract fun describe(): String -> } -> -> class Derived32 : Base33() { -> private val name: String = "Иван" -> override fun describe() = "name=$name" -> } -> -> fun main() { Derived32() } // из init: name=null -> ``` -> Подкласс ещё не инициализирован, поэтому non-null свойство оказывается `null`. Подробнее: [Инициализация](Инициализация.md) -> -> **Абстрактный класс расходует единственный слот наследования.** Класс, унаследованный от абстрактного, больше ни от чего наследоваться не сможет. Поэтому публичный API лучше начинать с интерфейса. -> -> **`protected abstract` - часть контракта с наследниками**, и менять его так же больно, как публичный API: изменение сигнатуры сломает все подклассы. -> -> **Абстрактный класс с конструктором усложняет тестирование.** Чтобы проверить логику шаблонного метода, нужен наследник; с интерфейсом достаточно лямбды или объекта-выражения. -> -> **`abstract` и `private` несовместимы** - приватный член невозможно переопределить: -> ```kotlin -> abstract class Base34 { -> // private abstract fun f() // ошибка -> protected abstract fun f(): String -> fun run3() = f() -> } -> -> class Impl7 : Base34() { override fun f() = "готово" } -> -> fun main() { println(Impl7().run3()) } // готово -> ``` -> -> **Абстрактное свойство легко переопределить несогласованно:** -> ```kotlin -> abstract class Base35 { abstract val id: Int } -> -> class Unstable : Base35() { -> override val id: Int get() = (0..100).random() // разное при каждом чтении -> } -> -> fun main() { -> val u = Unstable() -> println(u.id == u.id) // чаще false -> } -> ``` -> Контракт «свойство стабильно» компилятором не проверяется; при использовании в `equals` или как ключа это даст тяжёлые для отладки ошибки. -> -> **Глубокие абстрактные иерархии трудны для чтения.** Если шаблонный метод вызывает абстрактные шаги, которые вызывают другие шаблонные методы, поведение собирается из четырёх файлов. - -Дополнительно: [Интерфейсы](Интерфейсы.md) -Далее: [Интерфейсы](Интерфейсы.md) diff --git a/Kotlin/ООП/Вложенные и внутренние классы.md b/Kotlin/ООП/Вложенные и внутренние классы.md deleted file mode 100644 index 883bdd3..0000000 --- a/Kotlin/ООП/Вложенные и внутренние классы.md +++ /dev/null @@ -1,284 +0,0 @@ ---- -title: Вложенные и внутренние классы -description: Вложенный класс по умолчанию, inner для ссылки на внешний объект и утечки памяти -tags: - - kotlin - - oop ---- - -Класс внутри класса по умолчанию **не** имеет ссылки на внешний объект. Чтобы её получить, нужен `inner`. - -```kotlin -class Outer5 { - private val secret = "скрыто" - - class Nested2 { - fun describe() = "вложенный, внешний объект недоступен" - // fun bad() = secret // ошибка: нет доступа - } - - inner class Inner5 { - fun describe() = "внутренний, вижу: $secret" - } -} - -fun main() { - println(Outer5.Nested2().describe()) // внешний объект не нужен - println(Outer5().Inner5().describe()) // нужен экземпляр Outer5 -} -``` - -| | вложенный (по умолчанию) | `inner` | -|---|---|---| -| ссылка на внешний объект | нет | есть | -| доступ к членам внешнего класса | только к членам `companion object` | ко всем, включая `private` | -| как создать | `Outer.Nested()` | `outer.Inner()` | -| аналог в Java | `static class` | обычный вложенный класс | -| риск утечки памяти | нет | есть | - -Обратите внимание на создание: `Outer5().Inner5()` - сначала внешний объект, затем внутренний на нём. - -## Зачем вложенные классы -Группировка по смыслу: класс нужен только в контексте внешнего и не имеет смысла отдельно. -```kotlin -class HttpClient2 { - class Request2(val url: String, val method: String = "GET") - class Response(val code: Int, val body: String) - - fun send(request: Request2): Response = - Response(200, "ответ на ${request.method} ${request.url}") -} - -fun main() { - val client = HttpClient2() - val response = client.send(HttpClient2.Request2("http://x.ru")) - println("${response.code}: ${response.body}") // 200: ответ на GET http://x.ru -} -``` -Имя `HttpClient2.Request2` само документирует принадлежность, и пакет не засоряется. - -Частый случай - варианты sealed-иерархии: -```kotlin -sealed interface State3 { - data object Loading3 : State3 - data class Ready(val data: String) : State3 - data class Error3(val message: String) : State3 -} - -fun render(s: State3) = when (s) { - State3.Loading3 -> "загрузка" - is State3.Ready -> "готово: ${s.data}" - is State3.Error3 -> "ошибка: ${s.message}" -} - -fun main() { - println(render(State3.Ready("данные"))) // готово: данные - println(render(State3.Loading3)) // загрузка -} -``` -Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md) - -## Зачем inner -Когда внутренний объект логически часть конкретного экземпляра и должен видеть его состояние. -```kotlin -class Table(private val name: String) { - private val rows = mutableListOf() - - inner class Row(private val data: String) { - fun insert(): String { - rows.add(data) // меняем состояние внешнего - return "в таблицу $name добавлено '$data', всего ${rows.size}" - } - } - - fun newRow(data: String) = Row(data) - fun count() = rows.size -} - -fun main() { - val t = Table("users") - println(t.newRow("Иван").insert()) // в таблицу users добавлено 'Иван', всего 1 - println(t.newRow("Пётр").insert()) // в таблицу users добавлено 'Пётр', всего 2 - println(t.count()) // 2 -} -``` - -## this в inner-классе -При совпадении имён нужна квалификация метками: -```kotlin -class Outer6(val name: String = "внешний") { - inner class Inner6(val name: String = "внутренний") { - fun describe() = "${this.name} / ${this@Outer6.name}" - } -} - -fun main() { - println(Outer6().Inner6().describe()) // внутренний / внешний -} -``` -`this@Outer6` - ссылка на внешний объект. Без метки `this` относится к ближайшему классу. - -То же для `super`: -```kotlin -open class Base36 { open fun f() = "база" } - -class Outer7 : Base36() { - override fun f() = "внешний" - - inner class Inner7 { - fun both() = "${f()} / ${super@Outer7.f()}" - } -} - -fun main() { println(Outer7().Inner7().both()) } // внешний / база -``` - -## Локальные классы -Класс можно объявить внутри функции: -```kotlin -fun process(items: List): String { - val prefix = ">> " - - class Formatter(val value: Int) { - fun format() = "$prefix$value" // захватывает prefix - } - - return items.map { Formatter(it).format() }.joinToString() -} - -fun main() { - println(process(listOf(1, 2))) // >> 1, >> 2 -} -``` -Локальный класс видит переменные функции, включая изменяемые. Нужен редко: обычно хватает [объекта-выражения](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md) или лямбды. - -## Вложенный интерфейс и object -```kotlin -class Service17 { - fun interface Listener { // интерфейс всегда "вложенный" - fun onEvent(name: String): String - } - - object Defaults { // object не может быть inner - const val TIMEOUT = 30 - } - - companion object { - fun create() = Service17() - } - - fun notify2(l: Listener) = l.onEvent("старт") -} - -fun main() { - val s = Service17.create() - println(s.notify2 { "получено: $it" }) // получено: старт - println(Service17.Defaults.TIMEOUT) // 30 -} -``` -Интерфейсы, `object`, `enum` и `data class` не могут быть `inner` - у них нет смысла быть привязанными к экземпляру. - -## Вложенность и дженерики -`inner`-класс видит параметры типа внешнего: -```kotlin -class Box3(private val value: T) { - inner class Wrapper { // T доступен - fun get(): T = value - fun describe() = "обёртка над ${value}" - } - - class Helper4(val other: U) // вложенный: свой параметр типа -} - -fun main() { - val b = Box3("данные") - println(b.Wrapper().get()) // данные - println(b.Wrapper().describe()) // обёртка над данные - println(Box3.Helper4(42).other) // 42 -} -``` -Вложенный класс параметров внешнего не видит - ему нужны свои. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `static class Nested` | `class Nested` (по умолчанию) | -> | `class Inner` (без static) | `inner class Inner` | -> | `outer.new Inner()` | `outer.Inner()` | -> | `Outer.this.field` | `this@Outer.field` | -> | `Outer.super.method()` | `super@Outer.method()` | -> | анонимный класс | объект-выражение | -> -> **Значения по умолчанию перевёрнуты - и это важно.** В Java вложенный класс по умолчанию держит ссылку на внешний объект, и чтобы этого избежать, нужно написать `static`. Разработчики об этом забывают, из-за чего возникают утечки памяти: внутренний объект, переживший внешний, не даёт собрать его сборщику мусора. -> -> В Kotlin по умолчанию ссылки нет - её нужно запросить словом `inner`. Безопасный вариант стал умолчанием, а опасный требует явного решения. Это особенно заметно в Android, где утечки активити через нестатические внутренние классы и анонимные обработчики были массовой проблемой. -> -> **`Outer.this` против `this@Outer`.** Синтаксис меток в Kotlin единообразен: та же `@`-нотация используется для `return@forEach` и `this@apply` в scope-функциях. -> -> Сравнить: [Вложенные классы в Java](../../Программирование%20на%20Java/ООП/Вложенные%20классы.md) - -> [!warning] Подводные камни -> **`inner` держит внешний объект в памяти - это утечка.** Если внутренний объект переживает внешний (попал в долгоживущую коллекцию, кеш или обработчик), внешний не будет собран: -> ```kotlin -> class Activity2(val heavyData: ByteArray = ByteArray(1024)) { -> inner class Handler3 { -> fun handle() = "обработка, внешний объект жив" -> } -> } -> -> val globalHandlers = mutableListOf() -> -> fun main() { -> globalHandlers.add(Activity2().Handler3()) -> println(globalHandlers.first().handle()) -> println("Activity2 не может быть собран: он держится ссылкой из Handler3") -> } -> ``` -> Правило: `inner` только когда нужен доступ к состоянию внешнего объекта и время жизни внутреннего не превышает внешнего. -> -> **Вложенный класс не видит членов экземпляра внешнего класса** - частая ошибка при переносе кода из Java, где поведение обратное: -> ```kotlin -> class Outer8 { -> private val value = 1 -> class Nested3 { -> // fun bad() = value // ошибка -> fun ok() = "нужен inner или параметр" -> } -> } -> -> fun main() { println(Outer8.Nested3().ok()) } -> ``` -> -> **Создание `inner`-класса требует экземпляра внешнего:** -> ```kotlin -> class Outer9 { -> inner class Inner8 -> class Nested4 -> } -> -> fun main() { -> println(Outer9.Nested4()::class.simpleName) // Nested4 -> println(Outer9().Inner8()::class.simpleName) // Inner8 -> // Outer9.Inner8() // ошибка -> } -> ``` -> -> **`data class` не может быть `inner`:** -> ```kotlin -> class Outer10 { -> data class Point5(val x: Int) // вложенный — ок -> // inner data class Bad(val x: Int) // ошибка -> } -> -> fun main() { println(Outer10.Point5(1)) } // Point5(x=1) -> ``` -> Сгенерированные `equals` и `copy` не учитывали бы ссылку на внешний объект, что дало бы противоречивое поведение. -> -> **Глубокая вложенность ухудшает читаемость.** `Outer.Middle.Inner.Deep` в сигнатурах - сигнал, что классы стоит вынести на верхний уровень или в отдельный файл. -> -> **Вложенный класс попадает в публичный API под составным именем.** Переименование внешнего класса меняет полное имя вложенного - для библиотеки это ломающее изменение. -> -> **Локальный класс в функции захватывает переменные** и продлевает их жизнь, как и лямбда. Если объект локального класса возвращается из функции, захваченные значения остаются в памяти. - -Дополнительно: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md) -Далее: [Полиморфизм](Полиморфизм.md) diff --git a/Kotlin/ООП/Делегирование.md b/Kotlin/ООП/Делегирование.md deleted file mode 100644 index 51f21a6..0000000 --- a/Kotlin/ООП/Делегирование.md +++ /dev/null @@ -1,372 +0,0 @@ ---- -title: Делегирование -description: Делегирование интерфейсов через by, делегаты свойств и границы автоматического перенаправления -tags: - - kotlin - - oop ---- - -Делегирование - передача работы другому объекту. Kotlin поддерживает его на уровне языка словом `by`, поэтому композиция требует не больше кода, чем наследование. - -## Делегирование интерфейса -```kotlin -interface Logger5 { - fun log(msg: String): String - fun warn(msg: String): String -} - -class ConsoleLogger3 : Logger5 { - override fun log(msg: String) = "[LOG] $msg" - override fun warn(msg: String) = "[WARN] $msg" -} - -class Service15(logger: Logger5) : Logger5 by logger { - fun work() = log("начинаю работу") -} - -fun main() { - val s = Service15(ConsoleLogger3()) - println(s.work()) // [LOG] начинаю работу - println(s.warn("внимание")) // [WARN] внимание — метод перенаправлен -} -``` -`Service15` реализует `Logger5`, но код методов не пишет: компилятор генерирует перенаправление на `logger`. Унаследованной реализации при этом нет - есть ссылка на другой объект. - -## Переопределение части методов -Делегировать можно всё, кроме того, что нужно изменить: -```kotlin -interface Repository3 { - fun find(id: Int): String? - fun save(id: Int, value: String): String -} - -class DbRepository : Repository3 { - private val data = mutableMapOf() - override fun find(id: Int) = data[id] - override fun save(id: Int, value: String): String { - data[id] = value - return "сохранено" - } -} - -class CachingRepository(private val origin: Repository3) : Repository3 by origin { - private val cache = mutableMapOf() - - override fun find(id: Int): String? = // только этот метод свой - cache.getOrPut(id) { origin.find(id) ?: return null } -} - -fun main() { - val repo = CachingRepository(DbRepository()) - println(repo.save(1, "Иван")) // сохранено — делегировано - println(repo.find(1)) // Иван — через кеш - println(repo.find(1)) // Иван — уже из кеша - println(repo.find(99)) // null -} -``` -Это декоратор в три строки. На наследовании он потребовал бы `open`-класса и был бы связан с его внутренностями. - -## Делегирование нескольких интерфейсов -```kotlin -interface Reader { fun read(): String } -interface Writer2 { fun write(data: String): String } - -class FileReader2 : Reader { override fun read() = "данные из файла" } -class FileWriter2 : Writer2 { override fun write(data: String) = "записано: $data" } - -class ReadWriteFile( - reader: Reader, - writer: Writer2, -) : Reader by reader, Writer2 by writer - -fun main() { - val f = ReadWriteFile(FileReader2(), FileWriter2()) - println(f.read()) // данные из файла - println(f.write("текст")) // записано: текст -} -``` -Это даёт эффект множественного наследования реализации - но без его проблем, потому что каждый интерфейс обслуживает отдельный объект. - -## Расширение коллекции -Классическая задача, которую нельзя безопасно решить наследованием: -```kotlin -class CountingList3(private val inner: MutableList = mutableListOf()) - : MutableList by inner { - - var addedCount = 0 - private set - - override fun add(element: T): Boolean { - addedCount++ - return inner.add(element) - } - - override fun addAll(elements: Collection): Boolean { - addedCount += elements.size - return inner.addAll(elements) - } -} - -fun main() { - val list = CountingList3() - list.add(1) - list.addAll(listOf(2, 3)) - println("${list.size} элементов, добавлений: ${list.addedCount}") // 3 элементов, добавлений: 3 - println(list.toList()) // [1, 2, 3] -} -``` -При наследовании от `ArrayList` счётчик мог бы удвоиться, если `addAll` внутри вызывает `add`. С делегированием внутренние вызовы `inner` остаются внутри `inner` и наших переопределений не касаются. - -Обратите внимание на `list.toList()`: сам `println(list)` вывел бы `CountingList3@5fa7e7ff`, потому что `toString` - член `Any`, а члены `Any` делегирование не перенаправляет. - -Эта же особенность - источник главной ловушки делегирования, см. подводные камни. - -## Делегаты свойств -Тот же `by` для свойства передаёт работу объекту с методами `getValue` и `setValue`. - -### Готовые делегаты -```kotlin -import kotlin.properties.Delegates - -class Form3 { - val config: String by lazy { "вычислено один раз" } - - var name: String by Delegates.observable("") { _, old, new -> - println("name: '$old' -> '$new'") - } - - var age: Int by Delegates.vetoable(0) { _, _, new -> new in 0..150 } - - var port: Int by Delegates.notNull() -} - -fun main() { - val f = Form3() - println(f.config) // вычислено один раз - f.name = "Иван" // name: '' -> 'Иван' - f.age = 200 // отклонено - println(f.age) // 0 - f.port = 8080 - println(f.port) // 8080 -} -``` -Подробнее: [Свойства](Свойства.md) - -### Делегирование свойства в Map -```kotlin -class Settings(private val map: Map) { - val host: String by map - val port: Int by map -} - -fun main() { - val s = Settings(mapOf("host" to "localhost", "port" to 8080)) - println("${s.host}:${s.port}") // localhost:8080 -} -``` -Имя свойства становится ключом. Для `var` нужен `MutableMap`. - -### Свой делегат -Нужны операторы `getValue` и (для `var`) `setValue`: -```kotlin -import kotlin.reflect.KProperty - -class NonBlank(private var value: String = "") { - operator fun getValue(thisRef: Any?, property: KProperty<*>) = value - - operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) { - require(newValue.isNotBlank()) { "${property.name} не может быть пустым" } - value = newValue.trim() - } -} - -class User6 { - var name: String by NonBlank() -} - -fun main() { - val u = User6() - u.name = " Иван " - println("'${u.name}'") // 'Иван' - try { - u.name = " " - } catch (e: IllegalArgumentException) { - println(e.message) // name не может быть пустым - } -} -``` -`property` даёт имя и метаданные свойства - удобно для сообщений об ошибках и для чтения из конфигурации по имени. - -Для делегата только на чтение достаточно реализовать `ReadOnlyProperty`: -```kotlin -import kotlin.properties.ReadOnlyProperty - -fun envVar(name: String) = ReadOnlyProperty { _, _ -> - System.getenv(name) ?: "<не задано>" -} - -class Config7 { - val home: String by envVar("HOME") - val missing: String by envVar("НЕТ_ТАКОЙ_ПЕРЕМЕННОЙ") -} - -fun main() { - println(Config7().home.isNotEmpty()) // true - println(Config7().missing) // <не задано> -} -``` - -### provideDelegate -Позволяет проверить свойство при создании делегата - например, что имя есть в конфигурации: -```kotlin -import kotlin.reflect.KProperty - -class Required(private val source: Map) { - operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): Required { - require(property.name in source) { "нет ключа '${property.name}'" } - return this - } - - operator fun getValue(thisRef: Any?, property: KProperty<*>) = source[property.name]!! -} - -fun main() { - val source = mapOf("host" to "localhost") - - class Ok3 { val host: String by Required(source) } - println(Ok3().host) // localhost - - try { - class Bad4 { val port: String by Required(source) } - Bad4() - } catch (e: IllegalArgumentException) { - println(e.message) // нет ключа 'port' - } -} -``` -Проверка происходит при создании объекта, а не при первом чтении - ошибка обнаруживается раньше. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | все методы перенаправлять вручную | `by` генерирует их | -> | декоратор = класс с полем и 10 методами | `class D(x: I) : I by x` | -> | `Supplier` + `volatile` для ленивости | `by lazy` | -> | `PropertyChangeSupport` | `Delegates.observable` | -> | нет аналога делегатов свойств | `by` с `getValue`/`setValue` | -> -> **Делегирование интерфейса - главная экономия.** В Java декоратор над интерфейсом из десяти методов требует десять методов-перенаправлений, даже если менять нужно один. Любой новый метод в интерфейсе нужно добавить во все декораторы. Kotlin генерирует перенаправление сам: -> ```java -> class CachingRepository implements Repository { -> private final Repository origin; -> CachingRepository(Repository origin) { this.origin = origin; } -> public String save(int id, String v) { return origin.save(id, v); } // шум -> public String find(int id) { /* своя логика */ } -> } -> ``` -> -> **Делегатов свойств в Java нет совсем.** Ленивое поле требует ручной двойной проверки блокировки, наблюдаемое - `PropertyChangeSupport` и явных вызовов. В Kotlin это `by lazy` и `by Delegates.observable`. -> -> **Паттерн Декоратор перестаёт быть паттерном** - он становится однострочной конструкцией языка, поэтому его применяют там, где в Java предпочли бы наследование «чтобы не писать много кода». -> -> Сравнить: [Полиморфизм подтипов в Java](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм%20подтипов.md), [Абстрактные классы](../../Программирование%20на%20Java/ООП/Абстрактные%20классы.md) - -> [!warning] Подводные камни -> **Делегат не знает о переопределениях - это главная ловушка.** Если один метод делегата вызывает другой, он вызовет **свою** версию, а не вашу: -> ```kotlin -> interface Sender { -> fun send(msg: String): String -> fun sendAll(msgs: List): String -> } -> -> class BasicSender : Sender { -> override fun send(msg: String) = "отправлено: $msg" -> override fun sendAll(msgs: List) = msgs.joinToString { send(it) } -> } -> -> class LoggingSender(private val inner: Sender) : Sender by inner { -> var logged = 0 -> private set -> -> override fun send(msg: String): String { -> logged++ -> return inner.send(msg) -> } -> } -> -> fun main() { -> val s = LoggingSender(BasicSender()) -> s.sendAll(listOf("a", "b")) -> println("залогировано: ${s.logged}") // 0 — наш send не вызвался! -> } -> ``` -> `sendAll` делегирован и внутри вызывает `send` объекта `inner`. Чтобы счётчик работал, нужно переопределить и `sendAll`. Это обратная сторона изоляции: наследование здесь сработало бы «как ожидается», но стало бы хрупким. -> -> **Члены `Any` не делегируются.** `toString`, `equals` и `hashCode` остаются реализациями по умолчанию, то есть по ссылке: -> ```kotlin -> class Wrapped(private val inner: MutableList = mutableListOf()) : MutableList by inner -> -> fun main() { -> val a = Wrapped().apply { add(1) } -> val b = Wrapped().apply { add(1) } -> println(a.toString()) // Wrapped@4554617c, а не [1] -> println(a == b) // false, хотя содержимое одинаково -> println(a == listOf(1)) // false -> } -> ``` -> Обёртка над коллекцией, попавшая в `Set` или в сравнение, поведёт себя не как коллекция. Эти три метода нужно переопределять вручную. Подробнее: [equals, hashCode и toString](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md) -> -> **Делегат фиксируется при создании** и не может быть заменён: -> ```kotlin -> fun interface Source { fun get(): String } -> -> class Service16(private var source: Source) : Source by source { -> fun replace(newSource: Source) { source = newSource } // не влияет на by! -> } -> -> fun main() { -> val s = Service16 { "первый" } -> s.replace { "второй" } -> println(s.get()) // первый — by хранит исходную ссылку -> } -> ``` -> `by` копирует ссылку в скрытое поле при создании объекта. Для подменяемого делегата нужно перенаправлять вручную. -> -> **Делегирование нельзя сделать `by this`** или сослаться на собственное свойство в заголовке - объект ещё не создан. -> -> **Из делегированных методов нет доступа к `this` обёртки.** Делегат не знает, что его кто-то оборачивает, - он не может вызвать методы обёртки. -> -> **`by lazy` на `var` невозможен** - только `val`. -> -> **Делегат свойства - объект на каждый экземпляр:** -> ```kotlin -> class Heavy { -> val a: String by lazy { "a" } -> val b: String by lazy { "b" } // ещё один объект Lazy -> } -> -> fun main() { println(Heavy().a + Heavy().b) } // ab -> ``` -> Для класса с десятками ленивых свойств, создаваемого миллионами экземпляров, это заметный расход памяти. -> -> **Делегирование в `Map` падает в рантайме при отсутствии ключа:** -> ```kotlin -> class Settings2(map: Map) { -> val host: String by map -> } -> -> fun main() { -> val s = Settings2(emptyMap()) -> try { -> println(s.host) -> } catch (e: NoSuchElementException) { -> println("ключ отсутствует") // ключ отсутствует -> } -> } -> ``` -> Проверить это заранее помогает `provideDelegate`. -> -> **Делегирование `MutableList` раскрывает полный изменяемый API.** Обёртка, делегирующая `MutableList`, позволяет внешнему коду добавлять элементы в обход вашей логики - через `iterator().remove()`, `subList()` и другие пути. - -Дополнительно: [Свойства](Свойства.md) -Далее: [Вложенные и внутренние классы](Вложенные%20и%20внутренние%20классы.md) diff --git a/Kotlin/ООП/Инициализация.md b/Kotlin/ООП/Инициализация.md deleted file mode 100644 index 31eb143..0000000 --- a/Kotlin/ООП/Инициализация.md +++ /dev/null @@ -1,320 +0,0 @@ ---- -title: Инициализация -description: Порядок выполнения init и инициализаторов, инициализация при наследовании и ловушка open-метода -tags: - - kotlin - - oop ---- - -Инициализаторы свойств и блоки `init` выполняются **в порядке их объявления** в теле класса - как единая последовательность. - -```kotlin -class Order4 { - val a = println("1: свойство a").let { "a" } - init { println("2: первый init") } - val b = println("3: свойство b").let { "b" } - init { println("4: второй init") } - - constructor() { println("5: тело конструктора") } -} - -fun main() { Order4() } -``` -Вывод: `1`, `2`, `3`, `4`, `5`. Тело вторичного конструктора идёт **последним**, после всех инициализаторов. - -Блоков `init` может быть несколько; их используют, когда инициализация не сводится к одному выражению. - -## Параметры конструктора доступны сразу -```kotlin -class User3(name: String, age: Int) { - val displayName = name.uppercase() - val category: String - - init { - category = when { - age < 18 -> "несовершеннолетний" - age < 65 -> "взрослый" - else -> "пенсионер" - } - } -} - -fun main() { - val u = User3("иван", 20) - println("${u.displayName}: ${u.category}") // ИВАН: взрослый -} -``` - -## Порядок при наследовании -Базовый класс инициализируется **полностью** до начала инициализации подкласса: -```kotlin -open class Base4 { - val baseField = println("1: свойство Base").let { "base" } - init { println("2: init Base") } -} - -class Derived4 : Base4() { - val derivedField = println("3: свойство Derived").let { "derived" } - init { println("4: init Derived") } -} - -fun main() { Derived4() } -``` -Вывод: `1`, `2`, `3`, `4`. Внутри каждого класса - строго порядок объявления; базовый класс целиком раньше подкласса. Переставьте `init` выше свойства - и номера пойдут как `2`, `1`, `4`, `3`. - -Аргументы для конструктора базового класса вычисляются **до** всего остального: -```kotlin -open class Base5(val value: String) { - init { println("2: init Base, value=$value") } -} - -class Derived5(input: String) : Base5(input.uppercase().also { println("1: аргумент=$it") }) { - init { println("3: init Derived") } -} - -fun main() { Derived5("иван") } -``` - -## Ловушка: open-метод в конструкторе -Это главная ошибка инициализации. Вызов `open`-метода из конструктора базового класса попадёт в переопределённую версию - **до** того, как подкласс инициализирован. - -```kotlin -open class Base6 { - init { show() } // опасно - open fun show() = println("Base") -} - -class Derived6 : Base6() { - val name: String = "Иван" - override fun show() = println("Derived, name='$name'") -} - -fun main() { Derived6() } -``` -Вывод: `Derived, name='null'`. Свойство `name` объявлено non-null, но в этот момент ещё не присвоено - и мы наблюдаем `null` в типе `String`. - -Если свойство подкласса имеет примитивный тип, вместо `null` будет `0` - и это ещё труднее заметить, потому что исключения не возникает: -```kotlin -open class Base7 { - init { println("размер при инициализации: ${size()}") } - open fun size() = 0 -} - -class Derived7 : Base7() { - private val count: Int = 3 - override fun size() = count -} - -fun main() { - val d = Derived7() - println("размер после: ${d.size()}") -} -``` -Вывод: `0`, затем `3`. Ошибки нет, просто неверное значение. - -А вот если переопределённый метод обращается к объекту, результат - `NullPointerException` в коде, где ни одного nullable-типа не видно: -```kotlin -open class Base7b { - init { println("размер: ${size()}") } - open fun size() = 0 -} - -class Derived7b : Base7b() { - private val items = listOf(1, 2, 3) - override fun size() = items.size // items ещё null -} - -fun main() { - try { - Derived7b() - } catch (e: NullPointerException) { - println("NPE: ${e.message}") - // NPE: Cannot invoke "java.util.List.size()" because "this.items" is null - } -} -``` -Та же проблема существует в Java; разница в том, что Kotlin позволяет её избежать, закрывая классы по умолчанию. - -Как не попадать: -- не вызывать `open`-члены из конструкторов и `init` -- делать такие методы `private` или `final` -- если поведение подкласса нужно при создании - передавать его как параметр конструктора - -```kotlin -open class Base8(private val label: String) { - init { println("метка: $label") } // параметр уже готов -} - -class Derived8 : Base8("Derived") { - val name = "Иван" -} - -fun main() { Derived8() } // метка: Derived -``` - -## Порядок объявления имеет значение -Свойство нельзя использовать до его объявления: -```kotlin -class Bad3 { - // val b = a + 1 // ошибка: variable 'a' must be initialized - val a = 1 - val b = a + 1 -} - -fun main() { println(Bad3().b) } // 2 -``` -Компилятор ловит это для прямых обращений. Но через метод проверка обходится: -```kotlin -class Sneaky { - val b = compute() // вызывается до инициализации a - val a = 10 - private fun compute() = a * 2 - - override fun toString() = "a=$a b=$b" -} - -fun main() { println(Sneaky()) } // a=10 b=0 -``` -`b` равно `0`, потому что `a` на момент вызова ещё не присвоено. Ошибки компиляции нет - компилятор не отслеживает порядок через вызовы методов. - -## Ленивая инициализация как решение -Если порядок становится проблемой, `by lazy` снимает её: значение вычисляется при первом обращении, когда объект уже собран. -```kotlin -class Fixed { - val b: Int by lazy { a * 2 } // вычислится после создания - val a = 10 - - override fun toString() = "a=$a b=$b" -} - -fun main() { println(Fixed()) } // a=10 b=20 -``` -Подробнее: [Свойства](Свойства.md) - -## Исключение в инициализации -```kotlin -class Validated(val age: Int) { - init { - require(age >= 0) { "возраст отрицательный: $age" } - println("объект создан") - } -} - -fun main() { - try { - Validated(-1) - } catch (e: IllegalArgumentException) { - println("не создан: ${e.message}") // не создан: возраст отрицательный: -1 - } - println(Validated(20).age) // объект создан, 20 -} -``` -Ссылка на объект не вернётся, но побочные эффекты, выполненные до исключения, останутся. Поэтому в `init` держат только проверки и присваивания - без регистрации в реестрах, открытия файлов и запуска потоков. - -## Инициализация object и companion object -```kotlin -object Registry2 { - init { println("инициализация Registry") } - val items = mutableListOf() -} - -class Service9 { - companion object { - init { println("инициализация companion") } - val created = 0 - } -} - -fun main() { - println("старт") - println(Registry2.items.size) // инициализация Registry, затем 0 - println(Service9.created) // инициализация companion, затем 0 -} -``` -Оба инициализируются лениво - при первом обращении, потокобезопасно средствами JVM. `companion object` инициализируется при первом обращении к нему или при первом создании экземпляра класса. - -Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | блок `{ }` в теле класса | `init { }` | -> | `static { }` | `init` в `companion object` | -> | присваивание в конструкторе | инициализатор свойства | -> | `super(args)` первой строкой | `: Base(args)` в заголовке | -> | нет проверки порядка | ошибка при прямом обращении вперёд | -> -> **Порядок инициализации одинаков**: базовый класс, потом подкласс; внутри класса - по порядку объявления. Проблема вызова переопределяемого метода из конструктора тоже одинакова. -> -> **Kotlin частично защищает от неинициализированных свойств.** Прямое обращение к свойству до его объявления - ошибка компиляции, тогда как Java молча даёт значение по умолчанию: -> ```java -> class Sneaky { -> int b = a + 1; // компилируется, b = 1, потому что a == 0 -> int a = 10; -> } -> ``` -> Через вызов метода обход возможен в обоих языках. -> -> **Главная защита - закрытые классы.** Ловушка с `open`-методом в конструкторе требует, чтобы класс был наследуемым. Поскольку в Kotlin класс закрыт по умолчанию, большинство классов в эту ловушку попасть не может в принципе. -> -> Сравнить: [Блоки инициализации в Java](../../Программирование%20на%20Java/ООП/Блоки%20инициализации.md), [Конструкторы](../../Программирование%20на%20Java/ООП/Конструкторы.md) - -> [!warning] Подводные камни -> **Вызов `open`-метода из `init` видит неинициализированный подкласс** - значение будет `null` даже у non-null свойства: -> ```kotlin -> open class Base9 { -> init { check() } -> open fun check() {} -> } -> -> class Derived9 : Base9() { -> private val name: String = "Иван" -> override fun check() { -> @Suppress("SENSELESS_COMPARISON") -> println("name инициализирован: " + (name != null)) -> } -> } -> -> fun main() { Derived9() } // name инициализирован: false -> ``` -> Это единственный штатный способ увидеть `null` в переменной типа `String`. -> -> **Порядок объявления свойств меняет поведение**, если между ними есть зависимость через методы. Перестановка двух строк может дать `0` вместо правильного значения - без ошибки компиляции. -> -> **Тело вторичного конструктора выполняется последним**, уже после всех `init`. Код, который ожидает обратного порядка, получит неожиданный результат. -> -> **`lateinit` не инициализируется автоматически** - обращение до присваивания даёт `UninitializedPropertyAccessException`. -> -> **Круговая зависимость между `object` даёт `null` или `0` без ошибки:** -> ```kotlin -> object First { val value: String = Second.value + "-first" } -> object Second { val value: String = "second" } -> -> fun main() { println(First.value) } // second-first -> ``` -> Если поменять зависимость местами, получится `null-second`: один из объектов будет читать поле другого во время его инициализации. -> -> **Исключение в `init` оставляет побочные эффекты.** Частая ошибка - регистрация `this` в глобальном реестре до валидации: объект создан не будет, а ссылка на него останется. -> -> **Утечка `this` из конструктора.** Передача `this` куда-либо в `init` открывает доступ к недосозданному объекту: -> ```kotlin -> class Leaky { -> val items = mutableListOf() -> init { register(this) } // объект ещё не готов -> val name: String = "готов" -> -> companion object { -> val all = mutableListOf() -> fun register(l: Leaky) = all.add(l) -> } -> } -> -> fun main() { -> Leaky() -> println("в реестре: ${Leaky.all.size}") // в реестре: 1 -> } -> ``` -> Другой поток, получивший такую ссылку, увидит `name` как `null`. - -Дополнительно: [Классы и конструкторы](Классы%20и%20конструкторы.md) -Далее: [Модификаторы видимости](Модификаторы%20видимости.md) diff --git a/Kotlin/ООП/Интерфейсы.md b/Kotlin/ООП/Интерфейсы.md deleted file mode 100644 index 175cb7f..0000000 --- a/Kotlin/ООП/Интерфейсы.md +++ /dev/null @@ -1,332 +0,0 @@ ---- -title: Интерфейсы -description: Реализация по умолчанию, свойства без состояния, ромбовидное наследование и fun interface -tags: - - kotlin - - oop ---- - -Интерфейс описывает контракт. В Kotlin он может содержать и реализацию. - -```kotlin -interface Drawable2 { - fun draw(): String // только объявление - fun show() = "рисую: ${draw()}" // реализация по умолчанию -} - -class Circle4 : Drawable2 { - override fun draw() = "круг" -} - -fun main() { - println(Circle4().show()) // рисую: круг -} -``` -Скобок после имени интерфейса в списке супертипов нет - конструктора у него не бывает. - -## Свойства в интерфейсе -Свойство объявить можно, но **хранить значение - нет**: -```kotlin -interface Named2 { - val name: String // абстрактное - val upper: String get() = name.uppercase() // через геттер — можно - // val cached: String = "x" // ошибка: поля в интерфейсе запрещены -} - -class User5(override val name: String) : Named2 - -class Admin2 : Named2 { - override val name get() = "админ" // геттер вместо поля -} - -fun main() { - println(User5("иван").upper) // ИВАН - println(Admin2().name) // админ -} -``` -Реализовать свойство можно параметром конструктора, хранимым свойством или геттером - интерфейсу это безразлично. - -## Множественная реализация -```kotlin -interface Readable2 { fun read(): String } -interface Writable { fun write(data: String): String } -interface Closeable2 { fun close() = "закрыто" } - -class File2(private val name: String) : Readable2, Writable, Closeable2 { - override fun read() = "чтение $name" - override fun write(data: String) = "запись '$data' в $name" -} - -fun main() { - val f = File2("a.txt") - println(f.read()) // чтение a.txt - println(f.write("текст")) // запись 'текст' в a.txt - println(f.close()) // закрыто -} -``` -Ограничений на число интерфейсов нет - в отличие от классов. - -## Ромбовидное наследование -Если два интерфейса дают реализацию одного метода, компилятор требует разрешить конфликт: -```kotlin -interface A7 { fun f() = "A" } -interface B7 { fun f() = "B" } - -class C7 : A7, B7 { - override fun f() = "${super.f()}${super.f()}" -} - -fun main() { println(C7().f()) } // AB -``` -Без `override` класс не скомпилируется: `class C7 : A7, B7` даст ошибку `inherits multiple interface methods`. - -Требование строгое: `override` нужен, даже если реализацию даёт только **один** из интерфейсов: -```kotlin -interface A8 { fun f(): String } // абстрактный -interface B8 { fun f() = "B" } // с реализацией - -// class C8 : A8, B8 // ошибка: inherits multiple interface methods -class C8 : A8, B8 { - override fun f() = super.f() // выбираем явно -} - -fun main() { println(C8().f()) } // B -``` -А вот реализация из **класса** абстрактный метод интерфейса покрывает - здесь писать `override` не нужно: -```kotlin -interface Named3 { fun name(): String } - -abstract class Base3 : Named3 { - override fun name() = "из класса" -} - -class Impl10 : Base3(), Named3 // реализация унаследована от Base3 - -fun main() { println(Impl10().name()) } // из класса -``` - -Для свойств правило то же: -```kotlin -interface A9 { val name: String get() = "A" } -interface B9 { val name: String get() = "B" } - -class C9 : A9, B9 { - override val name get() = super.name + super.name -} - -fun main() { println(C9().name) } // AB -``` - -## Наследование интерфейсов -```kotlin -interface Entity2 { val id: Int } - -interface Timestamped : Entity2 { - val createdAt: Long - fun age(now: Long) = now - createdAt -} - -class Record2(override val id: Int, override val createdAt: Long) : Timestamped - -fun main() { - val r = Record2(1, 1000) - println("${r.id} ${r.age(1500)}") // 1 500 -} -``` -Интерфейс может наследовать несколько интерфейсов - ограничения на один супертип здесь нет. - -## private-члены -```kotlin -interface Validator2 { - fun validate(value: String): Boolean - - private fun isBlank2(value: String) = value.trim().isEmpty() - - fun check(value: String) = when { - isBlank2(value) -> "пусто" - validate(value) -> "ок" - else -> "неверно" - } -} - -class EmailValidator : Validator2 { - override fun validate(value: String) = "@" in value -} - -fun main() { - val v = EmailValidator() - println(v.check(" ")) // пусто - println(v.check("a@b.ru")) // ок - println(v.check("нет")) // неверно -} -``` -`private` в интерфейсе - способ вынести вспомогательную логику, не включая её в контракт. `protected` в интерфейсе **запрещён**: все члены либо публичны, либо приватны. - -## fun interface - SAM -Интерфейс с единственным абстрактным методом можно пометить `fun`, и тогда вместо объекта допустима лямбда: -```kotlin -fun interface Transformer { - fun transform(input: String): String -} - -fun apply3(value: String, t: Transformer) = t.transform(value) - -fun main() { - println(apply3("abc", Transformer { it.uppercase() })) // ABC - println(apply3("abc") { it.reversed() }) // cba - - val obj = object : Transformer { // можно и так - override fun transform(input: String) = "[$input]" - } - println(apply3("abc", obj)) // [abc] -} -``` -Для своего кода обычно лучше функциональный тип `(String) -> String` - он не требует объявления интерфейса. `fun interface` нужен, когда: -- интерфейсу нужны дополнительные методы с реализацией -- API должен быть удобным из Java -- имя контракта важно для читаемости - -```kotlin -fun interface Predicate2 { - fun test(value: T): Boolean - fun negate(): Predicate2 = Predicate2 { !test(it) } // метод по умолчанию -} - -fun main() { - val positive = Predicate2 { it > 0 } - println(positive.test(5)) // true - println(positive.negate().test(5)) // false -} -``` - -## Интерфейсы в делегировании -```kotlin -interface Logger4 { fun log(msg: String): String } - -class ConsoleLogger2 : Logger4 { - override fun log(msg: String) = "[LOG] $msg" -} - -class Service14(logger: Logger4) : Logger4 by logger { - fun work() = log("работаю") -} - -fun main() { - println(Service14(ConsoleLogger2()).work()) // [LOG] работаю -} -``` -Подробнее: [Делегирование](Делегирование.md) - -## Интерфейс как граница модуля -```kotlin -interface PaymentGateway { - fun charge(amount: Long): String -} - -internal class StripeGateway : PaymentGateway { - override fun charge(amount: Long) = "Stripe: списано $amount" -} - -internal class TestGateway : PaymentGateway { - override fun charge(amount: Long) = "Тест: якобы списано $amount" -} - -fun createGateway(test: Boolean): PaymentGateway = - if (test) TestGateway() else StripeGateway() - -fun main() { - println(createGateway(test = true).charge(100)) // Тест: якобы списано 100 - println(createGateway(test = false).charge(100)) // Stripe: списано 100 -} -``` -Это основное практическое применение: клиент зависит от интерфейса, реализации скрыты за `internal` и подменяемы. Подробнее: [Модификаторы видимости](Модификаторы%20видимости.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `interface A { }` | то же | -> | `default void f() { }` | `fun f() { }` - слово не нужно | -> | `static` методы в интерфейсе | `companion object` в интерфейсе | -> | `private` методы (Java 9+) | то же | -> | поля `public static final` | только `const` в `companion object` | -> | `A.super.f()` | `super.f()` | -> | `@FunctionalInterface` | `fun interface` | -> | нет свойств | `val`/`var` в интерфейсе | -> -> **Реализация по умолчанию не требует ключевого слова.** В Java нужно `default`, потому что до версии 8 интерфейсы реализации не имели и слово понадобилось для совместимости. В Kotlin такого ограничения не было. -> -> **Свойства в интерфейсе** - то, чего в Java нет. Java-интерфейс объявляет геттер как метод, и реализация обязана быть методом: -> ```java -> interface Named { String getName(); } -> ``` -> Kotlin позволяет реализовать `val name: String` параметром конструктора - короче и без ручного геттера. -> -> **Константы в интерфейсе работают иначе.** В Java поле интерфейса неявно `public static final`, что породило антипаттерн «интерфейс-константа». В Kotlin поля в интерфейсе запрещены; константы живут в `companion object`: -> ```kotlin -> interface Config6 { -> companion object { const val MAX = 100 } -> } -> -> fun main() { println(Config6.MAX) } // 100 -> ``` -> -> **Реализация по умолчанию и совместимость с Java.** По умолчанию Kotlin компилирует её в статический метод плюс `DefaultImpls`-класс, а не в Java `default`-метод. Java-классу, реализующему такой интерфейс, придётся реализовать метод самому. Флаг `-Xjvm-default=all` меняет это поведение. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md) -> -> Сравнить: [Интерфейсы в Java](../../Программирование%20на%20Java/ООП/Интерфейсы.md), [Полиморфизм подтипов](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм%20подтипов.md) - -> [!warning] Подводные камни -> **Свойство в интерфейсе не хранит значение.** Попытка задать значение - ошибка: -> ```kotlin -> interface Counter2 { -> // var count: Int = 0 // ошибка: property initializers are not allowed -> var count: Int // только объявление -> } -> -> class Impl8 : Counter2 { override var count = 0 } -> -> fun main() { -> val c = Impl8() -> c.count++ -> println(c.count) // 1 -> } -> ``` -> -> **Геттер в интерфейсе вычисляется у каждой реализации заново** - это не кеш: -> ```kotlin -> interface Sized { -> val items: List -> val total: Int get() = items.sum().also { println("считаю") } -> } -> -> class Impl9 : Sized { override val items = listOf(1, 2) } -> -> fun main() { -> val i = Impl9() -> println(i.total) -> println(i.total) // "считаю" дважды -> } -> ``` -> Кешировать можно только в классе-реализации через `by lazy`. -> -> **Ромбовидный конфликт не решается автоматически** - нужен явный `override` с `super`. Это строгость, а не недостаток: язык не выбирает реализацию за вас. -> -> **`protected` в интерфейсе запрещён.** Если нужна защищённая логика для наследников, нужен абстрактный класс. Подробнее: [Абстрактные классы](Абстрактные%20классы.md) -> -> **Добавление метода в интерфейс - ломающее изменение** для всех реализаций, если у него нет реализации по умолчанию. Для публичного API это означает, что интерфейс стоит проектировать узким. -> -> **`fun interface` позволяет только один абстрактный метод.** Добавив второй, вы сломаете все вызовы с лямбдой: -> ```kotlin -> fun interface Handler2 { fun handle(x: Int): String } -> -> fun main() { -> val h = Handler2 { "получено $it" } -> println(h.handle(1)) // получено 1 -> } -> ``` -> -> **Интерфейс с реализацией по умолчанию - уже не чистый контракт.** Изменение такой реализации меняет поведение всех классов, которые её не переопределили, - то есть это наследование со всеми его рисками. -> -> **Из Java реализация по умолчанию не видна** без `-Xjvm-default=all`. Java-код будет требовать реализовать метод, который в Kotlin считается необязательным. - -Дополнительно: [Абстрактные классы](Абстрактные%20классы.md) -Далее: [Делегирование](Делегирование.md) diff --git a/Kotlin/ООП/Классы и конструкторы.md b/Kotlin/ООП/Классы и конструкторы.md deleted file mode 100644 index cefbac6..0000000 --- a/Kotlin/ООП/Классы и конструкторы.md +++ /dev/null @@ -1,326 +0,0 @@ ---- -title: Классы и конструкторы -description: Основной конструктор, параметры-свойства, вторичные конструкторы и фабрики -tags: - - kotlin - - oop ---- - -Минимальный класс - одна строка: -```kotlin -class Empty - -fun main() { - val e = Empty() // new не нужен - println(e::class.simpleName) // Empty -} -``` -Оператора `new` в Kotlin нет: конструктор вызывается как функция. - -## Основной конструктор -Объявляется в заголовке класса: -```kotlin -class User(name: String, age: Int) { - val displayName = name.uppercase() // параметры доступны в теле - val isAdult = age >= 18 -} - -fun main() { - val u = User("иван", 20) - println("${u.displayName} ${u.isAdult}") // ИВАН true -} -``` -Параметры `name` и `age` - **только параметры**: они видны в инициализаторах свойств и в блоках `init`, но не становятся членами класса. - -```kotlin -class User(name: String) { - val displayName = name.uppercase() - // fun show() = name // ошибка: name недоступен в методах -} - -fun main() { println(User("иван").displayName) } // ИВАН -``` - -## Параметры-свойства -Добавьте `val` или `var` - параметр станет свойством: -```kotlin -class User(val name: String, var age: Int) { - fun show() = "$name, $age" // теперь доступны везде -} - -fun main() { - val u = User("Иван", 20) - u.age = 21 - println(u.show()) // Иван, 21 -} -``` -Это самая частая форма: одна строка вместо поля, конструктора, геттера и сеттера. - -Можно смешивать: часть параметров сделать свойствами, часть оставить аргументами: -```kotlin -class User(val name: String, password: String) { - private val hash = password.hashCode() // пароль не хранится как есть - - fun check(input: String) = input.hashCode() == hash -} - -fun main() { - val u = User("Иван", "secret") - println(u.check("secret")) // true - println(u.check("wrong")) // false -} -``` - -## Значения по умолчанию -```kotlin -class Server( - val host: String = "localhost", - val port: Int = 8080, - val secure: Boolean = false, -) - -fun main() { - println(Server().port) // 8080 - println(Server("example.com", 443, true).host) // example.com - println(Server(port = 9090).port) // 9090 — именованный аргумент -} -``` -Это заменяет телескопические конструкторы и во многих случаях - паттерн Builder. Подробнее: [Функции](../Синтаксис%20и%20типы/Работа%20с%20данными/Функции.md) - -Висячая запятая после последнего параметра разрешена - упрощает правки и diff. - -## Модификаторы и аннотации -Если у конструктора есть модификатор видимости или аннотация, слово `constructor` обязательно: -```kotlin -class Service private constructor(val id: Int) { - companion object { - fun create(id: Int) = if (id > 0) Service(id) else null - } -} - -fun main() { - println(Service.create(1)?.id) // 1 - println(Service.create(-1)) // null - // Service(1) // ошибка: конструктор приватный -} -``` -Приватный конструктор + фабрика в компаньоне - способ контролировать создание: валидация, кеширование, возврат `null` вместо исключения. - -## Вторичные конструкторы -Объявляются в теле класса словом `constructor`: -```kotlin -class Point(val x: Int, val y: Int) { - constructor(value: Int) : this(value, value) // точка на диагонали - constructor() : this(0, 0) - - override fun toString() = "($x, $y)" -} - -fun main() { - println(Point(1, 2)) // (1, 2) - println(Point(5)) // (5, 5) - println(Point()) // (0, 0) -} -``` -Вторичный конструктор **обязан** делегировать основному через `this(...)` - напрямую инициализировать свойства он не может. - -Класс без основного конструктора может иметь только вторичные: -```kotlin -class Config4 { - val source: String - - constructor(file: java.io.File) { - source = "файл ${file.name}" - } - - constructor(url: String, timeout: Int) { - source = "$url (таймаут $timeout)" - } -} - -fun main() { - println(Config4("http://x.ru", 30).source) // http://x.ru (таймаут 30) -} -``` -Здесь делегировать некому, поэтому свойство инициализируется в каждом конструкторе. Форма редкая - обычно хватает основного конструктора со значениями по умолчанию. - -### Когда нужны вторичные -Почти никогда. Значения по умолчанию покрывают большинство случаев: -```kotlin -// вместо трёх конструкторов -class Point(val x: Int = 0, val y: Int = x) - -fun main() { - println("${Point().x} ${Point(5).y} ${Point(1, 2).y}") // 0 5 2 -} -``` -Вторичный конструктор оправдан, когда нужно **преобразовать** аргументы другого типа или выполнить работу перед делегированием: -```kotlin -class Duration2(val millis: Long) { - constructor(seconds: Int) : this(seconds * 1000L) - constructor(text: String) : this(text.removeSuffix("s").toLong() * 1000) -} - -fun main() { - println(Duration2(2).millis) // 2000 - println(Duration2("3s").millis) // 3000 -} -``` -Но и здесь фабричные функции в компаньоне обычно читаются лучше - у них есть имена: -```kotlin -class Duration3 private constructor(val millis: Long) { - companion object { - fun ofSeconds(s: Int) = Duration3(s * 1000L) - fun parse(text: String) = Duration3(text.removeSuffix("s").toLong() * 1000) - } -} - -fun main() { - println(Duration3.ofSeconds(2).millis) // 2000 - println(Duration3.parse("3s").millis) // 3000 -} -``` -Два конструктора с одинаковыми типами параметров объявить нельзя, а фабрик с разными именами - сколько угодно. - -## Валидация в конструкторе -```kotlin -class Person(val name: String, val age: Int) { - init { - require(name.isNotBlank()) { "имя не может быть пустым" } - require(age in 0..150) { "недопустимый возраст: $age" } - } -} - -fun main() { - println(Person("Иван", 20).name) // Иван - try { - Person("", 20) - } catch (e: IllegalArgumentException) { - println(e.message) // имя не может быть пустым - } -} -``` -Блок `init` выполняется при создании объекта. Подробнее о порядке: [Инициализация](Инициализация.md) - -## Создание объекта -```kotlin -class User(val name: String) - -fun main() { - val u = User("Иван") // просто вызов - val list = List(3) { User("u$it") } // в лямбде - val byRef = ::User // ссылка на конструктор - println("${u.name} ${list.size} ${byRef("Пётр").name}") // Иван 3 Пётр -} -``` -`::User` - ссылка на конструктор, её можно передать как функцию. Подробнее: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `new User("Иван")` | `User("Иван")` | -> | конструктор = метод с телом | основной конструктор в заголовке | -> | поле + присваивание в конструкторе | `val` в параметрах конструктора | -> | телескопические конструкторы | значения по умолчанию | -> | Builder для многих параметров | именованные аргументы | -> | `this(...)` для делегирования | то же | -> | `private` конструктор + `static` фабрика | `private constructor` + `companion object` | -> -> **Главная экономия - параметры-свойства.** В Java класс из четырёх полей занимает 15+ строк: -> ```java -> public class User { -> private final String name; -> private final int age; -> public User(String name, int age) { -> this.name = name; -> this.age = age; -> } -> public String getName() { return name; } -> public int getAge() { return age; } -> } -> ``` -> В Kotlin это `class User(val name: String, val age: Int)`. -> -> **Builder обычно не нужен.** В Java паттерн Builder решает проблему конструктора с десятком опциональных параметров. Именованные аргументы и значения по умолчанию дают то же самое без дополнительного класса: -> ```kotlin -> class Request( -> val url: String, -> val method: String = "GET", -> val timeout: Int = 30, -> val retries: Int = 0, -> ) -> -> fun main() { -> println(Request("http://x.ru", timeout = 60).timeout) // 60 -> } -> ``` -> Builder остаётся полезен, когда объект собирается по частям в разных местах кода. -> -> Сравнить: [Конструкторы в Java](../../Программирование%20на%20Java/ООП/Конструкторы.md), [Builder](../../Программирование%20на%20Java/Паттерны%20проектирования/Builder.md) - -> [!warning] Подводные камни -> **Параметр без `val`/`var` недоступен в методах** - только в инициализаторах и `init`. Ошибка «unresolved reference» в методе обычно означает забытый `val`. -> -> **`val` в конструкторе делает свойство частью публичного API.** Если значение нужно только внутри, пишите `private val`: -> ```kotlin -> class Service4(private val apiKey: String) { -> fun call() = "запрос с ключом длины ${apiKey.length}" -> } -> -> fun main() { -> val s = Service4("secret") -> println(s.call()) // запрос с ключом длины 6 -> // println(s.apiKey) // ошибка: приватное свойство -> } -> ``` -> -> **Значения по умолчанию не видны из Java.** Без `@JvmOverloads` Java-код обязан передать все аргументы: -> ```kotlin -> class Server2 @JvmOverloads constructor( -> val host: String = "localhost", -> val port: Int = 8080, -> ) -> -> fun main() { println(Server2().port) } // 8080 -> ``` -> -> **Вторичный конструктор не может объявлять свойства:** -> ```kotlin -> class Point2(val x: Int) { -> // constructor(val y: Int) : this(y) // ошибка: val запрещён здесь -> constructor(y: Long) : this(y.toInt()) -> } -> -> fun main() { println(Point2(5L).x) } // 5 -> ``` -> -> **Порядок инициализации не совпадает с порядком конструкторов.** Блоки `init` и инициализаторы свойств выполняются **до** тела вторичного конструктора: -> ```kotlin -> class Order2 { -> val a = "свойство".also { println("1: $it") } -> init { println("2: init") } -> -> constructor() { println("3: тело вторичного") } -> } -> -> fun main() { Order2() } -> ``` -> Вывод: `1`, `2`, `3`. Подробнее: [Инициализация](Инициализация.md) -> -> **Два конструктора с одинаковой сигнатурой невозможны**, даже если смысл разный: -> ```kotlin -> class Range3 private constructor(val from: Int, val to: Int) { -> companion object { -> fun ofBounds(from: Int, to: Int) = Range3(from, to) -> fun ofStart(start: Int, length: Int) = Range3(start, start + length) -> } -> } -> -> fun main() { println(Range3.ofStart(1, 5).to) } // 6 -> ``` -> Фабричные функции решают это именами. -> -> **Исключение в конструкторе оставляет объект недосозданным.** Если в `init` что-то упало, ссылка на объект не вернётся - но уже выполненные побочные эффекты (запись в реестр, открытый файл) останутся. Поэтому в конструкторе делают только валидацию и присваивание. - -Дополнительно: [Свойства](Свойства.md) -Далее: [Свойства](Свойства.md) diff --git a/Kotlin/ООП/Модификаторы видимости.md b/Kotlin/ООП/Модификаторы видимости.md deleted file mode 100644 index 4937cd1..0000000 --- a/Kotlin/ООП/Модификаторы видимости.md +++ /dev/null @@ -1,290 +0,0 @@ ---- -title: Модификаторы видимости -description: public, internal, protected, private и чем internal отличается от package-private -tags: - - kotlin - - oop ---- - -Четыре модификатора. По умолчанию - `public`. - -| Модификатор | Где виден | -|---|---| -| `public` | везде (по умолчанию) | -| `internal` | в пределах модуля | -| `protected` | в классе и его подклассах | -| `private` | в классе (или в файле, если член верхнего уровня) | - -## На верхнем уровне -```kotlin -public val a = 1 // везде; слово public избыточно -internal val b = 2 // в своём модуле -private val c = 3 // только в этом файле - -private fun helper() = c * 2 - -fun main() { println("$a $b ${helper()}") } // 1 2 6 -``` -Для членов верхнего уровня `private` означает «виден в файле» - аналога «виден в пакете» у Kotlin нет. `protected` на верхнем уровне применить нельзя. - -## В классе -```kotlin -open class Service10 { - val publicField = 1 - internal val internalField = 2 - protected val protectedField = 3 - private val privateField = 4 - - fun show() = "$publicField $internalField $protectedField $privateField" -} - -class Child2 : Service10() { - fun access() = "$publicField $internalField $protectedField" - // privateField недоступен -} - -fun main() { - val s = Service10() - println(s.show()) // 1 2 3 4 - println(Child2().access()) // 1 2 3 - // println(s.protectedField) // ошибка: protected -} -``` - -`protected` в Kotlin **не** даёт видимости в пакете - только в классе и подклассах. Это строже, чем в Java. - -## private в классе и вложенные классы -```kotlin -class Outer2 { - private val secret = "скрыто" - - class Nested { - // fun bad() = secret // ошибка: нет доступа к членам Outer - fun ok() = "вложенный класс" - } - - inner class Inner2 { - fun read() = secret // inner видит внешний класс - } -} - -fun main() { - println(Outer2.Nested().ok()) // вложенный класс - println(Outer2().Inner2().read()) // скрыто -} -``` -Подробнее: [Вложенные и внутренние классы](Вложенные%20и%20внутренние%20классы.md) - -## internal - видимость в модуле -Модуль - это единица компиляции: модуль Gradle или Maven, набор файлов одной задачи `kotlinc`. -```kotlin -internal class Repository { - internal fun load() = "данные" -} - -internal fun helper2() = Repository().load() - -fun main() { println(helper2()) } // данные -``` -`internal` - основной инструмент для библиотек: публичный API помечается `public`, всё остальное - `internal`, и клиент библиотеки не видит внутренностей. - -Типичный приём - публичный интерфейс и внутренняя реализация: -```kotlin -interface UserRepository { - fun find(id: Int): String? -} - -internal class UserRepositoryImpl : UserRepository { - override fun find(id: Int) = if (id == 1) "Иван" else null -} - -fun createRepository(): UserRepository = UserRepositoryImpl() - -fun main() { - println(createRepository().find(1)) // Иван -} -``` -Снаружи модуля видны только `UserRepository` и `createRepository` - реализацию заменить можно без ломающих изменений. - -## Видимость конструктора -```kotlin -class Service11 private constructor(val id: Int) { - companion object { - fun create(id: Int) = if (id > 0) Service11(id) else null - } -} - -fun main() { - println(Service11.create(5)?.id) // 5 - println(Service11.create(0)) // null -} -``` -Слово `constructor` обязательно, когда у конструктора есть модификатор. Подробнее: [Классы и конструкторы](Классы%20и%20конструкторы.md) - -## Видимость сеттера -Геттер всегда имеет видимость самого свойства, а сеттер можно ограничить: -```kotlin -class Session2 { - var token: String = "" - private set - - var attempts: Int = 0 - internal set - - fun login(t: String) { token = t; attempts++ } -} - -fun main() { - val s = Session2() - s.login("abc") - println("${s.token} ${s.attempts}") // abc 1 - // s.token = "x" // ошибка -} -``` - -## Правила согласованности -Публичный член не может раскрывать тип с меньшей видимостью: -```kotlin -internal class Internal2 - -class Public2 { - // fun bad(): Internal2 = Internal2() // ошибка: public раскрывает internal - internal fun ok(): Internal2 = Internal2() - private fun alsoOk(): Internal2 = Internal2() -} - -fun main() { println(Public2().ok()::class.simpleName) } // Internal2 -``` -Компилятор проверяет это и для параметров, и для типов результата, и для супертипов. - -## Видимость и переопределение -Переопределённый член не может сужать видимость: -```kotlin -open class Base10 { - open internal fun f() = "base" - protected open fun g() = "g" -} - -class Derived10 : Base10() { - override fun f() = "derived" // internal сохранена - public override fun g() = "расширена" // protected → public можно -} - -fun main() { - println(Derived10().f()) // derived - println(Derived10().g()) // расширена — теперь доступен снаружи -} -``` -Расширять видимость разрешено, сужать - нет: это нарушило бы контракт базового типа. - -`protected`-член можно сделать `public` в подклассе - приём для постепенного раскрытия API. - -## Что видно из Java -Это важное практическое различие: - -| Kotlin | В байт-коде | Доступно из Java | -|---|---|---| -| `public` | `public` | да | -| `internal` | `public` + искажённое имя | **да** | -| `protected` | `protected` | да, в подклассе и в пакете | -| `private` (член класса) | `private` | нет | -| `private` (верхний уровень) | `private` в файловом классе | нет | - -```kotlin -class Service12 { - internal fun internalMethod() = "в байт-коде это public" - private fun privateMethod() = "действительно private" - - fun show() = "${internalMethod()} / ${privateMethod()}" -} - -fun main() { println(Service12().show()) } -``` -`internal` существует только на уровне компилятора Kotlin. В байт-коде член становится `public`, а к его имени добавляется суффикс с именем модуля - чтобы случайный вызов из Java выглядел странно, но он возможен. Полагаться на `internal` как на защиту от Java-кода нельзя. - -Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `public` | `public` | -> | нет модификатора = package-private | **нет аналога** | -> | `protected` = подклассы + пакет | `protected` = только подклассы | -> | `private` | `private` | -> | нет аналога | `internal` = модуль | -> | модули JPMS (Java 9+) | `internal` работает всегда | -> -> **По умолчанию `public`, а не package-private.** Это ловушка при переходе: Java-разработчик не пишет модификатор, ожидая видимость в пакете, и случайно публикует API. -> -> **Package-private в Kotlin нет совсем.** Замена - `internal` (шире: весь модуль) или `private` на верхнем уровне (уже: один файл). Пакет в Kotlin не является границей инкапсуляции, потому что пакеты легко расширить извне - любой может объявить класс в вашем пакете. -> -> **`protected` строже.** В Java `protected`-член виден всему пакету, что часто неожиданно. В Kotlin - только подклассам. -> -> **`internal` решает задачу, для которой в Java нужны JPMS.** Модульная система Java появилась в 9 и применяется редко; `internal` работает в любом проекте с модулями Gradle и не требует настройки. -> -> Сравнить: [Модификаторы в Java](../../Программирование%20на%20Java/ООП/Инкапсуляция/Модификаторы.md), [Инкапсуляция](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md) - -> [!warning] Подводные камни -> **Забыли модификатор - получили публичный API.** Всё, что не помечено, видно всем и становится обязательством на поддержку. -> -> **`internal` не защищает от Java-кода** - в байт-коде это `public`. Единственная настоящая защита - `private` или отдельный модуль. -> -> **`private` на верхнем уровне - это «виден в файле», а не «в пакете»:** -> ```kotlin -> private val fileLocal = "виден только здесь" -> private class Helper3 { fun work() = fileLocal } -> -> fun main() { println(Helper3().work()) } -> ``` -> Другой файл того же пакета доступа не получит. -> -> **`private` член класса виден во всём классе, включая вложенные объявления:** -> ```kotlin -> class Outer3 { -> private val secret = "скрыто" -> -> inner class Inner3 { -> fun read() = secret // доступ есть -> } -> -> companion object { -> fun describe() = "компаньон не видит secret экземпляра" -> } -> } -> -> fun main() { -> println(Outer3().Inner3().read()) // скрыто -> println(Outer3.describe()) -> } -> ``` -> -> **`protected` недоступен в расширениях** - даже внутри того же класса: -> ```kotlin -> open class Base11 { -> protected val value = 42 -> fun show() = value -> } -> -> // fun Base11.bad() = value // ошибка: protected недоступен в расширении -> -> fun main() { println(Base11().show()) } // 42 -> ``` -> -> **`private` конструктор не защищает `data class` от `copy`** в версиях до Kotlin 2.1: клиент мог обойти фабрику через `copy()`. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md) -> -> **Расширение видимости при переопределении легко сделать случайно.** `public override fun` у `protected`-метода публикует то, что автор базового класса прятал. -> -> **`internal`-классы в публичных сигнатурах - ошибка компиляции**, но `internal`-тип внутри дженерика может просочиться: -> ```kotlin -> internal class Impl2 -> -> class Api2 { -> internal val items = mutableListOf() -> val count: Int get() = items.size // публично только число -> } -> -> fun main() { println(Api2().count) } // 0 -> ``` - -Дополнительно: [ООП](ООП.md) -Далее: [Наследование](Наследование.md) diff --git a/Kotlin/ООП/Наследование.md b/Kotlin/ООП/Наследование.md deleted file mode 100644 index 85c980b..0000000 --- a/Kotlin/ООП/Наследование.md +++ /dev/null @@ -1,307 +0,0 @@ ---- -title: Наследование -description: open, вызов конструктора базового класса, наследование свойств и выбор в пользу композиции -tags: - - kotlin - - oop ---- - -Класс закрыт для наследования, пока не объявлен `open`. - -```kotlin -open class Animal(val name: String) { - open fun sound() = "..." - fun describe() = "$name говорит ${sound()}" // final: переопределить нельзя -} - -class Dog(name: String) : Animal(name) { - override fun sound() = "Гав" -} - -fun main() { - println(Dog("Рекс").describe()) // Рекс говорит Гав -} -``` -`describe()` не `open`, но вызывает `sound()`, который переопределён - это и есть полиморфизм: шаблон поведения в базовом классе, детали в подклассе. - -## Вызов конструктора базового класса -Указывается в заголовке: -```kotlin -open class Base12(val id: Int) - -class Derived12(id: Int, val name: String) : Base12(id) - -fun main() { - val d = Derived12(1, "Иван") - println("${d.id} ${d.name}") // 1 Иван -} -``` -Аргументы можно вычислять: -```kotlin -open class Shape2(val area: Double) - -class Circle2(val radius: Double) : Shape2(Math.PI * radius * radius) - -fun main() { - println("%.2f".format(java.util.Locale.US, Circle2(1.0).area)) // 3.14 -} -``` - -Если у подкласса нет основного конструктора, базовый вызывается из каждого вторичного через `super`: -```kotlin -open class Base13(val source: String) - -class Derived13 : Base13 { - constructor(file: java.io.File) : super("файл ${file.name}") - constructor(url: String, port: Int) : super("$url:$port") -} - -fun main() { - println(Derived13("http://x.ru", 80).source) // http://x.ru:80 -} -``` - -Скобки в заголовке обязательны, даже если конструктор без параметров: -```kotlin -open class Base14 -class Derived14 : Base14() // скобки = вызов конструктора - -interface Marker2 -class Impl3 : Marker2 // у интерфейса скобок нет - -fun main() { println(Derived14()::class.simpleName) } // Derived14 -``` -Наличие скобок различает класс и интерфейс в списке супертипов. - -## Что можно наследовать -```kotlin -open class Base15 { - open val version: Int = 1 - open var state: String = "новый" - open fun run2() = "база" - val fixed = "не переопределить" // не open -} - -class Derived15 : Base15() { - override val version = 2 - override var state: String = "изменённый" - override fun run2() = "подкласс" -} - -fun main() { - val d: Base15 = Derived15() - println("${d.version} ${d.state} ${d.run2()} ${d.fixed}") - // 2 изменённый подкласс не переопределить -} -``` -`open` нужен каждому члену отдельно: открытый класс не делает своих членов открытыми. - -### val можно переопределить как var -```kotlin -open class Base16 { - open val value: Int = 1 -} - -class Derived16 : Base16() { - override var value: Int = 2 // добавили сеттер -} - -fun main() { - val d = Derived16() - d.value = 5 - println(d.value) // 5 -} -``` -Обратное запрещено: `var` нельзя сузить до `val` - это сломало бы код, который присваивает значение. - -### Переопределение свойства геттером -```kotlin -open class Base17 { - open val size: Int = 0 -} - -class Derived17(private val items: List) : Base17() { - override val size: Int get() = items.size // вычисляемое вместо хранимого -} - -fun main() { - println(Derived17(listOf(1, 2, 3)).size) // 3 -} -``` - -## Свойство в конструкторе подкласса -```kotlin -open class Base18 { - open val name: String = "базовое" -} - -class Derived18(override val name: String) : Base18() - -fun main() { - println(Derived18("своё").name) // своё -} -``` -`override` прямо в параметре конструктора - короткая и частая форма. - -## Абстрактный член не требует open -```kotlin -abstract class Shape3 { - abstract val area: Double // open подразумевается - abstract fun describe(): String - open fun show() = "фигура: ${describe()}" -} - -class Square2(val side: Double) : Shape3() { - override val area get() = side * side - override fun describe() = "квадрат со стороной $side" -} - -fun main() { - println(Square2(2.0).show()) // фигура: квадрат со стороной 2.0 -} -``` -Подробнее: [Абстрактные классы](Абстрактные%20классы.md) - -## Одиночное наследование, множественные интерфейсы -```kotlin -open class Entity(val id: Int) -interface Serializable2 { fun serialize(): String } -interface Validatable { fun validate(): Boolean } - -class User4(id: Int, val name: String) : Entity(id), Serializable2, Validatable { - override fun serialize() = """{"id":$id,"name":"$name"}""" - override fun validate() = name.isNotBlank() -} - -fun main() { - val u = User4(1, "Иван") - println(u.serialize()) // {"id":1,"name":"Иван"} - println(u.validate()) // true -} -``` -Класс в списке супертипов идёт первым - это соглашение, а не требование компилятора. - -## Что наследовать нельзя -```kotlin -class Final2 // закрыт по умолчанию -data class Point3(val x: Int) // data class не может быть open -@JvmInline value class Id2(val value: Int) // value class тоже -object Singleton2 // object -enum class Status2 { OK } // enum -sealed class Limited // только в своём модуле и пакете - -fun main() { println("перечислены закрытые формы") } -``` -Для `enum` есть исключение: отдельная константа может переопределить метод, образуя анонимный подкласс. Подробнее: [Enum class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Enum%20class.md) - -## Композиция вместо наследования -Наследование связывает подкласс с реализацией базового класса. Изменение базового класса может сломать подкласс, даже если публичный контракт не менялся - это проблема хрупкого базового класса. - -Классический пример - подсчёт добавленных элементов: -```kotlin -// плохо: наследование от реализации -class CountingList : ArrayList() { - var added = 0 - override fun add(element: T): Boolean { - added++ - return super.add(element) - } - override fun addAll(elements: Collection): Boolean { - added += elements.size - return super.addAll(elements) // внутри может вызвать add()! - } -} - -fun main() { - val list = CountingList() - list.addAll(listOf(1, 2, 3)) - println("добавлено: ${list.added}, реально: ${list.size}") -} -``` -На текущем JDK выведется `добавлено: 3, реально: 3` - потому что `ArrayList.addAll` копирует массив напрямую и `add` не вызывает. Но это не гарантия: реализация, которая реализует `addAll` через цикл с `add`, удвоит счётчик. Корректность класса зависит от внутренностей базового, а они не входят в его контракт и могут измениться между версиями JDK. - -Композиция такой зависимости не создаёт: -```kotlin -class CountingList2 { - private val items = mutableListOf() - var added = 0 - private set - - fun add(element: T) { added++; items.add(element) } - fun addAll(elements: Collection) { elements.forEach { add(it) } } - val size: Int get() = items.size -} - -fun main() { - val list = CountingList2() - list.addAll(listOf(1, 2, 3)) - println("добавлено: ${list.added}, реально: ${list.size}") // 3, 3 -} -``` -Kotlin делает композицию такой же короткой, как наследование, - через делегирование `by`. Подробнее: [Делегирование](Делегирование.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `class A extends B` | `class A : B()` | -> | `implements I` | та же запись через запятую | -> | класс открыт, `final` закрывает | закрыт, `open` открывает | -> | метод виртуален по умолчанию | финален по умолчанию | -> | `super(args)` первой строкой тела | `: Base(args)` в заголовке | -> | `@Override` опциональна | `override` обязательно | -> | нельзя переопределить поле | свойство переопределяется | -> -> **Закрытость по умолчанию - осознанный выбор.** Проектировать класс под наследование дорого: нужно документировать, какие методы вызывают какие, и поддерживать это между версиями. Java по умолчанию делает всё наследуемым, из-за чего библиотечные классы либо получают `final`, либо навсегда остаются связанными своими внутренностями. -> -> **Переопределение свойств** в Java невозможно - поля скрываются, а не переопределяются: -> ```java -> class Base { int value = 1; } -> class Derived extends Base { int value = 2; } // сокрытие, не переопределение -> // ((Base) new Derived()).value == 1 -> ``` -> Обращение по ссылке базового типа даст поле базового класса. В Kotlin свойство - это методы доступа, поэтому `override` работает полиморфно и такой ловушки нет. -> -> Сравнить: [Наследование в Java](../../Программирование%20на%20Java/ООП/Наследование/Наследование.md), [Сокрытие](../../Программирование%20на%20Java/ООП/Наследование/Сокрытие.md) - -> [!warning] Подводные камни -> **`open` у класса не открывает его члены** - каждый метод и свойство нужно помечать отдельно. -> -> **Переопределённый метод остаётся `open`.** Чтобы закрыть ветку иерархии, нужен `final override`: -> ```kotlin -> open class A4 { open fun f() = "A" } -> open class B4 : A4() { final override fun f() = "B" } -> // class C4 : B4() { override fun f() = "C" } // ошибка: f закрыт -> -> fun main() { println(B4().f()) } // B -> ``` -> -> **Вызов `open`-метода из конструктора видит неинициализированный подкласс:** -> ```kotlin -> open class Base19 { -> init { println("из init: ${describe()}") } -> open fun describe() = "база" -> } -> -> class Derived19 : Base19() { -> private val name: String = "Иван" -> override fun describe() = "подкласс, name=$name" -> } -> -> fun main() { Derived19() } // из init: подкласс, name=null -> ``` -> Подробнее: [Инициализация](Инициализация.md) -> -> **Скобки обязательны при наследовании класса**: `: Base()`, а не `: Base`. Без скобок компилятор ищет интерфейс. -> -> **`protected` в Kotlin не даёт доступа из пакета** - только из подклассов. Код, перенесённый из Java, может перестать компилироваться. -> -> **`var` нельзя переопределить как `val`** - только наоборот. -> -> **Наследование от классов стандартной библиотеки хрупко.** `ArrayList`, `HashMap` и подобные не документируют, какие методы вызывают друг друга внутри. Используйте делегирование. -> -> **Глубокие иерархии трудно читать.** Чтобы понять поведение класса в иерархии из пяти уровней, нужно просмотреть все пять. Практическое правило: наследование - для «является» и когда базовый класс спроектирован под него; во всех остальных случаях - композиция. -> -> **Нельзя наследоваться от `data class`**, поэтому иерархии носителей данных строят на `sealed`-интерфейсах с `data class`-реализациями. - -Дополнительно: [Переопределение](Переопределение.md) -Далее: [Переопределение](Переопределение.md) diff --git a/Kotlin/ООП/ООП.md b/Kotlin/ООП/ООП.md deleted file mode 100644 index cec3c4a..0000000 --- a/Kotlin/ООП/ООП.md +++ /dev/null @@ -1,197 +0,0 @@ ---- -title: ООП -description: Обзор объектной модели Kotlin: что закрыто по умолчанию, чего нет совсем и почему -tags: - - kotlin - - oop ---- - -Объектная модель Kotlin устроена так же, как в Java - один суперкласс, множество интерфейсов, виртуальные вызовы. Отличия в значениях по умолчанию и в том, что из Java убрано. - -## Главное отличие: всё закрыто по умолчанию -```kotlin -class User(val name: String) { - fun greet() = "Привет, $name" -} - -// class Admin : User("админ") // ошибка: User закрыт для наследования -``` -Класс нельзя унаследовать, пока он не объявлен `open`. Метод нельзя переопределить, пока он не `open`. Это противоположность Java, где `final` нужно писать явно. - -```kotlin -open class User(val name: String) { - open fun greet() = "Привет, $name" - fun id() = name.hashCode() // переопределить нельзя -} - -class Admin(name: String) : User(name) { - override fun greet() = "Админ $name" - // override fun id() = 0 // ошибка: id не open -} - -fun main() { - val u: User = Admin("Иван") - println(u.greet()) // Админ Иван — виртуальный вызов -} -``` - -Причина - проблема хрупкого базового класса. Автор класса, который не проектировал его под наследование, не может гарантировать, что подкласс не нарушит инварианты. `open` - это явное обещание поддерживать наследование как часть контракта. - -## Карта раздела -```mermaid -graph TD - A["Классы и конструкторы"] --> B["Свойства"] - B --> C["Инициализация"] - C --> D["Модификаторы видимости"] - D --> E["Наследование"] - E --> F["Переопределение"] - F --> G["Абстрактные классы"] - G --> H["Интерфейсы"] - H --> I["Делегирование"] - I --> J["Вложенные классы"] - J --> K["Полиморфизм"] -``` - -| Заметка | О чём | -|---|---| -| [Классы и конструкторы](Классы%20и%20конструкторы.md) | основной и вторичные конструкторы, параметры-свойства | -| [Свойства](Свойства.md) | геттеры, сеттеры, backing field, `lateinit`, делегаты | -| [Инициализация](Инициализация.md) | порядок выполнения `init`, ловушка вызова open-метода | -| [Модификаторы видимости](Модификаторы%20видимости.md) | `public`, `internal`, `protected`, `private` | -| [Наследование](Наследование.md) | `open`, вызов конструктора базового класса | -| [Переопределение](Переопределение.md) | `override`, `final`, `super`, ковариантность результата | -| [Абстрактные классы](Абстрактные%20классы.md) | `abstract` против `open`, выбор с интерфейсом | -| [Интерфейсы](Интерфейсы.md) | реализация по умолчанию, свойства, ромбовидное наследование | -| [Делегирование](Делегирование.md) | `by` для интерфейсов и свойств | -| [Вложенные и внутренние классы](Вложенные%20и%20внутренние%20классы.md) | `nested` по умолчанию, `inner` для ссылки на внешний | -| [Полиморфизм](Полиморфизм.md) | подтипы, перегрузка, дженерики | - -## Чего в Kotlin нет -| Из Java | Что вместо | -|---|---| -| `static` | [companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md), функции верхнего уровня | -| поля (`field`) в публичном API | [свойства](Свойства.md) с геттером и сеттером | -| checked exceptions | все исключения непроверяемые, см. [Система исключений](../Исключения/Система%20исключений.md) | -| `package-private` (без модификатора) | `internal` - видимость в пределах модуля | -| анонимные классы | [объект-выражение](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md) | -| примитивы и обёртки как разные типы | один `Int`, упаковка по необходимости | -| `wait`/`notify` в `Object` | [корутины](../Корутины/Корутины.md) и примитивы JDK | - -## Классы на любой вкус -Kotlin предлагает специализированные формы класса вместо одного универсального: - -```kotlin -class Service // обычный -data class Point(val x: Int, val y: Int) // носитель данных -enum class Status { OK, FAIL } // фиксированный набор значений -sealed interface Result3 // фиксированный набор типов -object Registry // синглтон -@JvmInline value class UserId(val value: Int) // обёртка без аллокации -abstract class Shape // частичная реализация -interface Named // контракт -annotation class Marker // аннотация -fun interface Handler { fun handle(x: Int) } // SAM-интерфейс -``` -Правильный выбор формы снимает большую часть ручной работы: `data class` избавляет от написания `equals`, `sealed` - от `else` в `when`, `object` - от реализации синглтона. - -Подробнее о первых пяти: [Базовые типы ООП](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md) - -## Свойства вместо полей -В Kotlin нет публичных полей - только свойства. Снаружи обращение выглядит как к полю, но всегда идёт через методы доступа: -```kotlin -class Rect(val width: Int, val height: Int) { - val area: Int get() = width * height // вычисляется при каждом чтении - - var scale: Double = 1.0 - set(value) { - require(value > 0) { "масштаб должен быть положительным" } - field = value - } -} - -fun main() { - val r = Rect(2, 3) - println(r.area) // 6 — похоже на поле, но это вызов геттера - r.scale = 2.0 - println(r.scale) // 2.0 -} -``` -Практическое следствие: заменить хранимое свойство на вычисляемое можно без изменения кода клиентов. В Java для этого пришлось бы менять `obj.field` на `obj.getField()` везде. - -Подробнее: [Свойства](Свойства.md) - -## Композиция удобнее наследования -Kotlin делает композицию такой же короткой, как наследование, - через делегирование: -```kotlin -interface Logger2 { fun log(msg: String) } - -class ConsoleLogger : Logger2 { - override fun log(msg: String) = println("[LOG] $msg") -} - -class Service2(logger: Logger2) : Logger2 by logger // делегирует всё - -fun main() { - Service2(ConsoleLogger()).log("работает") // [LOG] работает -} -``` -`Service2` реализует `Logger2`, не наследуя реализацию: методы просто перенаправляются. Подробнее: [Делегирование](Делегирование.md) - -## Что осталось от Java без изменений -- одиночное наследование классов, множественная реализация интерфейсов -- динамическая диспетчеризация: вызов идёт по фактическому типу объекта -- `Any` в корне иерархии (аналог `Object`) -- стирание типов в дженериках -- модель памяти и сборка мусора JVM - -То есть байт-код, который порождает Kotlin, - обычные JVM-классы. Любая Java-библиотека работает с Kotlin-классами, и наоборот. Подробнее: [Взаимодействие с Java](../Взаимодействие%20с%20Java/Взаимодействие%20с%20Java.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | класс открыт, `final` закрывает | класс закрыт, `open` открывает | -> | метод виртуален по умолчанию | метод финален по умолчанию | -> | `@Override` опциональна | `override` обязательно | -> | поля + геттеры/сеттеры | свойства | -> | `static` | `companion object` | -> | нет модификатора = package-private | нет модификатора = `public` | -> | нет аналога | `internal` (видимость в модуле) | -> | checked exceptions | их нет | -> | делегирование вручную | `by` | -> -> **Обязательный `override`** закрывает два класса ошибок Java: опечатка в имени метода создаёт новый метод вместо переопределения, а добавление метода в базовый класс может случайно «переопределиться» существующим методом подкласса. В Kotlin и то и другое - ошибка компиляции. -> -> **Отсутствие модификатора означает `public`**, а не package-private. Это ловушка при переходе: Java-разработчик по привычке не пишет модификатор, рассчитывая на видимость в пакете, и получает публичный API. -> -> Сравнить: [ООП в Java](../../Программирование%20на%20Java/ООП/ООП.md), [Инкапсуляция](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md) - -> [!warning] Подводные камни -> **`open` не наследуется транзитивно для метода.** Переопределённый метод остаётся открытым, пока его явно не закроют: -> ```kotlin -> open class A3 { open fun f() = "A" } -> open class B3 : A3() { override fun f() = "B" } // всё ещё open! -> class C3 : B3() { override fun f() = "C" } // можно -> -> open class D3 : A3() { final override fun f() = "D" } // так закрывают -> -> fun main() { -> println(listOf(A3(), B3(), C3(), D3()).joinToString { it.f() }) // A, B, C, D -> } -> ``` -> -> **Публичный `val` - часть API навсегда.** Превратить его в `var` можно, а убрать или переименовать - ломающее изменение, потому что клиенты обращаются к нему как к полю. -> -> **`internal` не защищает от Java.** В байт-коде такой член становится `public` с искажённым именем, поэтому Java-код из другого модуля может его вызвать: -> ```kotlin -> class Service3 { -> internal fun secret() = "виден из Java" -> } -> -> fun main() { println(Service3().secret()) } -> ``` -> -> **Отсутствие checked exceptions означает, что сигнатура не говорит об ошибках.** Функция может бросить что угодно, и компилятор об этом не предупредит. Способы это компенсировать - в [Result и runCatching](../Исключения/Result%20и%20runCatching.md). -> -> **Наследование от `data class` невозможно**, потому что он не может быть `open`. Иерархии строят на `sealed`-типах или на интерфейсах. - -Далее: [Классы и конструкторы](Классы%20и%20конструкторы.md) diff --git a/Kotlin/ООП/Переопределение.md b/Kotlin/ООП/Переопределение.md deleted file mode 100644 index eef1068..0000000 --- a/Kotlin/ООП/Переопределение.md +++ /dev/null @@ -1,341 +0,0 @@ ---- -title: Переопределение -description: override и final, вызов super, конфликт реализаций и ковариантность результата -tags: - - kotlin - - oop ---- - -`override` обязательно. Без него член не переопределяет, а конфликтует с родительским. - -```kotlin -open class Animal2 { - open fun sound() = "..." -} - -class Cat : Animal2() { - override fun sound() = "Мяу" - // fun sound() = "Мяу" // ошибка: 'sound' hides member and needs 'override' -} - -fun main() { - val a: Animal2 = Cat() - println(a.sound()) // Мяу — вызов по фактическому типу -} -``` - -Это закрывает два класса ошибок: опечатка в имени создала бы новый метод, а добавление метода в базовый класс случайно «переопределилось» бы существующим методом подкласса. В Kotlin и то и другое - ошибка компиляции. - -## final override -Переопределённый член остаётся открытым для дальнейшего переопределения. Закрыть ветку - `final`: -```kotlin -open class A5 { open fun f() = "A" } -open class B5 : A5() { final override fun f() = "B" } -// class C5 : B5() { override fun f() = "C" } // ошибка: f закрыт - -fun main() { println(B5().f()) } // B -``` - -## super -```kotlin -open class Logger3 { - open fun log(msg: String) = "[базовый] $msg" -} - -class TimestampLogger : Logger3() { - override fun log(msg: String) = super.log("[время] $msg") -} - -fun main() { - println(TimestampLogger().log("привет")) // [базовый] [время] привет -} -``` -`super` работает и для свойств - через их геттеры: -```kotlin -open class Base20 { - open val items: List get() = listOf(1, 2) -} - -class Derived20 : Base20() { - override val items: List get() = super.items + 3 -} - -fun main() { println(Derived20().items) } // [1, 2, 3] -``` - -Из вложенного класса к `super` внешнего обращаются через `super@Outer`: -```kotlin -open class Base21 { - open fun name() = "база" -} - -class Outer4 : Base21() { - override fun name() = "внешний" - - inner class Inner4 { - fun bothNames() = "${name()} / ${super@Outer4.name()}" - } -} - -fun main() { println(Outer4().Inner4().bothNames()) } // внешний / база -``` - -## Конфликт реализаций -Если два супертипа дают реализацию одного метода, компилятор требует разрешить конфликт явно: -```kotlin -interface A6 { fun f() = "A" } -interface B6 { fun f() = "B" } - -class C6 : A6, B6 { - override fun f() = super.f() + super.f() // указываем источник -} - -fun main() { println(C6().f()) } // AB -``` -Без `override` такой класс не скомпилируется - Kotlin не выбирает реализацию за вас. - -То же с классом и интерфейсом: -```kotlin -open class Base22 { open fun f() = "класс" } -interface Mixin { fun f() = "интерфейс" } - -class Impl4 : Base22(), Mixin { - override fun f() = "${super.f()} + ${super.f()}" -} - -fun main() { println(Impl4().f()) } // класс + интерфейс -``` -Подробнее: [Интерфейсы](Интерфейсы.md) - -## Что можно переопределять -| Член | Условие | -|---|---| -| метод | `open` или `abstract` | -| `val` | `open` или `abstract`; можно расширить до `var` | -| `var` | `open` или `abstract`; сузить до `val` нельзя | -| свойство в конструкторе | `override val` в параметре | - -```kotlin -abstract class Base23 { - abstract val size: Int - abstract var label: String - abstract fun compute(): Int -} - -class Impl5(override var label: String, private val items: List) : Base23() { - override val size get() = items.size - override fun compute() = items.sum() -} - -fun main() { - val i = Impl5("данные", listOf(1, 2, 3)) - println("${i.label} ${i.size} ${i.compute()}") // данные 3 6 -} -``` - -## Ковариантность результата -Переопределение может сузить тип результата: -```kotlin -open class Animal3 -class Dog2 : Animal3() - -open class Shelter { - open fun adopt(): Animal3 = Animal3() -} - -class DogShelter : Shelter() { - override fun adopt(): Dog2 = Dog2() // Dog2 — подтип Animal3 -} - -fun main() { - val s: Shelter = DogShelter() - println(s.adopt()::class.simpleName) // Dog2 -} -``` -Это безопасно: клиент ожидает `Animal3`, а получает `Dog2`, который им является. - -Параметры сузить нельзя - это сломало бы контракт: -```kotlin -open class Base24 { - open fun process(x: Any) = "any: $x" -} - -class Derived24 : Base24() { - override fun process(x: Any) = "переопределено: $x" - fun process(x: String) = "перегрузка: $x" // это перегрузка, не override -} - -fun main() { - val d = Derived24() - println(d.process("текст")) // перегрузка: текст - println((d as Base24).process("текст")) // переопределено: текст -} -``` -Вызов зависит от статического типа ссылки - классическая ловушка смешения перегрузки и переопределения. - -## Nullability при переопределении -```kotlin -open class Base25 { - open fun find(id: Int): String? = null -} - -class Derived25 : Base25() { - override fun find(id: Int): String = "найдено $id" // сузили String? до String -} - -fun main() { - val b: Base25 = Derived25() - println(b.find(1)) // найдено 1 -} -``` -Результат можно сделать non-null - это сужение. А параметр, наоборот, можно расширить до nullable: -```kotlin -open class Base26 { - open fun handle(x: String) = "строка" -} - -class Derived26 : Base26() { - override fun handle(x: String) = "подкласс: $x" -} - -fun main() { println(Derived26().handle("a")) } // подкласс: a -``` - -## Переопределение equals, hashCode, toString -Члены `Any` открыты всегда: -```kotlin -class Point4(val x: Int, val y: Int) { - override fun equals(other: Any?) = other is Point4 && other.x == x && other.y == y - override fun hashCode() = 31 * x + y - override fun toString() = "($x, $y)" -} - -fun main() { - println(Point4(1, 2)) // (1, 2) - println(Point4(1, 2) == Point4(1, 2)) // true -} -``` -Подробнее: [equals, hashCode и toString](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md) - -## Переопределение и расширения -Функция-расширение **не** переопределяется - она разрешается статически, по типу ссылки: -```kotlin -open class Base27 -class Derived27 : Base27() - -fun Base27.describe() = "база" -fun Derived27.describe() = "подкласс" - -fun main() { - val d: Base27 = Derived27() - println(d.describe()) // база — по типу переменной! - println(Derived27().describe()) // подкласс -} -``` -Это принципиальное отличие от методов. Если нужен полиморфизм - нужен метод класса. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md) - -Расширение не может скрыть метод с той же сигнатурой: метод всегда побеждает. -```kotlin -class Service13 { - fun run2() = "метод" -} - -fun Service13.run2() = "расширение" // никогда не вызовется - -fun main() { println(Service13().run2()) } // метод -``` - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `@Override` опциональна | `override` обязательно | -> | метод виртуален, `final` закрывает | нужен `open`, иначе финален | -> | переопределённый метод остаётся виртуальным | то же, закрывается `final override` | -> | `super.method()` | то же | -> | `A.super.method()` при конфликте | `super.method()` | -> | поля скрываются | свойства переопределяются | -> | ковариантный результат (Java 5+) | то же | -> -> **Обязательный `override`** - главное практическое улучшение. В Java `@Override` необязательна, поэтому опечатка в имени тихо создаёт новый метод, который никогда не вызовется. -> -> **Конфликт реализаций интерфейсов** решается похоже, но синтаксис другой: `A.super.f()` в Java против `super.f()` в Kotlin. Требование явно разрешать конфликт одинаково строгое в обоих языках. -> -> **Сокрытие полей** - проблема Java, которой в Kotlin нет. Поле подкласса с тем же именем скрывает поле базового класса, и какое из них увидит код, зависит от типа ссылки: -> ```java -> class Base { int value = 1; } -> class Derived extends Base { int value = 2; } -> Base b = new Derived(); -> // b.value == 1, ((Derived) b).value == 2 -> ``` -> В Kotlin свойства - это методы доступа, поэтому `override` работает полиморфно. -> -> **Статические методы в Java не переопределяются, а скрываются.** В Kotlin статики нет; члены `companion object` тоже не переопределяются, но путаницы меньше, потому что компаньон не наследуется. -> -> Сравнить: [Переопределение в Java](../../Программирование%20на%20Java/ООП/Наследование/Переопределение.md), [Сокрытие](../../Программирование%20на%20Java/ООП/Наследование/Сокрытие.md), [Ad-hoc полиморфизм](../../Программирование%20на%20Java/ООП/Полиморфизм/Ad-hoc%20(перегрузка).md) - -> [!warning] Подводные камни -> **Расширение не переопределяется** - выбор идёт по статическому типу. Это самая частая ошибка ожиданий: -> ```kotlin -> open class Base28 -> class Derived28 : Base28() -> -> fun Base28.name() = "база" -> fun Derived28.name() = "подкласс" -> -> fun main() { -> val list: List = listOf(Derived28()) -> println(list[0].name()) // база — не "подкласс" -> } -> ``` -> -> **Перегрузка вместо переопределения компилируется молча:** -> ```kotlin -> open class Base29 { open fun f(x: Any) = "any" } -> class Derived29 : Base29() { -> override fun f(x: Any) = "override" -> fun f(x: Int) = "overload" -> } -> -> fun main() { -> val d = Derived29() -> println(d.f(1)) // overload — точнее по типу -> println((d as Base29).f(1)) // override — по типу ссылки -> } -> ``` -> -> **Вызов `open`-метода из конструктора попадает в подкласс до его инициализации.** Подробнее: [Инициализация](Инициализация.md) -> -> **`super` в лямбде внутри метода работает, а в объекте-выражении - относится к нему:** -> ```kotlin -> open class Base30 { open fun f() = "база" } -> -> class Derived30 : Base30() { -> override fun f() = "подкласс" -> fun test(): String { -> val fromLambda = listOf(1).map { super.f() }.first() // база -> return fromLambda -> } -> } -> -> fun main() { println(Derived30().test()) } // база -> ``` -> -> **Переопределение `equals` без `hashCode` ломает хеш-коллекции.** Компилятор не предупреждает. -> -> **Сужение видимости при переопределении запрещено**, а расширение - разрешено и легко делается случайно: `public override` у `protected`-метода публикует внутреннюю деталь. -> -> **Делегат свойства не наследуется.** Переопределяя `val x by lazy { }`, подкласс должен задать своё хранилище - делегат базового класса не переиспользуется: -> ```kotlin -> open class Base31 { -> open val value: String by lazy { "база" } -> } -> -> class Derived31 : Base31() { -> override val value: String by lazy { "подкласс" } // свой делегат -> } -> -> fun main() { println(Derived31().value) } // подкласс -> ``` - -Дополнительно: [Полиморфизм](Полиморфизм.md) -Далее: [Абстрактные классы](Абстрактные%20классы.md) diff --git a/Kotlin/ООП/Полиморфизм.md b/Kotlin/ООП/Полиморфизм.md deleted file mode 100644 index 0e31745..0000000 --- a/Kotlin/ООП/Полиморфизм.md +++ /dev/null @@ -1,325 +0,0 @@ ---- -title: Полиморфизм -description: Полиморфизм подтипов, перегрузка, параметрический полиморфизм и что разрешается статически -tags: - - kotlin - - oop ---- - -Полиморфизм - возможность работать с разными типами через один интерфейс. В Kotlin четыре формы: - -| Форма | Механизм | Когда выбирается реализация | -|---|---|---| -| подтипов | `override` | в рантайме, по фактическому типу | -| ad-hoc (перегрузка) | одно имя, разные параметры | при компиляции | -| параметрический | дженерики | при компиляции (типы стираются) | -| через `when` по sealed | сопоставление типов | в рантайме, но проверяется компилятором | - -## Полиморфизм подтипов -```kotlin -interface Shape5 { - fun area(): Double - fun name(): String -} - -class Circle5(val r: Double) : Shape5 { - override fun area() = Math.PI * r * r - override fun name() = "круг" -} - -class Square4(val side: Double) : Shape5 { - override fun area() = side * side - override fun name() = "квадрат" -} - -fun describe(s: Shape5) = "${s.name()}: %.2f".format(java.util.Locale.US, s.area()) - -fun main() { - val shapes: List = listOf(Circle5(1.0), Square4(2.0)) - shapes.forEach { println(describe(it)) } - // круг: 3.14 - // квадрат: 4.00 - println(shapes.sumOf { it.area() } > 7) // true -} -``` -Функция `describe` написана один раз и работает с любым `Shape5`, включая те, которые появятся позже. Выбор реализации происходит в рантайме по фактическому типу объекта - это динамическая диспетчеризация. - -## Перегрузка -Одно имя, разные параметры. Выбор - при компиляции, по статическим типам аргументов: -```kotlin -fun format2(value: Int) = "целое: $value" -fun format2(value: Double) = "дробное: $value" -fun format2(value: String) = "строка: '$value'" -fun format2(value: Any?) = "что-то: $value" - -fun main() { - println(format2(1)) // целое: 1 - println(format2(1.0)) // дробное: 1.0 - println(format2("текст")) // строка: 'текст' - println(format2(listOf(1))) // что-то: [1] - - val x: Any = 1 // статический тип Any! - println(format2(x)) // что-то: 1 — не "целое: 1" -} -``` -Последняя строка показывает суть: компилятор видит тип переменной, а не объекта. Это основное отличие от полиморфизма подтипов. - -В Kotlin перегрузка нужна реже - её заменяют значения по умолчанию: -```kotlin -fun connect(host: String, port: Int = 8080, secure: Boolean = false) = - "$host:$port secure=$secure" - -fun main() { - println(connect("localhost")) // localhost:8080 secure=false - println(connect("localhost", secure = true)) // localhost:8080 secure=true -} -``` -Подробнее: [Функции](../Синтаксис%20и%20типы/Работа%20с%20данными/Функции.md) - -## Параметрический полиморфизм -Один код для любого типа: -```kotlin -fun firstOrDefault(items: List, default: T): T = - if (items.isEmpty()) default else items[0] - -class Box4(private val value: T) { - fun get(): T = value - fun map(f: (T) -> R): Box4 = Box4(f(value)) -} - -fun main() { - println(firstOrDefault(listOf(1, 2), 0)) // 1 - println(firstOrDefault(emptyList(), "нет")) // нет - println(Box4(5).map { it * 2 }.get()) // 10 -} -``` -Ограничение параметра типа позволяет вызывать методы: -```kotlin -fun > maxOf2(a: T, b: T): T = if (a > b) a else b - -fun main() { - println(maxOf2(3, 7)) // 7 - println(maxOf2("abc", "abd")) // abd -} -``` -Подробнее: [Параметрический полиморфизм](../Дженерики/Параметрический%20полиморфизм.md) - -## Полиморфизм через sealed и when -```kotlin -sealed interface Payment { - data class Card(val number: String) : Payment - data class Cash(val amount: Long) : Payment - data object Invoice : Payment -} - -fun process2(p: Payment): String = when (p) { - is Payment.Card -> "карта ****${p.number.takeLast(4)}" - is Payment.Cash -> "наличные ${p.amount}" - Payment.Invoice -> "счёт" -} - -fun main() { - println(process2(Payment.Card("1234567890123456"))) // карта ****3456 - println(process2(Payment.Cash(500))) // наличные 500 - println(process2(Payment.Invoice)) // счёт -} -``` -Это альтернатива полиморфизму подтипов с обратным распределением ответственности: - -| | метод в классе | `when` по sealed | -|---|---|---| -| добавить новый тип | правок в одном месте | компилятор найдёт все `when` | -| добавить новую операцию | правка во всех классах | один новый `when` | -| логика операции | размазана по классам | собрана в одном месте | - -Выбор зависит от того, что меняется чаще. Набор типов стабилен, а операций много - `sealed` + `when`. Операции стабильны, а типы добавляются - методы в классах. - -Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md) - -## Что разрешается статически -Это главный источник ошибок ожиданий. Статически (по типу ссылки, а не объекта) выбираются: - -**Функции-расширения:** -```kotlin -open class Base37 -class Derived33 : Base37() - -fun Base37.describe() = "база" -fun Derived33.describe() = "подкласс" - -fun main() { - val d: Base37 = Derived33() - println(d.describe()) // база — по типу переменной! - println(Derived33().describe()) // подкласс -} -``` - -**Перегруженные функции:** -```kotlin -open class Animal4 -class Dog3 : Animal4() - -fun feed(a: Animal4) = "корм для животного" -fun feed(d: Dog3) = "корм для собаки" - -fun main() { - val a: Animal4 = Dog3() - println(feed(a)) // корм для животного - println(feed(Dog3())) // корм для собаки -} -``` - -**Члены `companion object`** - они не переопределяются: -```kotlin -open class Base38 { - companion object { fun create() = "из Base" } -} - -class Derived34 : Base38() { - companion object { fun create() = "из Derived" } -} - -fun main() { - println(Base38.create()) // из Base - println(Derived34.create()) // из Derived — выбор по имени класса -} -``` - -Динамически выбираются только **члены класса**, помеченные `open`/`abstract` и переопределённые через `override`. - -## Приведение и проверка типа -```kotlin -fun handle(x: Any): String = when (x) { - is String -> "строка длины ${x.length}" // умное приведение - is Int -> "число ${x + 1}" - is List<*> -> "список из ${x.size}" - else -> "неизвестно: ${x::class.simpleName}" -} - -fun main() { - println(handle("abc")) // строка длины 3 - println(handle(5)) // число 6 - println(handle(listOf(1, 2))) // список из 2 - println(handle(1.0)) // неизвестно: Double -} -``` -Длинная цепочка `is` часто означает, что иерархия просит `sealed`-типа или полиморфного метода. Подробнее: [Приведение типов](../Синтаксис%20и%20типы/Работа%20с%20данными/Приведение%20типов.md) - -## Вариантность -Полиморфизм распространяется на дженерики через `out` и `in`: -```kotlin -open class Animal5(val name: String) -class Dog4(name: String) : Animal5(name) - -fun printAll(animals: List) = animals.joinToString { it.name } - -fun main() { - val dogs: List = listOf(Dog4("Рекс"), Dog4("Бобик")) - println(printAll(dogs)) // Рекс, Бобик — List подходит как List -} -``` -Это работает, потому что `List` ковариантен. С `MutableList` так нельзя - он инвариантен. Подробнее: [Вариантность](../Дженерики/Вариантность.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | метод виртуален по умолчанию | нужен `open` | -> | `@Override` опциональна | `override` обязательно | -> | перегрузка для опциональных параметров | значения по умолчанию | -> | `instanceof` + каст | `is` + умное приведение | -> | `switch` с pattern matching (Java 21) | `when` с `is` | -> | Visitor для операций над иерархией | `sealed` + `when` | -> | статические методы скрываются | члены компаньона не наследуются | -> -> **Механика полиморфизма подтипов идентична** - это та же виртуальная диспетчеризация JVM. Отличается значение по умолчанию: в Kotlin метод финален, пока не помечен `open`. -> -> **Visitor почти не нужен.** В Java для добавления операций к закрытой иерархии без правки классов применяют паттерн Visitor: интерфейс с методом на каждый тип, метод `accept` в каждом классе. `sealed` + `when` даёт ту же проверку полноты компилятором, но без дополнительного кода: -> ```kotlin -> sealed interface Expr2 { -> data class Num2(val value: Int) : Expr2 -> data class Add2(val l: Expr2, val r: Expr2) : Expr2 -> } -> -> fun eval(e: Expr2): Int = when (e) { -> is Expr2.Num2 -> e.value -> is Expr2.Add2 -> eval(e.l) + eval(e.r) -> } -> -> fun show(e: Expr2): String = when (e) { // новая операция — один when -> is Expr2.Num2 -> "${e.value}" -> is Expr2.Add2 -> "(${show(e.l)} + ${show(e.r)})" -> } -> -> fun main() { -> val expr = Expr2.Add2(Expr2.Num2(1), Expr2.Num2(2)) -> println("${show(expr)} = ${eval(expr)}") // (1 + 2) = 3 -> } -> ``` -> -> **Умное приведение убирает двойную работу.** В Java после `instanceof` нужен явный каст (до версии 16, где появился pattern matching). -> -> Сравнить: [Полиморфизм в Java](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм.md), [Полиморфизм подтипов](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм%20подтипов.md), [Ad-hoc полиморфизм](../../Программирование%20на%20Java/ООП/Полиморфизм/Ad-hoc%20(перегрузка).md) - -> [!warning] Подводные камни -> **Расширения не полиморфны** - выбор по статическому типу. Это самая частая ошибка: -> ```kotlin -> open class Base39 -> class Derived35 : Base39() -> -> fun Base39.name() = "база" -> fun Derived35.name() = "подкласс" -> -> fun main() { -> println(listOf(Derived35()).first().name()) // база -> } -> ``` -> Нужен полиморфизм - нужен член класса. -> -> **Перегрузка выбирается по типу ссылки, а не объекта.** Код ведёт себя по-разному в зависимости от того, как объявлена переменная. -> -> **Смешение перегрузки и переопределения даёт неочевидный результат:** -> ```kotlin -> open class Base40 { open fun f(x: Any) = "any" } -> class Derived36 : Base40() { -> override fun f(x: Any) = "override" -> fun f(x: Int) = "overload" -> } -> -> fun main() { -> val d = Derived36() -> println(d.f(1)) // overload -> println((d as Base40).f(1)) // override -> } -> ``` -> -> **Вызов `open`-метода из конструктора попадает в подкласс до его инициализации.** Подробнее: [Инициализация](Инициализация.md) -> -> **Стирание типов ломает перегрузку по дженерикам:** -> ```kotlin -> // fun show(items: List) = "строки" -> // fun show(items: List) = "числа" // ошибка: та же JVM-сигнатура -> -> @JvmName("showStrings") -> fun show2(items: List) = "строки: ${items.size}" -> fun show2(items: List) = "числа: ${items.size}" -> -> fun main() { println(show2(listOf("a"))) } // строки: 1 -> ``` -> Обойти можно через `@JvmName`, но обычно это признак, что нужен один дженерик-метод. -> -> **Длинная цепочка `is` вместо полиморфизма.** Если `when (x) { is A -> ...; is B -> ... }` повторяется в нескольких местах, логику стоит перенести в методы типов или закрыть иерархию через `sealed`, чтобы компилятор контролировал полноту. -> -> **Члены `companion object` не переопределяются.** Попытка «переопределить» фабрику в подклассе создаёт независимый компаньон; вызов `Derived.create()` работает только потому, что имя разрешается статически. -> -> **Массивы инвариантны, а `List` ковариантен** - в отличие от Java, где массивы ковариантны и могут бросить `ArrayStoreException`: -> ```kotlin -> fun main() { -> val strings: List = listOf("a") -> val anys: List = strings // ok -> println(anys.size) // 1 -> // val arr: Array = arrayOf("a") // ошибка компиляции -> } -> ``` - -Дополнительно: [Переопределение](Переопределение.md) -Далее: [Система исключений](../Исключения/Система%20исключений.md) diff --git a/Kotlin/ООП/Свойства.md b/Kotlin/ООП/Свойства.md deleted file mode 100644 index 476c1e4..0000000 --- a/Kotlin/ООП/Свойства.md +++ /dev/null @@ -1,433 +0,0 @@ ---- -title: Свойства -description: Геттеры и сеттеры, backing field, вычисляемые свойства, lateinit, lazy и делегаты -tags: - - kotlin - - oop ---- - -Свойство - это поле плюс методы доступа. Снаружи выглядит как поле, но обращение всегда идёт через геттер и сеттер. - -```kotlin -class User { - val id: Int = 1 // только чтение: геттер - var name: String = "Иван" // чтение и запись: геттер + сеттер -} - -fun main() { - val u = User() - println(u.id) // вызов геттера - u.name = "Пётр" // вызов сеттера - println(u.name) // Пётр -} -``` - -Полная форма объявления: -```kotlin -class Rect { - var width: Int = 0 - get() = field - set(value) { field = value } -} - -fun main() { - val r = Rect() - r.width = 5 - println(r.width) // 5 -} -``` -Такой код писать не нужно - это то, что компилятор генерирует сам. Писать вручную стоит только нестандартное поведение. - -## field - backing field -Внутри геттера и сеттера доступен `field` - само хранилище. Прямое обращение к свойству по имени привело бы к бесконечной рекурсии. -```kotlin -class Counter { - var value: Int = 0 - set(newValue) { - println("было $field, стало $newValue") - field = newValue // так - // value = newValue // бесконечная рекурсия! - } -} - -fun main() { - val c = Counter() - c.value = 5 // было 0, стало 5 - c.value = 7 // было 5, стало 7 - println(c.value) // 7 -} -``` - -`field` появляется только если он реально нужен. У вычисляемого свойства хранилища нет: -```kotlin -class Rect2(val width: Int, val height: Int) { - val area: Int get() = width * height // поля нет, считается каждый раз -} - -fun main() { - println(Rect2(2, 3).area) // 6 -} -``` -Это важно для памяти: свойство-геттер не занимает места в объекте. - -## Валидация в сеттере -```kotlin -class Account { - var balance: Long = 0 - set(value) { - require(value >= 0) { "баланс не может быть отрицательным: $value" } - field = value - } -} - -fun main() { - val a = Account() - a.balance = 100 - println(a.balance) // 100 - try { - a.balance = -1 - } catch (e: IllegalArgumentException) { - println(e.message) // баланс не может быть отрицательным: -1 - } -} -``` - -## Видимость сеттера -Частый приём - читать можно всем, менять только внутри: -```kotlin -class Session { - var token: String = "" - private set // геттер public, сеттер private - - fun login(t: String) { token = t } -} - -fun main() { - val s = Session() - s.login("abc") - println(s.token) // abc - // s.token = "x" // ошибка: сеттер приватный -} -``` -Это аккуратнее, чем приватное поле плюс отдельный публичный геттер. - -## Вычисляемые свойства или функции -```kotlin -class Order3(val items: List) { - val total: Int get() = items.sum() // свойство - fun computeTotal() = items.sum() // функция -} - -fun main() { - val o = Order3(listOf(1, 2, 3)) - println("${o.total} ${o.computeTotal()}") // 6 6 -} -``` -Соглашение: свойство, если это **характеристика** объекта, вычисляется дешёво и не бросает исключений. Функция - если выполняется работа: запрос, вычисление сложнее O(1), обращение к сети. - -```kotlin -class Data2(val rows: List) { - val size: Int get() = rows.size // дешёво — свойство - fun parse(): List = rows.map { it.length } // работа — функция -} - -fun main() { - val d = Data2(listOf("ab", "cde")) - println("${d.size} ${d.parse()}") // 2 [2, 3] -} -``` - -## Инициализация и порядок -Свойство должно быть инициализировано при создании объекта: -```kotlin -class Service5 { - val a: Int = 1 // сразу - val b: Int // в init - init { b = a + 1 } - val c: Int get() = a + b // геттер, инициализации не требует -} - -fun main() { - val s = Service5() - println("${s.a} ${s.b} ${s.c}") // 1 2 3 -} -``` -Подробнее: [Инициализация](Инициализация.md) - -## lateinit -Когда значение появится позже, но хочется non-null тип: -```kotlin -class Service6 { - lateinit var client: String - - fun setup() { client = "готов" } - fun use() = if (::client.isInitialized) client else "не настроен" -} - -fun main() { - val s = Service6() - println(s.use()) // не настроен - s.setup() - println(s.use()) // готов - println(s.client) // готов -} -``` -Ограничения `lateinit`: -- только `var` -- только non-null **ссылочный** тип: для `Int`, `Double` и других примитивов нельзя -- нельзя задать свой геттер или сеттер -- проверка `::x.isInitialized` доступна только внутри класса - -Обращение до инициализации даёт понятную ошибку: -```kotlin -class Service7 { lateinit var name: String } - -fun main() { - try { - println(Service7().name) - } catch (e: UninitializedPropertyAccessException) { - println(e.message) // lateinit property name has not been initialized - } -} -``` -`lateinit` нужен для внедрения зависимостей и жизненных циклов фреймворков (Android, Spring). В остальных случаях лучше nullable-тип или `lazy`. - -## lazy -Вычисляется один раз при первом обращении: -```kotlin -class Config5 { - val heavy: String by lazy { - println("вычисляем...") - "результат" - } -} - -fun main() { - val c = Config5() - println("создан") // создан - println(c.heavy) // вычисляем... затем результат - println(c.heavy) // результат — повторно не считается -} -``` -`lazy` работает только с `val`. По умолчанию потокобезопасен (синхронизация при первом доступе); если объект гарантированно в одном потоке, можно ослабить: -```kotlin -val value: Int by lazy(LazyThreadSafetyMode.NONE) { 42 } - -fun main() { println(value) } // 42 -``` - -| | `lateinit var` | `by lazy` | -|---|---|---| -| изменяемость | `var` | `val` | -| кто задаёт значение | внешний код | сама лямбда | -| примитивы | нельзя | можно | -| потокобезопасность | нет | есть по умолчанию | -| доступ до инициализации | исключение | вычислит на месте | - -## observable и vetoable -Готовые делегаты для реакции на изменение: -```kotlin -import kotlin.properties.Delegates - -class Form { - var name: String by Delegates.observable("") { _, old, new -> - println("name: '$old' -> '$new'") - } - - var age: Int by Delegates.vetoable(0) { _, _, new -> - new in 0..150 // false отменяет присваивание - } -} - -fun main() { - val f = Form() - f.name = "Иван" // name: '' -> 'Иван' - f.age = 20 - f.age = 500 // отклонено - println(f.age) // 20 -} -``` - -`Delegates.notNull()` - альтернатива `lateinit` для примитивов: -```kotlin -import kotlin.properties.Delegates - -class Service8 { - var port: Int by Delegates.notNull() -} - -fun main() { - val s = Service8() - s.port = 8080 - println(s.port) // 8080 -} -``` - -## Свойства в Map -Делегирование в `Map` - удобно для разбора динамических данных: -```kotlin -class User2(map: Map) { - val name: String by map - val age: Int by map -} - -fun main() { - val u = User2(mapOf("name" to "Иван", "age" to 20)) - println("${u.name} ${u.age}") // Иван 20 -} -``` - -## Свой делегат -```kotlin -import kotlin.reflect.KProperty - -class Trimmed { - private var value = "" - operator fun getValue(thisRef: Any?, property: KProperty<*>) = value - operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) { - value = newValue.trim() - } -} - -class Form2 { - var login: String by Trimmed() -} - -fun main() { - val f = Form2() - f.login = " иван " - println("'${f.login}'") // 'иван' -} -``` -Подробнее: [Делегирование](Делегирование.md) - -## Свойства верхнего уровня и расширения -```kotlin -val appName = "MyApp" // свойство файла -const val VERSION = "1.0" // константа компиляции - -val String.firstWord: String // свойство-расширение - get() = substringBefore(" ") - -fun main() { - println("$appName $VERSION") // MyApp 1.0 - println("привет мир".firstWord) // привет -} -``` -У свойства-расширения не может быть `field` - только геттер. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | поле + `getX()` + `setX()` | `var x` | -> | `obj.getName()` | `obj.name` | -> | `obj.setName("x")` | `obj.name = "x"` | -> | `private final` поле | `val` | -> | геттер с вычислением | `val x get() = ...` | -> | Lombok `@Getter`/`@Setter` | не нужны | -> | `Supplier` + кеш для ленивости | `by lazy` | -> | `Optional` поле | `T?` | -> -> **Свойства - не синтаксический сахар над геттерами, а единица абстракции.** Переход от хранимого свойства к вычисляемому не меняет код клиентов: -> ```kotlin -> class Rect3(val w: Int, val h: Int) { -> val area: Int = w * h // было: хранится -> } -> -> class Rect4(val w: Int, val h: Int) { -> val area: Int get() = w * h // стало: считается. Клиенты не изменились -> } -> -> fun main() { println("${Rect3(2,3).area} ${Rect4(2,3).area}") } // 6 6 -> ``` -> В Java переход от `obj.field` к `obj.getField()` - ломающее изменение, поэтому там поля сразу прячут за геттерами «на всякий случай». В Kotlin эта защита встроена. -> -> **Java-геттеры видны как свойства.** Метод `getName()` в Java-классе доступен из Kotlin как `obj.name`: -> ```kotlin -> fun main() { -> val sb = StringBuilder("abc") -> println(sb.length) // вместо sb.length() — синтетическое свойство -> } -> ``` -> -> **`by lazy` заменяет двойную проверку блокировки.** Классический ленивый синглтон в Java требует `volatile` и синхронизации; в Kotlin это одна строка. -> -> Сравнить: [Инкапсуляция в Java](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md) - -> [!warning] Подводные камни -> **Обращение к свойству внутри его сеттера - бесконечная рекурсия:** -> ```kotlin -> class Bad2 { -> var x: Int = 0 -> set(value) { field = value } // верно -> // set(value) { x = value } // StackOverflowError -> } -> -> fun main() { Bad2().x = 1; println("ok") } -> ``` -> -> **Вычисляемое свойство выполняется при каждом чтении:** -> ```kotlin -> class Slow(val items: List) { -> val max: Int get() = items.also { println("считаем") }.max() -> } -> -> fun main() { -> val s = Slow(listOf(1, 2)) -> println(s.max) -> println(s.max) // "считаем" выведется дважды -> } -> ``` -> Если вычисление дорогое, нужен `by lazy` или хранимое свойство. -> -> **`val` не означает неизменяемость объекта** - только то, что ссылку нельзя переприсвоить: -> ```kotlin -> class Box2 { val items = mutableListOf(1) } -> -> fun main() { -> val b = Box2() -> b.items.add(2) // разрешено -> println(b.items) // [1, 2] -> } -> ``` -> -> **`val` с геттером может возвращать разное:** -> ```kotlin -> class Clock { val now: Long get() = System.nanoTime() } -> -> fun main() { -> val c = Clock() -> println(c.now != c.now) // true — два разных значения -> } -> ``` -> Поэтому умное приведение для таких свойств не работает. Подробнее: [Умные приведения](../Синтаксис%20и%20типы/Null-безопасность/Умные%20приведения.md) -> -> **`lateinit` не работает с примитивами и с `val`.** Для `Int` используйте `Delegates.notNull()` или значение-маркер. -> -> **`lateinit` скрывает незаданную зависимость до рантайма.** Чем раньше объект получает всё необходимое (лучше - в конструкторе), тем меньше шансов на `UninitializedPropertyAccessException`. -> -> **`by lazy` с исключением внутри повторит попытку:** -> ```kotlin -> class Risky { -> var attempts = 0 -> val value: String by lazy { -> attempts++ -> if (attempts < 2) error("первая попытка падает") -> "успех на попытке $attempts" -> } -> } -> -> fun main() { -> val r = Risky() -> runCatching { r.value } -> println(r.value) // успех на попытке 2 -> } -> ``` -> Результат не кешируется, пока лямбда не завершится успешно. -> -> **Свойство-расширение не имеет поля** - состояние к чужому классу добавить нельзя, только вычисление. -> -> **Публичный `var` в API позволяет внешнему коду менять состояние в любой момент.** Часто правильнее `var` с `private set` и явным методом изменения. - -Дополнительно: [Делегирование](Делегирование.md) -Далее: [Инициализация](Инициализация.md) diff --git a/Kotlin/Рефлексия/Аннотации.md b/Kotlin/Рефлексия/Аннотации.md deleted file mode 100644 index 14c41c5..0000000 --- a/Kotlin/Рефлексия/Аннотации.md +++ /dev/null @@ -1,653 +0,0 @@ ---- -title: Аннотации -description: Объявление аннотаций, цели, retention, use-site targets, чтение через рефлексию, аннотации интеропа -tags: - - kotlin - - annotations - - reflection ---- - -Аннотация - метаданные, прикреплённые к объявлению. Сама по себе она ничего не делает: смысл ей придаёт тот, кто её читает - рефлексия в рантайме, процессор аннотаций в компиляции или сам компилятор. - -```kotlin -import kotlin.reflect.full.findAnnotation - -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -annotation class Table(val name: String, val schema: String = "public") - -@Table("users") -class User(val id: Int, val name: String) - -fun main() { - val table = User::class.findAnnotation() - println("таблица=${table?.name}, схема=${table?.schema}") -} -``` -``` -таблица=users, схема=public -``` - -## Объявление -```kotlin -annotation class Simple - -annotation class WithParams( - val name: String, - val priority: Int = 0, - val tags: Array = [], -) - -fun main() { - println("объявлены") -} -``` -``` -объявлены -``` -Правила для аннотаций жёстче, чем для обычных классов: -- нет тела, нет функций и состояния - только параметры конструктора; -- все параметры - `val`; -- типы параметров ограничены: примитивы, `String`, `KClass`, `enum`, другая аннотация, массивы перечисленного; -- нельзя `null` в качестве значения по умолчанию - сам тип не может быть nullable. - -## Цели - @Target -```kotlin -@Target( - AnnotationTarget.CLASS, - AnnotationTarget.FUNCTION, - AnnotationTarget.PROPERTY, - AnnotationTarget.VALUE_PARAMETER, -) -annotation class Marker - -@Marker -class Annotated(@Marker val prop: Int) { - @Marker - fun method() = Unit -} - -fun main() { - println("применено к четырём местам") -} -``` -``` -применено к четырём местам -``` -| Цель | Куда | -|---|---| -| `CLASS` | класс, интерфейс, `object`, аннотация | -| `FUNCTION` | функция, не конструктор | -| `PROPERTY` | свойство (только Kotlin-код) | -| `FIELD` | backing field свойства | -| `VALUE_PARAMETER` | параметр функции или конструктора | -| `CONSTRUCTOR` | конструктор | -| `PROPERTY_GETTER`, `PROPERTY_SETTER` | аксессоры | -| `LOCAL_VARIABLE` | локальная переменная | -| `TYPE` | использование типа: `List<@Marker String>` | -| `TYPE_PARAMETER` | параметр типа | -| `EXPRESSION` | выражение (требует `SOURCE`) | -| `FILE` | файл, через `@file:` | -| `ANNOTATION_CLASS` | мета-аннотация | - -Без `@Target` аннотация применима почти везде, кроме `TYPE`, `EXPRESSION`, `FILE` и `TYPE_PARAMETER` - их нужно указывать явно. - -## Хранение - @Retention -```kotlin -@Retention(AnnotationRetention.SOURCE) -@Target(AnnotationTarget.CLASS) -annotation class Src - -@Retention(AnnotationRetention.BINARY) -@Target(AnnotationTarget.CLASS) -annotation class Bin - -@Retention(AnnotationRetention.RUNTIME) -@Target(AnnotationTarget.CLASS) -annotation class Run - -@Target(AnnotationTarget.CLASS) -annotation class NoRetention - -@Src @Bin @Run -class All - -@NoRetention -class Default - -fun main() { - println("All=${All::class.annotations.map { it.annotationClass.simpleName }}") - println("Default=${Default::class.annotations.map { it.annotationClass.simpleName }}") -} -``` -``` -All=[Run] -Default=[NoRetention] -``` -| Retention | В байт-коде | Видна рефлексии | -|---|---|---| -| `SOURCE` | нет | нет | -| `BINARY` | да | нет | -| `RUNTIME` (по умолчанию) | да | да | - -Ключевое отличие от Java: **по умолчанию `RUNTIME`**, а не `CLASS`. Поэтому `@NoRetention` без явного указания видна через рефлексию - в Java аналогичная аннотация была бы невидима. Для аннотаций, которые читает только KSP или линтер, стоит явно ставить `SOURCE`: это убирает их из байт-кода. - -## Use-site targets -Свойство в Kotlin компилируется в несколько Java-элементов: поле, геттер, иногда сеттер, а для параметра конструктора ещё и параметр. Аннотация должна попасть на конкретный из них - для этого есть префиксы. - -```kotlin -import kotlin.reflect.KMutableProperty1 -import kotlin.reflect.full.memberProperties -import kotlin.reflect.full.primaryConstructor -import kotlin.reflect.jvm.javaConstructor -import kotlin.reflect.jvm.javaField -import kotlin.reflect.jvm.javaGetter -import kotlin.reflect.jvm.javaSetter - -@Target( - AnnotationTarget.PROPERTY, - AnnotationTarget.FIELD, - AnnotationTarget.PROPERTY_GETTER, - AnnotationTarget.PROPERTY_SETTER, - AnnotationTarget.VALUE_PARAMETER, -) -@Retention(AnnotationRetention.RUNTIME) -annotation class Mark(val where: String) - -class Targets( - @Mark("без префикса") val implicit: Int, - @field:Mark("field") val onField: Int, - @get:Mark("getter") val onGetter: Int, - @property:Mark("property") val onProperty: Int, -) { - @set:Mark("setter") - var mutable: Int = 0 -} - -fun main() { - for (p in Targets::class.memberProperties.sortedBy { it.name }) { - val prop = p.annotations.filterIsInstance().map { it.where } - val field = p.javaField?.annotations?.filterIsInstance()?.map { it.where }.orEmpty() - val getter = p.javaGetter?.annotations?.filterIsInstance()?.map { it.where }.orEmpty() - println("${p.name}: property=$prop field=$field getter=$getter") - } - val ctor = Targets::class.primaryConstructor!!.javaConstructor!! - ctor.parameterAnnotations.forEachIndexed { i, anns -> - println("параметр $i: ${anns.filterIsInstance().map { it.where }}") - } - val mutable = Targets::class.memberProperties.first { it.name == "mutable" } - val setter = (mutable as KMutableProperty1<*, *>).javaSetter - println("setter=${setter?.annotations?.filterIsInstance()?.map { it.where }}") -} -``` -``` -implicit: property=[без префикса] field=[] getter=[] -mutable: property=[] field=[] getter=[] -onField: property=[] field=[field] getter=[] -onGetter: property=[] field=[] getter=[getter] -onProperty: property=[property] field=[] getter=[] -параметр 0: [без префикса] -параметр 1: [] -параметр 2: [] -параметр 3: [] -setter=[setter] -``` -| Префикс | Куда попадёт | -|---|---| -| `@property:` | свойство (видно только Kotlin-рефлексии) | -| `@field:` | backing field | -| `@get:` | геттер | -| `@set:` | сеттер | -| `@param:` | параметр конструктора | -| `@setparam:` | параметр сеттера | -| `@receiver:` | получатель функции-расширения | -| `@delegate:` | поле делегата | -| `@file:` | файл, до объявления `package` | -| `@all:` | все применимые цели сразу | - -### Правило выбора по умолчанию -Без префикса аннотация применяется к **первой подходящей** цели из списка, в порядке: `param` → `property` → `field`. Это проверяется легко: -```kotlin -import kotlin.reflect.full.memberProperties -import kotlin.reflect.full.primaryConstructor -import kotlin.reflect.jvm.javaConstructor -import kotlin.reflect.jvm.javaField - -@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.PROPERTY, AnnotationTarget.FIELD) -@Retention(AnnotationRetention.RUNTIME) -annotation class Multi - -@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FIELD) -@Retention(AnnotationRetention.RUNTIME) -annotation class PropOrField - -@Target(AnnotationTarget.FIELD) -@Retention(AnnotationRetention.RUNTIME) -annotation class FieldOnly - -class C( - @Multi val a: Int, - @PropOrField val b: Int, - @FieldOnly val c: Int, -) - -fun report(name: String) { - val p = C::class.memberProperties.first { it.name == name } - val onProperty = p.annotations.map { it.annotationClass.simpleName } - val onField = p.javaField?.annotations?.map { it.annotationClass.simpleName }.orEmpty() - val index = C::class.primaryConstructor!!.parameters.indexOfFirst { it.name == name } - val onParam = C::class.primaryConstructor!!.javaConstructor!! - .parameterAnnotations[index].map { it.annotationClass.simpleName } - println("$name: property=$onProperty field=$onField param=$onParam") -} - -fun main() { - report("a") - report("b") - report("c") -} -``` -``` -a: property=[Multi] field=[] param=[Multi] -b: property=[PropOrField] field=[] param=[] -c: property=[] field=[FieldOnly] param=[] -``` -Обратите внимание на `a`: аннотация, применимая и к параметру, и к свойству, попала **на оба** - на параметр конструктора и на Kotlin-свойство (но не на поле). Именно здесь возникают проблемы с Java-библиотеками: Jackson или Hibernate читают поля через Java Reflection, а там аннотации нет - нужен явный `@field:`. - -### @all -```kotlin -import kotlin.reflect.full.memberProperties -import kotlin.reflect.full.primaryConstructor -import kotlin.reflect.jvm.javaConstructor -import kotlin.reflect.jvm.javaField -import kotlin.reflect.jvm.javaGetter - -@Target( - AnnotationTarget.PROPERTY, - AnnotationTarget.FIELD, - AnnotationTarget.PROPERTY_GETTER, - AnnotationTarget.VALUE_PARAMETER, -) -@Retention(AnnotationRetention.RUNTIME) -annotation class Mark - -class WithAll(@all:Mark val x: Int) - -fun main() { - val p = WithAll::class.memberProperties.first() - println("property=${p.annotations.size}") - println("field=${p.javaField?.annotations?.size}") - println("getter=${p.javaGetter?.annotations?.size}") - val params = WithAll::class.primaryConstructor!!.javaConstructor!!.parameterAnnotations - println("param=${params[0].size}") -} -``` -``` -property=1 -field=1 -getter=1 -param=1 -``` -`@all:` - решение давней проблемы: раньше приходилось писать `@field:Mark @get:Mark @param:Mark` подряд. В Kotlin 2.4 работает без дополнительных флагов компилятора. - -## Параметры аннотаций -```kotlin -import kotlin.reflect.KClass -import kotlin.reflect.full.findAnnotation - -interface Validator -class NotBlank : Validator -class MaxLength : Validator - -enum class Mode { FAST, SAFE } - -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -annotation class Validators(val value: Array>) - -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -annotation class Levels(val value: IntArray, val names: Array) - -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -annotation class Nested(val inner: Levels) - -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -annotation class WithEnum(val mode: Mode) - -@Validators([NotBlank::class, MaxLength::class]) -@Levels(value = [1, 2, 3], names = ["low", "mid", "high"]) -@Nested(Levels([9], ["x"])) -@WithEnum(Mode.SAFE) -class Configured - -fun main() { - val kc = Configured::class - println("классы=${kc.findAnnotation()!!.value.map { it.simpleName }}") - val levels = kc.findAnnotation()!! - println("IntArray=${levels.value.toList()} names=${levels.names.toList()}") - println("вложенная=${kc.findAnnotation()!!.inner.value.toList()}") - println("enum=${kc.findAnnotation()!!.mode}") -} -``` -``` -классы=[NotBlank, MaxLength] -IntArray=[1, 2, 3] names=[low, mid, high] -вложенная=[9] -enum=SAFE -``` -Массивы задаются литералом `[a, b]` - старый синтаксис `arrayOf(a, b)` тоже работает, но литерал короче. Классы передаются как `KClass`, то есть `Foo::class`, а не `Foo::class.java`. - -## Значения по умолчанию и equals -```kotlin -import kotlin.reflect.full.findAnnotation - -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -annotation class HasDefaults(val a: Int = 1, val b: String = "x", val c: IntArray = [1, 2]) - -@HasDefaults -class WithDefaults - -@HasDefaults(a = 5) -class Partial - -fun main() { - val d = WithDefaults::class.findAnnotation()!! - println("дефолты a=${d.a} b=${d.b} c=${d.c.toList()}") - val p = Partial::class.findAnnotation()!! - println("частично a=${p.a} b=${p.b}") - println("toString=$p") - println("равенство=${d == WithDefaults::class.findAnnotation()}") -} -``` -``` -дефолты a=1 b=x c=[1, 2] -частично a=5 b=x -toString=@HasDefaults(a=5, b="x", c={1, 2}) -равенство=true -``` -Экземпляры аннотаций сравниваются по значениям параметров, а `toString` даёт читаемое представление с подставленными дефолтами - удобно для отладки. - -## Повторяемые аннотации -```kotlin -import kotlin.reflect.full.findAnnotations - -@Repeatable -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -annotation class Tag(val value: String) - -@Tag("a") -@Tag("b") -class Tagged - -fun main() { - println("все теги=${Tagged::class.findAnnotations().map { it.value }}") - println("через annotations=${Tagged::class.annotations.filterIsInstance().map { it.value }}") -} -``` -``` -все теги=[a, b] -через annotations=[a, b] -``` -В Java для повторяемых аннотаций нужен явный контейнер (`@Repeatable(Tags.class)` плюс класс `Tags` с массивом). В Kotlin достаточно `@Repeatable`, а для чтения есть `findAnnotations()`, возвращающий список. - -## Чтение через рефлексию -```kotlin -import kotlin.reflect.full.findAnnotation -import kotlin.reflect.full.hasAnnotation -import kotlin.reflect.full.memberProperties - -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -annotation class Table(val name: String) - -@Target(AnnotationTarget.PROPERTY) -@Retention(AnnotationRetention.RUNTIME) -annotation class Column(val name: String, val nullable: Boolean = false) - -@Table("users") -class User( - @Column("user_id") val id: Int, - @Column("full_name", nullable = true) val name: String?, -) - -fun main() { - val kc = User::class - println("hasAnnotation=${kc.hasAnnotation
()}") - println("findAnnotation=${kc.findAnnotation
()?.name}") - for (p in kc.memberProperties.sortedBy { it.name }) { - val column = p.findAnnotation() - println("${p.name}: column=${column?.name} nullable=${column?.nullable}") - } -} -``` -``` -hasAnnotation=true -findAnnotation=users -id: column=user_id nullable=false -name: column=full_name nullable=true -``` -| Функция | Что возвращает | -|---|---| -| `annotations` | список всех `RUNTIME`-аннотаций элемента | -| `findAnnotation()` | первую аннотацию типа `T` или `null` | -| `findAnnotations()` | все аннотации типа `T` (для `@Repeatable`) | -| `hasAnnotation()` | есть ли аннотация | -| `annotationClass` | `KClass` самой аннотации | - -Эти функции есть у `KClass`, `KProperty`, `KFunction` и `KParameter` - интерфейс единый. Подробнее: [Рефлексия](Рефлексия.md) - -## Практический пример: валидация -```kotlin -import kotlin.reflect.full.findAnnotation -import kotlin.reflect.full.memberProperties -import kotlin.reflect.jvm.isAccessible - -@Target(AnnotationTarget.PROPERTY) -@Retention(AnnotationRetention.RUNTIME) -annotation class NotBlank(val message: String = "не должно быть пустым") - -@Target(AnnotationTarget.PROPERTY) -@Retention(AnnotationRetention.RUNTIME) -annotation class Range(val min: Int, val max: Int) - -data class Form( - @NotBlank val name: String, - @NotBlank("email обязателен") val email: String, - @Range(18, 120) val age: Int, -) - -fun validate(obj: Any): List { - val errors = mutableListOf() - for (p in obj::class.memberProperties) { - p.isAccessible = true - val value = p.call(obj) - p.findAnnotation()?.let { rule -> - if ((value as? String).isNullOrBlank()) errors += "${p.name}: ${rule.message}" - } - p.findAnnotation()?.let { rule -> - val number = value as? Int ?: return@let - if (number !in rule.min..rule.max) { - errors += "${p.name}: должно быть от ${rule.min} до ${rule.max}" - } - } - } - return errors.sorted() -} - -fun main() { - println(validate(Form("Иван", "ivan@example.com", 30))) - println(validate(Form("", "", 5))) -} -``` -``` -[] -[age: должно быть от 18 до 120, email: email обязателен, name: не должно быть пустым] -``` -Это рабочая схема, но она платит рефлексией в рантайме: разбор метаданных при первом обращении и потеря проверок на этапе компиляции. Тот же результат даёт KSP-процессор, генерирующий код валидации при сборке. Подробнее: [KSP](../Инструментарий/KSP.md) - -## Аннотации компилятора -Часть аннотаций обрабатывает сам компилятор - они меняют генерируемый код или проверки. - -| Аннотация | Что делает | -|---|---| -| `@Deprecated` | предупреждение или ошибка при использовании | -| `@Suppress("NAME")` | подавляет предупреждение в области применения | -| `@JvmName` | имя в байт-коде | -| `@JvmStatic` | статический метод для членов `companion object` | -| `@JvmOverloads` | перегрузки для параметров со значениями по умолчанию | -| `@JvmField` | публичное поле вместо геттера | -| `@JvmInline` | обязательна для `value class` | -| `@Throws` | добавляет `throws` в сигнатуру для Java | -| `@OptIn`, `@RequiresOptIn` | механизм согласия на нестабильный API | -| `@PublishedApi` | доступ к `internal` из `public inline` | -| `@Volatile`, `@Synchronized` | то же, что в Java | -| `@Transient` | исключение из сериализации | -| `@DslMarker` | ограничение области в DSL | -| `@BuilderInference` | вывод типа в билдерах | - -```kotlin -import kotlin.reflect.full.findAnnotation - -@Deprecated("используйте new()", ReplaceWith("new()"), DeprecationLevel.WARNING) -fun old() = 1 - -fun new() = 2 - -fun main() { - val d = ::old.findAnnotation() - println("message=${d?.message}") - println("level=${d?.level}") - println("replaceWith=${d?.replaceWith?.expression}") -} -``` -``` -message=используйте new() -level=WARNING -replaceWith=new() -``` -`ReplaceWith` понимает IDE: она предлагает автозамену вызова. `DeprecationLevel.ERROR` делает использование ошибкой компиляции, `HIDDEN` убирает объявление из автодополнения, сохраняя бинарную совместимость. - -Аннотации интеропа меняют байт-код заметно: -```kotlin -@file:JvmName("InteropUtils") - -import java.io.IOException -import java.lang.reflect.Modifier - -class Interop { - @Throws(IOException::class) - fun mayThrow() = Unit - - @JvmField - val field = 1 - - @get:JvmName("fetchValue") - val value = 2 - - @JvmOverloads - fun withDefaults(a: Int, b: Int = 1, c: Int = 2) = a + b + c - - companion object { - @JvmStatic - fun staticFn() = 3 - } -} - -fun main() { - val c = Interop::class.java - println("методы=${c.declaredMethods.map { it.name }.sorted()}") - println("поля=${c.declaredFields.map { it.name }.sorted()}") - println("throws=${c.getDeclaredMethod("mayThrow").exceptionTypes.map { it.simpleName }}") - println("статические=${c.declaredMethods.filter { Modifier.isStatic(it.modifiers) }.map { it.name }.sorted()}") -} -``` -``` -методы=[fetchValue, mayThrow, staticFn, withDefaults, withDefaults, withDefaults, withDefaults$default] -поля=[Companion, field, value] -throws=[IOException] -статические=[staticFn, withDefaults$default] -``` -`@JvmOverloads` дал три перегрузки `withDefaults`, `@get:JvmName` переименовал геттер в `fetchValue`, `@JvmField` превратил свойство в поле без геттера. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md) - -## Аннотации для KSP -Аннотация, предназначенная процессору, не нужна в рантайме: -```kotlin -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.SOURCE) -annotation class GenerateBuilder - -@GenerateBuilder -data class Person(val name: String, val age: Int) - -fun main() { - println("в рантайме=${Person::class.annotations.size}") -} -``` -``` -в рантайме=0 -``` -`SOURCE` убирает аннотацию из байт-кода полностью - KSP читает исходники, а не классы. Это уменьшает размер артефакта и исключает случайное чтение рефлексией. Подробнее: [KSP](../Инструментарий/KSP.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `@interface Foo { String value(); }` | `annotation class Foo(val value: String)` | -> | `String value() default "x"` | `val value: String = "x"` | -> | retention по умолчанию `CLASS` | по умолчанию `RUNTIME` | -> | `@Target(ElementType.FIELD)` | `@Target(AnnotationTarget.FIELD)` | -> | нет понятия свойства | `@property:`, `@field:`, `@get:`, `@set:` | -> | `@Repeatable(Container.class)` + контейнер | просто `@Repeatable` | -> | `Foo.class` в параметре | `Foo::class` | -> | `{1, 2}` для массива | `[1, 2]` | -> | `getAnnotation(Foo.class)` | `findAnnotation()` | -> | аннотации на файл невозможны | `@file:JvmName(...)` | -> -> **Use-site targets существуют потому, что свойство - не поле.** В Java `@Column private String name` однозначна: цель одна. В Kotlin `val name: String` порождает поле, геттер и Kotlin-свойство как отдельную сущность в метаданных, а в конструкторе ещё и параметр - аннотация должна выбрать одну цель. По умолчанию выбирается первая применимая в порядке `param` → `property` → `field`, и это главная причина, по которой Java-библиотеки «не видят» аннотацию: Jackson читает поле, а аннотация оказалась на параметре. Префикс `@field:` или новый `@all:` решают проблему. Подробнее: [Свойства](../ООП/Свойства.md) -> -> **Retention по умолчанию - `RUNTIME`, а не `CLASS`.** Java по умолчанию не включает аннотацию в рантайм, поэтому каждая библиотека обязана ставить `@Retention(RUNTIME)` явно. Kotlin выбрал обратный дефолт - аннотации чаще читают рефлексией, чем процессором. Обратная сторона: аннотация для линтера без явного `SOURCE` попадёт и в байт-код, и в рантайм, увеличивая артефакт. -> -> **Повторяемость не требует класса-контейнера.** В Java `@Repeatable` обязывает объявить отдельную аннотацию-контейнер с массивом - лишний код, который приходится писать для каждой повторяемой аннотации. Kotlin генерирует контейнер сам; `findAnnotations()` возвращает список, и раскрывать контейнер вручную не нужно. -> -> **Аннотации на файл - новая цель, которой в Java нет.** `@file:JvmName("Utils")` меняет имя класса-контейнера для функций верхнего уровня, `@file:Suppress` подавляет предупреждения во всём файле. В Java единица объявления - всегда класс, поэтому и цели `FILE` быть не может. Подробнее: [Пакеты и импорты](../Синтаксис%20и%20типы/Базовый%20синтаксис/Пакеты%20и%20импорты.md) -> -> **Генерация кода вместо рефлексии - предпочтительный путь.** В Java-экосистеме аннотации почти всегда читаются рефлексией: Spring, Jackson, Hibernate строят модель при старте приложения. Kotlin подталкивает к другому: `kotlinx.serialization` и KSP обрабатывают аннотации при компиляции и генерируют код, поэтому `kotlin-reflect` не нужен, старт быстрее, а ошибки видны при сборке. Для Kotlin/Native и Kotlin/JS это единственный работающий вариант. Подробнее: [KSP](../Инструментарий/KSP.md) -> -> Сравнить: [Reflection API в Java](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md) - -> [!warning] Подводные камни -> **Аннотация без префикса может не попасть туда, куда нужно Java-библиотеке.** Проверено: аннотация с целями `VALUE_PARAMETER` и `PROPERTY` на `val` в конструкторе попадает на параметр и Kotlin-свойство, но **не на поле** - Jackson, Hibernate и JPA её не увидят. Нужен `@field:` или `@all:`. -> -> **`@property:` видна только Kotlin-рефлексии.** В Java-представлении такой аннотации нет вообще: она хранится в метаданных Kotlin. Библиотека на Java Reflection её не найдёт никогда. -> -> **Retention по умолчанию `RUNTIME` раздувает байт-код.** Аннотации для KSP, линтеров и внутренних проверок стоит помечать `@Retention(AnnotationRetention.SOURCE)` - иначе они остаются в классах и видны рефлексии без необходимости. -> -> **`BINARY` не видна рефлексии, хотя лежит в байт-коде.** Проверено: из `@Src @Bin @Run` рефлексия вернёт только `Run`. Это частая причина «аннотация есть в коде, а `findAnnotation` даёт `null`». -> -> **`null` в качестве значения аннотации невозможен.** Типы параметров не могут быть nullable, поэтому «отсутствие значения» кодируют пустой строкой или сигнальной константой. Это ограничение формата аннотаций в JVM, а не Kotlin. -> -> **Параметры аннотаций ограничены по типам.** `data class`, коллекции, `Duration` и функции использовать нельзя - только примитивы, `String`, `KClass`, `enum`, другая аннотация и массивы из них. Попытка передать `List` вместо `Array` не скомпилируется. -> -> **`KClass` в параметре, а не `Class`.** `@Validators([NotBlank::class])` - правильно, `NotBlank::class.java` в аннотации не примут. При чтении обратно получается `KClass`, и для Java-API нужен `.java`. -> -> **`@JvmStatic` работает только в `companion object` и `object`.** На обычном методе класса даёт ошибку компиляции - статических методов у Kotlin-классов не бывает. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md) -> -> **`@JvmOverloads` генерирует перегрузки только справа налево.** Для `f(a, b = 1, c = 2)` появятся `f(a)`, `f(a, b)`, `f(a, b, c)` - вызвать `f(a, c)` из Java не получится. Проверено на реальном байт-коде. -> -> **`@Throws` обязательна для Java-вызывающих.** Kotlin не имеет проверяемых исключений, поэтому без `@Throws` Java-код не сможет написать `catch (IOException e)` - компилятор Java сообщит, что исключение не бросается. Подробнее: [Система исключений](../Исключения/Система%20исключений.md) -> -> **Аннотации на типах требуют явной цели `TYPE`.** `List<@Marker String>` не скомпилируется, если в `@Target` нет `AnnotationTarget.TYPE` - по умолчанию эта цель не включена. -> -> **`@Target(AnnotationTarget.EXPRESSION)` работает только с `SOURCE`.** Выражения не существуют в байт-коде, поэтому `RUNTIME` с такой целью даст ошибку компиляции. -> -> **`@all:` - новая возможность и ломает сборку на старых версиях Kotlin.** В 2.4 работает без флагов, в более ранних версиях требует `-Xannotation-target-all` или недоступна вовсе. Для библиотек с широкой совместимостью пока надёжнее перечислять цели вручную. -> -> **Чтение аннотаций рефлексией стоит дорого при первом обращении.** Это тот же разбор метаданных, что и у остальной рефлексии: ~150 мс на класс впервые. Кешируйте результат, а лучше переносите обработку в KSP. Подробнее: [Рефлексия](Рефлексия.md) -> -> **Обфускация ломает аннотации с именами в строках.** `@Column("user_id")` переживёт R8, а вот поиск свойства по имени из аннотации - нет: само свойство переименуется. Нужны правила `-keep`. - -Далее: [Kotlin](../Kotlin.md) diff --git a/Kotlin/Рефлексия/Рефлексия.md b/Kotlin/Рефлексия/Рефлексия.md deleted file mode 100644 index ffd63f0..0000000 --- a/Kotlin/Рефлексия/Рефлексия.md +++ /dev/null @@ -1,505 +0,0 @@ ---- -title: Рефлексия -description: KClass, KProperty, KFunction, kotlin-reflect, ссылки на члены, интероп с Java Reflection -tags: - - kotlin - - reflection ---- - -Рефлексия - доступ к структуре программы во время выполнения: какие у класса свойства, какие параметры у функции, как вызвать то, что не известно на этапе компиляции. Kotlin даёт собственный API (`kotlin.reflect`) поверх Java Reflection, потому что в байт-коде не видно ни свойств, ни `suspend`, ни значений по умолчанию. - -```kotlin -import kotlin.reflect.full.memberProperties - -data class User(val id: Int, var name: String) - -fun main() { - val user = User(1, "Иван") - User::class.memberProperties - .sortedBy { it.name } - .forEach { println("${it.name} = ${it.call(user)}") } -} -``` -``` -id = 1 -name = Иван -``` - -## Два уровня API -Kotlin-рефлексия разделена на две части, и это важно практически. - -```kotlin -// Доступно всегда, только kotlin-stdlib -import kotlin.reflect.KClass - -enum class Status { OK } -data class P(val x: Int) - -fun main() { - println(Status::class.simpleName) - println(P::class.qualifiedName) - println(P::class.java.name) - println(P::class.isInstance(P(1))) -} -``` -``` -Status -P -P -true -``` -Без `kotlin-reflect` в classpath работают только `simpleName`, `qualifiedName`, `isInstance`, `java` и несколько похожих свойств. Всё остальное - `memberProperties`, `primaryConstructor`, `sealedSubclasses` - бросает `KotlinReflectionNotSupportedError`: -``` -Exception in thread "main" kotlin.jvm.KotlinReflectionNotSupportedError: -Kotlin reflection implementation is not found at runtime. -Make sure you have kotlin-reflect.jar in the classpath -``` - -Подключается отдельно: -```kotlin -// build.gradle.kts -dependencies { - implementation(kotlin("reflect")) -} -``` -`kotlin-reflect` весит около 3 МБ и тянет собственный парсер метаданных - для Android и serverless это значимо. Подробнее: [Gradle и Kotlin DSL](../Инструментарий/Gradle%20и%20Kotlin%20DSL.md) - -## KClass -```kotlin -import kotlin.reflect.full.companionObject -import kotlin.reflect.full.declaredMemberFunctions -import kotlin.reflect.full.memberProperties -import kotlin.reflect.full.primaryConstructor - -data class User(val id: Int, var name: String) { - val display: String get() = "#$id $name" - fun greet(greeting: String): String = "$greeting, $name" - companion object { const val TABLE = "users" } -} - -fun main() { - val kc = User::class - println("simpleName=${kc.simpleName}") - println("isData=${kc.isData}") - println("isFinal=${kc.isFinal}") - println("visibility=${kc.visibility}") - println("primary params=${kc.primaryConstructor!!.parameters.map { it.name }}") - println("memberProperties=${kc.memberProperties.map { it.name }.sorted()}") - println("функции=${kc.declaredMemberFunctions.map { it.name }.sorted()}") - println("companionObject=${kc.companionObject?.simpleName}") -} -``` -``` -simpleName=User -isData=true -isFinal=true -visibility=PUBLIC -primary params=[id, name] -memberProperties=[display, id, name] -функции=[component1, component2, copy, equals, greet, hashCode, toString] -companionObject=Companion -``` -Видно, что `data class` генерирует `componentN`, `copy`, `equals`, `hashCode`, `toString` - рефлексия показывает их наравне с написанными руками. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md) - -| Свойство `KClass` | Что даёт | -|---|---| -| `simpleName`, `qualifiedName` | имена; `null` для локальных и анонимных | -| `isData`, `isSealed`, `isInner`, `isCompanion`, `isValue`, `isFun` | характер класса | -| `isAbstract`, `isFinal`, `isOpen` | модальность | -| `visibility` | `PUBLIC`, `INTERNAL`, `PROTECTED`, `PRIVATE` | -| `constructors`, `primaryConstructor` | конструкторы | -| `memberProperties`, `declaredMemberProperties` | свойства с унаследованными и без | -| `memberFunctions`, `declaredMemberFunctions` | функции | -| `supertypes`, `superclasses` | иерархия | -| `sealedSubclasses` | подклассы `sealed`-типа | -| `nestedClasses` | вложенные классы | -| `objectInstance` | экземпляр `object`, иначе `null` | -| `companionObject`, `companionObjectInstance` | компаньон | -| `typeParameters` | параметры типа | -| `java` | мост к `Class` | - -### sealed, enum, object -```kotlin -import kotlin.reflect.full.isSubclassOf - -sealed interface Shape { - data class Circle(val r: Double) : Shape - data class Square(val a: Double) : Shape -} - -enum class Status { OK, FAIL } - -object Single { val x = 1 } - -open class Base -class Derived : Base() - -fun main() { - println("sealedSubclasses=${Shape::class.sealedSubclasses.map { it.simpleName }}") - println("enum values=${Status::class.java.enumConstants.toList()}") - println("object instance=${Single::class.objectInstance?.x}") - println("supertypes=${Derived::class.supertypes}") - println("isSubclassOf=${Derived::class.isSubclassOf(Base::class)}") -} -``` -``` -sealedSubclasses=[Circle, Square] -enum values=[OK, FAIL] -object instance=1 -supertypes=[Base] -isSubclassOf=true -``` -`sealedSubclasses` даёт полный список вариантов - это основа для сериализаторов и фабрик по типу. Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md) - -## Ссылки на члены -```kotlin -import kotlin.reflect.KFunction2 -import kotlin.reflect.KMutableProperty1 -import kotlin.reflect.full.memberProperties -import kotlin.reflect.full.declaredMemberFunctions - -data class User(val id: Int, var name: String) { - fun greet(greeting: String): String = "$greeting, $name" -} - -fun topLevel(a: Int, b: String = "по умолчанию"): String = "$a-$b" - -fun main() { - val user = User(1, "Иван") - - // свойство как объект - @Suppress("UNCHECKED_CAST") - val nameProp = User::class.memberProperties - .first { it.name == "name" } as KMutableProperty1 - println("get=${nameProp.get(user)}") - nameProp.set(user, "Пётр") - println("после set=${user.name}") - - // функция класса - val greet = User::class.declaredMemberFunctions.first { it.name == "greet" } - println("call=${greet.call(user, "Привет")}") - - // ссылка на функцию верхнего уровня - val ref: KFunction2 = ::topLevel - println("name=${ref.name}, параметров=${ref.parameters.size}") - println("call=${ref.call(1, "b")}") - println("callBy с дефолтом=${ref.callBy(mapOf(ref.parameters[0] to 5))}") -} -``` -``` -get=Иван -после set=Пётр -call=Привет, Пётр -name=topLevel, параметров=2 -call=1-b -callBy с дефолтом=5-по умолчанию -``` -| Тип | Что представляет | -|---|---| -| `KProperty0` | свойство без получателя: `::topLevelVal`, `obj::prop` | -| `KProperty1` | свойство класса: `User::name` | -| `KProperty2` | свойство-расширение с двумя получателями | -| `KMutableProperty1` | то же для `var` - добавляет `set` | -| `KFunction` | функция; `KFunction1`, `KFunction2`, ... по арности | -| `KParameter` | параметр: `name`, `type`, `isOptional`, `kind` | -| `KType` | тип с аргументами и `isMarkedNullable` | - -`call` и `callBy` различаются принципиально: `call` требует все аргументы по порядку, `callBy` принимает `Map` и **учитывает значения по умолчанию** для отсутствующих. Значения по умолчанию в байт-коде не видны, поэтому через Java Reflection `callBy` не воспроизвести. Подробнее: [Функции](../Синтаксис%20и%20типы/Работа%20с%20данными/Функции.md) - -### Приватные члены -```kotlin -import kotlin.reflect.full.declaredMemberProperties -import kotlin.reflect.jvm.isAccessible - -class Hidden { - private val secret = 42 -} - -fun main() { - val prop = Hidden::class.declaredMemberProperties.first { it.name == "secret" } - - val error = runCatching { prop.call(Hidden()) }.exceptionOrNull() - println("без isAccessible: ${error!!::class.simpleName}") - - prop.isAccessible = true - println("с isAccessible: ${prop.call(Hidden())}") -} -``` -``` -без isAccessible: IllegalCallableAccessException -с isAccessible: 42 -``` -`isAccessible = true` - это Kotlin-обёртка над `setAccessible` из Java. На JDK 17+ она может упираться в строгую инкапсуляцию модулей для классов стандартной библиотеки; для собственного кода работает всегда. - -## Типы -```kotlin -import kotlin.reflect.full.memberProperties -import kotlin.reflect.typeOf - -class G { - val field: List> = emptyList() -} - -fun nullableParam(x: String?) = x - -fun main() { - val t = typeOf>>() - println("typeOf=$t") - println("classifier=${t.classifier}") - println("аргументы=${t.arguments}") - - val prop = G::class.memberProperties.first() - println("тип свойства=${prop.returnType}") - println("вложенный аргумент=${prop.returnType.arguments[0].type}") - - val p = ::nullableParam.parameters[0] - println("nullable=${p.type.isMarkedNullable}") -} -``` -``` -typeOf=kotlin.collections.List> -classifier=class kotlin.collections.List -аргументы=[kotlin.collections.Map] -тип свойства=kotlin.collections.List> -вложенный аргумент=kotlin.collections.Map -nullable=true -``` -`KType` сохраняет аргументы типов и признак nullable - эта информация берётся из метаданных Kotlin, а не из байт-кода. Но это **статическая** информация: тип объявления, а не фактический тип значения. - -```kotlin -fun main() { - val list: List = listOf("a") - println("класс=${list::class.simpleName}") - println("параметры типа=${list::class.typeParameters.map { it.name }}") -} -``` -``` -класс=SingletonList -параметры типа=[E] -``` -У значения узнать аргумент типа нельзя - дженерики стираются. `list::class` даёт лишь имя параметра `E`, а не `String`. Для сохранения типа нужен `reified` на этапе компиляции. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md) - -## Делегаты -```kotlin -import kotlin.reflect.KProperty0 -import kotlin.reflect.jvm.isAccessible - -class Lazily { - val value: String by lazy { "вычислено" } -} - -fun main() { - val obj = Lazily() - val prop: KProperty0 = obj::value - prop.isAccessible = true - - println("делегат до чтения=${prop.getDelegate()}") - println("значение=${obj.value}") - println("делегат после=${prop.getDelegate()}") -} -``` -``` -делегат до чтения=Lazy value not initialized yet. -значение=вычислено -делегат после=вычислено -``` -`getDelegate()` возвращает сам объект-делегат, а не значение свойства - для `by lazy` это `Lazy`, чей `toString` до инициализации сообщает «not initialized yet». Это единственный способ узнать состояние ленивого свойства, не вычисляя его. Подробнее: [Делегирование](../ООП/Делегирование.md) - -## Интероп с Java Reflection -```kotlin -import kotlin.reflect.full.primaryConstructor -import kotlin.reflect.jvm.javaConstructor -import kotlin.reflect.jvm.javaField -import kotlin.reflect.jvm.javaMethod -import kotlin.reflect.jvm.kotlinFunction -import kotlin.reflect.full.memberProperties - -data class Point(val x: Int, val y: Int) - -class Target { - lateinit var late: String - val lazyProp: Int by lazy { 7 } -} - -fun topLevel(a: Int): Int = a - -fun main() { - println("KClass -> Class=${Point::class.java.name}") - println("Class -> KClass=${Point::class.java.kotlin.simpleName}") - println("javaMethod=${::topLevel.javaMethod?.name}") - println("Method -> KFunction=${::topLevel.javaMethod?.kotlinFunction?.name}") - println("javaConstructor=${Point::class.primaryConstructor?.javaConstructor}") - - val late = Target::class.memberProperties.first { it.name == "late" } - println("lateinit isLateinit=${late.isLateinit}, javaField=${late.javaField?.name}") - - val lz = Target::class.memberProperties.first { it.name == "lazyProp" } - println("lazy javaField=${lz.javaField?.name}") -} -``` -``` -KClass -> Class=Point -Class -> KClass=Point -javaMethod=topLevel -Method -> KFunction=topLevel -javaConstructor=public Point(int,int) -lateinit isLateinit=true, javaField=late -lazy javaField=lazyProp$delegate -``` -| Переход | Как | -|---|---| -| `KClass` → `Class` | `kc.java` | -| `Class` → `KClass` | `cls.kotlin` | -| `KFunction` → `Method` | `fn.javaMethod` | -| `KFunction` → `Constructor` | `fn.javaConstructor` | -| `KProperty` → `Field` | `prop.javaField` | -| `KProperty` → геттер/сеттер | `prop.javaGetter`, `prop.javaSetter` | -| `Method` → `KFunction` | `method.kotlinFunction` | -| `Field` → `KProperty` | `field.kotlinProperty` | - -Поле делегированного свойства называется `имя$delegate` - это важно для библиотек, которые ищут поля по имени. Подробнее: [Kotlin в байт-коде](../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md) - -### Что Java Reflection не видит -```kotlin -class Visibility { - private val hidden = 1 - internal val internalProp = 2 - val open = 3 -} - -fun main() { - println("java методы=${Visibility::class.java.declaredMethods.map { it.name }.sorted()}") - println("java поля=${Visibility::class.java.declaredFields.map { it.name }.sorted()}") -} -``` -``` -java методы=[getInternalProp$main, getOpen] -java поля=[hidden, internalProp, open] -``` -Через Java API видны только геттеры (`private val` вообще не имеет метода), а `internal` превращается в `public` с манглированным именем `getInternalProp$main`. Kotlin-рефлексия показывает исходную картину: три свойства с видимостями `PRIVATE`, `INTERNAL`, `PUBLIC`. - -## Практический пример: маппинг -```kotlin -import kotlin.reflect.KClass -import kotlin.reflect.full.memberProperties -import kotlin.reflect.full.primaryConstructor - -data class Config(val host: String, val port: Int, val debug: Boolean) - -fun toMap(obj: Any): Map = - obj::class.memberProperties.associate { it.name to it.call(obj) } - -fun fromMap(kClass: KClass, map: Map): T { - val ctor = requireNonNull(kClass.primaryConstructor) - val args = ctor.parameters - .filter { map.containsKey(it.name) || !it.isOptional } - .associateWith { map[it.name] } - return ctor.callBy(args) -} - -private fun requireNonNull(value: T?): T = - requireNotNull(value) { "у класса нет первичного конструктора" } - -fun main() { - val config = Config("localhost", 8080, true) - println(toMap(config).toSortedMap()) - - val restored = fromMap( - Config::class, - mapOf("host" to "example.com", "port" to 443, "debug" to false), - ) - println("восстановлено=$restored") -} -``` -``` -{debug=true, host=localhost, port=8080} -восстановлено=Config(host=example.com, port=443, debug=false) -``` -Фильтр по `isOptional` позволяет не передавать параметры со значениями по умолчанию - этого достаточно для простого маппера конфигурации. Для сериализации в продакшене лучше взять `kotlinx.serialization`: она генерирует код на этапе компиляции и работает без `kotlin-reflect`. - -## Производительность -Измерено на 200 000 обращений к свойству `data class`: -``` -прямой доступ мс=2 -через рефлексию мс=9 -замедление раз=3 -``` -Само обращение медленнее в несколько раз - для большинства задач приемлемо. Дороже стоит **первое** обращение к метаданным: -``` -первое обращение к memberProperties мс=158 -второе обращение мкс=16 -``` -158 миллисекунд против 16 микросекунд: `kotlin-reflect` при первом запросе разбирает аннотацию `@Metadata` класса и строит модель. Отсюда простое правило: результаты `memberProperties` и `primaryConstructor` нужно кешировать, а не запрашивать в цикле. - -Для многих задач рефлексия вообще не нужна: -| Задача | Альтернатива | -|---|---| -| сериализация | `kotlinx.serialization` - генерация в компиляции | -| DI | ручная сборка или KSP-генерация | -| валидация по аннотациям | KSP-процессор | -| копирование с изменением | `copy()` у `data class` | -| вызов по имени | `Map Unit>` | -| проверка типа | `is` и `when` по `sealed` | - -Подробнее: [KSP](../Инструментарий/KSP.md) - -> [!info] Отличия от Java -> | Java Reflection | kotlin.reflect | -> |---|---| -> | `Class` | `KClass` | -> | `Field` + `Method` раздельно | `KProperty` как единое целое | -> | `Method` | `KFunction` | -> | в JDK, доступна всегда | отдельная зависимость `kotlin-reflect` | -> | значения по умолчанию не видны | `callBy` их учитывает | -> | nullability не видна | `KType.isMarkedNullable` | -> | аргументы типов стёрты | `KType.arguments` из метаданных | -> | `setAccessible(true)` | `isAccessible = true` | -> | `getSuperclass()` | `supertypes`, `superclasses` | -> | подклассы `sealed` не перечислить | `sealedSubclasses` | -> -> **Свойство - первичная сущность, а не пара «поле + методы».** В Java `private String name` с геттером и сеттером - это `Field` и два `Method`, никак формально не связанные: код, обходящий бин, вынужден угадывать соответствие по именам (`getName` → `name`) и спотыкаться на `isActive` для `boolean`. `KProperty` представляет свойство целиком, с `get`, `set` (у `KMutableProperty`), типом и видимостью; переходы к Java-представлению есть, но нужны редко. Подробнее: [Свойства](../ООП/Свойства.md) -> -> **Метаданные Kotlin хранят то, чего в байт-коде нет.** Компилятор пишет в класс аннотацию `@Metadata` с описанием исходной структуры: nullability, аргументы типов, `suspend`, значения по умолчанию, `internal`-видимость, имена параметров. Java Reflection этого не видит и показывает результат компиляции - `getInternalProp$main` вместо `internal val internalProp`, `Object f(int, Continuation)` вместо `suspend fun f(x: Int): String`. Цена - 3 МБ `kotlin-reflect` и разбор метаданных при первом обращении. Подробнее: [Kotlin в байт-коде](../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md) -> -> **Рефлексия не входит в стандартную библиотеку, и это осознанное решение.** В Java рефлексия доступна в любой программе - её используют Spring, Jackson, Hibernate, и платить за неё приходится всем. Kotlin целится и в Android с Kotlin/Native, где 3 МБ и динамический разбор метаданных неприемлемы, поэтому полная рефлексия вынесена в отдельный артефакт и на Native отсутствует вовсе. Это подталкивает к генерации кода: `kotlinx.serialization` и KSP решают те же задачи в компиляции. Подробнее: [KSP](../Инструментарий/KSP.md) -> -> **`callBy` возможен только благодаря метаданным.** Значения по умолчанию в Java-байт-коде превращаются в набор перегрузок или в синтетический метод с битовой маской - восстановить «вызови конструктор, остальное по умолчанию» через `Constructor.newInstance` нельзя. `callBy` принимает карту известных параметров и подставляет дефолты сам, что делает рефлективное создание объектов из `Map` практичным. -> -> Сравнить: [Reflection API в Java](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md) - -> [!warning] Подводные камни -> **Без `kotlin-reflect` в classpath большая часть API падает в рантайме.** `KotlinReflectionNotSupportedError` возникает при обращении к `memberProperties`, `primaryConstructor`, `sealedSubclasses` - компилятор ошибки не покажет, потому что объявления берутся из stdlib. Добавляйте `implementation(kotlin("reflect"))`. -> -> **Первое обращение к метаданным класса стоит ~150 мс.** Измерено: `memberProperties` впервые - 158 мс, повторно - 16 мкс. В цикле или на горячем пути это катастрофа. Кешируйте `KClass`-производные в `val` на уровне объекта или в `Map`. -> -> **`memberProperties` включает унаследованные, `declaredMemberProperties` - нет.** Путаница этих двух даёт либо лишние поля родителя при сериализации, либо потерю унаследованных. Выбирайте осознанно. -> -> **Доступ к `private` без `isAccessible = true` бросает `IllegalCallableAccessException`.** Это отдельный тип, не `IllegalAccessException` из Java - обработчики, ловящие только Java-исключение, его пропустят. -> -> **`isAccessible = true` может не сработать на JDK 17+.** Строгая инкапсуляция модулей блокирует доступ к внутренностям JDK и закрытых модулей; для своего кода ограничений нет, для чужих библиотек может потребоваться `--add-opens`. -> -> **У локальных и анонимных классов `qualifiedName` равен `null`.** У анонимного объекта `simpleName` тоже `null` - проверено. Код вида `kc.qualifiedName!!` падает именно на них, причём обычно в тестах, где анонимные объекты используются чаще. -> -> **Дженерики стираются, и фактический аргумент типа у значения недоступен.** `list::class.typeParameters` даёт `[E]`, а не `[String]`. Сохранить тип можно только через `reified` в момент компиляции или передавая `KType` параметром. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md) -> -> **`KType` отражает объявленный тип, а не фактический.** У свойства типа `Any`, в котором лежит `String`, `returnType` останется `kotlin.Any` - для определения фактического нужен `value::class`. -> -> **`call` игнорирует значения по умолчанию, `callBy` - нет.** `ctor.call("a")` на конструкторе с тремя параметрами бросит `IllegalArgumentException` даже если два имеют дефолты. Для частичных аргументов нужен `callBy` с `Map`. -> -> **`callBy` с `null` для необязательного параметра передаст именно `null`, а не дефолт.** Ключ, присутствующий в карте, считается заданным; чтобы использовать значение по умолчанию, параметр нужно **не включать** в карту вовсе. -> -> **Поле делегированного свойства называется `имя$delegate`.** `javaField?.name` для `val x by lazy { }` даст `x$delegate`, а не `x` - код, ищущий поля по имени свойства, их не найдёт. -> -> **`getDelegate()` требует `isAccessible = true` и возвращает делегат, а не значение.** Для `by lazy` это `Lazy`, чей `toString` до инициализации - «Lazy value not initialized yet.»; попытка привести его к типу значения даст `ClassCastException`. -> -> **`internal` в Java-представлении становится `public` с манглированным именем.** `getInternalProp$main` - проверено; обход через `Class.getDeclaredMethods` покажет именно это имя, а не `getInternalProp`. -> -> **`objectInstance` равен `null` для обычных классов.** Для проверки «это `object`?» нужен `kc.objectInstance != null`, а не наличие свойства. -> -> **Рефлексия обходит проверки null-безопасности.** `prop.set(obj, null)` через `KMutableProperty` с типом `String` запишет `null` в непустое свойство: компилятор не участвует, а проверки `Intrinsics` стоят только на входе в функции. Дальше код упадёт NPE в неожиданном месте. Подробнее: [Null-безопасность](../Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md) -> -> **Рефлексия ломается при обфускации.** R8 и ProGuard переименовывают классы и члены; код, ищущий свойство по строковому имени, перестаёт работать в релизной сборке и работает в отладочной. Нужны правила `-keep`. -> -> **Kotlin/Native и Kotlin/JS не поддерживают полную рефлексию.** `kotlin-reflect` существует только для JVM; в Multiplatform-коде доступны лишь `::class.simpleName` и подобное. Общий код на рефлексии не построить. - -Далее: [Аннотации](Аннотации.md) diff --git a/Kotlin/Синтаксис и типы/Null-безопасность/Null-безопасность.md b/Kotlin/Синтаксис и типы/Null-безопасность/Null-безопасность.md deleted file mode 100644 index 2d54e80..0000000 --- a/Kotlin/Синтаксис и типы/Null-безопасность/Null-безопасность.md +++ /dev/null @@ -1,185 +0,0 @@ ---- -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 deleted file mode 100644 index f0c77be..0000000 --- a/Kotlin/Синтаксис и типы/Null-безопасность/Операторы null-безопасности.md +++ /dev/null @@ -1,194 +0,0 @@ ---- -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` в байт-коде - накладных расходов нет. -> -> Сравнить: [Приведение ссылочных типов в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Приведение%20ссылочных%20типов.md) - -> [!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 deleted file mode 100644 index 979505f..0000000 --- a/Kotlin/Синтаксис и типы/Null-безопасность/Платформенные типы.md +++ /dev/null @@ -1,160 +0,0 @@ ---- -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 deleted file mode 100644 index aff5a8b..0000000 --- a/Kotlin/Синтаксис и типы/Null-безопасность/Умные приведения.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -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/Синтаксис и типы/Базовые типы ООП/Any, Unit, Nothing.md b/Kotlin/Синтаксис и типы/Базовые типы ООП/Any, Unit, Nothing.md deleted file mode 100644 index dafa322..0000000 --- a/Kotlin/Синтаксис и типы/Базовые типы ООП/Any, Unit, Nothing.md +++ /dev/null @@ -1,274 +0,0 @@ ---- -title: Any, Unit, Nothing -description: Корень иерархии Any, тип-единица Unit и нижний тип Nothing -tags: - - kotlin - - types ---- - -Три особых типа образуют каркас системы типов Kotlin. - -| Тип | Сколько значений | Роль | -|---|---|---| -| `Any` | все объекты | супертип всего | -| `Unit` | одно (`Unit`) | «результата нет» | -| `Nothing` | ни одного | «возврата не будет» | - -```mermaid -graph TD - A["Any?"] --> B["Any"] - A --> N1["Nothing?"] - B --> C["String"] - B --> D["Int"] - B --> E["Unit"] - C --> N["Nothing"] - D --> N - E --> N - N1 --> N -``` -`Any` сверху, `Nothing` снизу: он подтип любого типа. - -## Any -Супертип всех non-null типов. Объявляет три метода: -```kotlin -fun main() { - val x: Any = 42 - println(x.toString()) // 42 - println(x.hashCode()) // 42 - println(x == 42) // true — вызов equals -} -``` -Больше у `Any` ничего нет: ни `wait`, ни `notify`, ни `getClass`. - -```kotlin -fun main() { - val items: List = listOf(1, "два", 3.0, true) - println(items.joinToString { it::class.simpleName ?: "?" }) - // Int, String, Double, Boolean -} -``` - -`Any` не принимает `null` - для этого есть `Any?`, самый широкий тип в языке: -```kotlin -fun main() { - // val a: Any = null // ошибка компиляции - val b: Any? = null // ok - println(b) // null -} -``` - -Расширения `Any?` можно звать на `null`: -```kotlin -fun main() { - val x: String? = null - println(x.toString()) // "null" — расширение Any?.toString() - println(x.hashCode()) // 0 -} -``` -Это не методы `Any`, а функции-расширения stdlib, объявленные для `Any?`. - -## Unit -Тип функций, которые ничего не возвращают. Это **настоящий объект**, а не отсутствие значения: -```kotlin -fun log(msg: String): Unit = println(msg) - -fun main() { - val result = log("привет") - println(result) // kotlin.Unit - println(result == Unit) // true -} -``` -`Unit` - `object`, у него ровно одно значение. Тип результата `Unit` можно опускать, и обычно так и делают: -```kotlin -fun log(msg: String) = println(msg) // тип выведен: Unit -fun log2(msg: String) { println(msg) } // то же самое -``` - -Поскольку `Unit` - полноценный тип, он подставляется в дженерики, где `void` в Java не работает: -```kotlin -fun main() { - val actions: List<() -> Unit> = listOf({ print("a") }, { print("b") }) - actions.forEach { it() } // ab - println() - - val map = mutableMapOf() - map["ключ"] = Unit // Unit как значение — законно - println(map) // {ключ=kotlin.Unit} -} -``` - -В лямбде с типом результата `Unit` последнее выражение игнорируется - возвращать ничего не нужно: -```kotlin -fun run2(block: () -> Unit) = block() - -fun main() { - run2 { 42 } // 42 отбрасывается, лямбда подходит под () -> Unit - println("ok") -} -``` - -## Nothing -Тип без значений. Функция с результатом `Nothing` **не может завершиться нормально** - только исключением или бесконечным циклом: -```kotlin -fun fail(message: String): Nothing = throw IllegalStateException(message) - -fun main() { - val x: String = if (true) "ok" else fail("нет значения") - println(x) // ok -} -``` - -Зачем это нужно: `Nothing` - подтип любого типа, поэтому выражение с типом `Nothing` подходит в любую позицию. Компилятор знает, что дальше кода не будет. - -```kotlin -fun fail(msg: String): Nothing = throw IllegalArgumentException(msg) - -fun parse(s: String): Int = s.toIntOrNull() ?: fail("не число: $s") - -fun main() { - println(parse("42")) // 42 - // parse("abc") // IllegalArgumentException: не число: abc -} -``` -Если бы `fail` возвращала `Unit`, этот код не скомпилировался бы: `Int` и `Unit` несовместимы. - -`throw` сам по себе - выражение типа `Nothing`: -```kotlin -fun main() { - val x: Int = try { - "abc".toInt() - } catch (e: NumberFormatException) { - throw IllegalStateException("плохие данные", e) // Nothing подходит - } - println(x) -} -``` - -### Анализ недостижимого кода -```kotlin -fun fail(): Nothing = throw RuntimeException() - -fun main() { - val x = 1 - if (x > 0) { - println("да") - } else { - fail() - // println("недостижимо") // предупреждение компилятора - } -} -``` -Компилятор использует `Nothing`, чтобы доказать: ветка не продолжается. Поэтому `TODO()` работает где угодно: -```kotlin -fun calculate(x: Int): String = TODO("ещё не реализовано") - -fun main() { - // calculate(1) // NotImplementedError - println("компилируется") -} -``` -`TODO()` объявлена как `fun TODO(): Nothing`, поэтому подходит под любой тип результата. - -### Nothing? и null -```kotlin -fun main() { - val x = null - println(x) // null - // тип x — Nothing?, единственное значение которого null - val list = listOf(null, null) // List - println(list.size) // 2 -} -``` -Пустые коллекции тоже используют `Nothing`: -```kotlin -fun main() { - val empty: List = emptyList() - val strings: List = empty // подходит — List ковариантен - println(strings.size) // 0 -} -``` -Один объект `EmptyList` подходит под `List` для любого `T` именно потому, что `List` - подтип всех `List`. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `Object` | `Any` (но без `wait`/`notify`/`getClass`) | -> | `void` | `Unit` | -> | `Void` | `Unit` (или `Nothing?`) | -> | нет аналога | `Nothing` | -> | `Object o = null;` | `Any? = null`, `Any` не принимает null | -> -> **`Any` беднее `Object`.** Методы `wait()`, `notify()`, `getClass()` в `Any` не объявлены: первые два относятся к устаревшей модели синхронизации, третий заменён на `::class`. На JVM они доступны после приведения к `Object`, но из Kotlin их намеренно не видно. -> -> **`Unit` против `void`** - не косметика. `void` не тип, поэтому в Java нельзя написать `List` или `Function`; приходится использовать `Void` (у которого единственное значение - `null`) или `Consumer`. В Kotlin `() -> Unit` - обычный тип. -> -> **`Nothing` в Java нет вообще.** Из-за этого Java-компилятор не знает, что метод никогда не вернёт управление: -> ```java -> int parse(String s) { -> if (s == null) fail("null"); // fail объявлен void -> return Integer.parseInt(s); // компилятор требует этот return -> } -> ``` -> В Kotlin `fail(): Nothing` сообщает компилятору, что ветка обрывается, и анализ потока это учитывает. -> -> Сравнить: [Класс Object в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Класс%20Object.md) - -> [!warning] Подводные камни -> **`Any` - не то же самое, что `Any?`.** Функция, принимающая `Any`, не примет `null`: -> ```kotlin -> fun show(x: Any) = x.toString() -> -> fun main() { -> val n: String? = null -> // println(show(n)) // ошибка компиляции -> println(show(n ?: "пусто")) // пусто -> } -> ``` -> -> **Лямбда с результатом `Unit` молча глотает значение:** -> ```kotlin -> fun main() { -> val items = mutableListOf() -> listOf(1, 2).forEach { items.add(it) } // add возвращает Boolean, -> println(items) // но тип () -> Unit его отбрасывает -> } -> ``` -> Это обычно удобно, но если по ошибке вместо `map` написать `forEach`, результат исчезнет без предупреждения. -> -> **`return` с типом `Unit` писать не нужно**, а `return Unit` - признак непонимания: -> ```kotlin -> fun f(): Unit { -> return Unit // компилируется, но так не пишут -> } -> ``` -> -> **Функция с типом `Nothing` не может иметь тело-блок с `return`:** -> ```kotlin -> fun fail(): Nothing { -> // return // ошибка: ничего нельзя вернуть -> throw RuntimeException() // только выход через исключение -> } -> ``` -> -> **`val x = null` имеет тип `Nothing?`, а не `Any?`.** Присвоить такой переменной что-либо другое нельзя: -> ```kotlin -> fun main() { -> var x = null -> // x = "строка" // ошибка: ожидался Nothing? -> val y: String? = null // так -> println("$x $y") -> } -> ``` -> -> **`Nothing` в дженерике означает «значений не будет»:** -> ```kotlin -> fun main() { -> val producer: () -> Nothing = { throw RuntimeException("всегда падает") } -> // producer() — вызов гарантированно бросит исключение -> println("тип ${producer::class.simpleName} объявлен, но вызов невозможен") -> } -> ``` -> Если такой тип получился случайно (например, `emptyList()` без указания параметра), в коллекцию ничего нельзя добавить. - -Дополнительно: [equals, hashCode и toString](equals,%20hashCode%20и%20toString.md) -Далее: [Data class](Data%20class.md) diff --git a/Kotlin/Синтаксис и типы/Базовые типы ООП/Data class.md b/Kotlin/Синтаксис и типы/Базовые типы ООП/Data class.md deleted file mode 100644 index a447a0f..0000000 --- a/Kotlin/Синтаксис и типы/Базовые типы ООП/Data class.md +++ /dev/null @@ -1,276 +0,0 @@ ---- -title: Data class -description: Автоматические equals, hashCode, toString, copy и componentN, их границы и ловушки -tags: - - kotlin - - oop ---- - -`data class` - класс, для которого компилятор генерирует стандартный набор методов. - -```kotlin -data class User(val id: Int, val name: String) - -fun main() { - val u = User(1, "Иван") - println(u) // User(id=1, name=Иван) - println(u == User(1, "Иван")) // true - println(u.copy(name = "Пётр")) // User(id=1, name=Пётр) - val (id, name) = u - println("$id $name") // 1 Иван -} -``` - -Генерируется: - -| Метод | Что делает | -|---|---| -| `equals` | сравнивает все свойства основного конструктора | -| `hashCode` | считает по тем же свойствам | -| `toString` | `Имя(свойство=значение, ...)` | -| `componentN` | для [деструктуризации](../Работа%20с%20данными/Деструктуризация.md) | -| `copy` | копия с заменой части свойств | - -## Требования -```kotlin -data class Point(val x: Int, val y: Int) -``` -- основной конструктор с **хотя бы одним** параметром -- все параметры конструктора - `val` или `var` -- класс не может быть `abstract`, `open`, `sealed` или `inner` - -```kotlin -// data class Empty // ошибка: нет параметров -// data class Bad(x: Int) // ошибка: x не val/var -``` - -## Только свойства конструктора -Это главное, что нужно понимать про `data class`. Свойства, объявленные в теле, **не участвуют** в генерации: -```kotlin -data class User(val id: Int) { - var nickname: String = "" // в теле — игнорируется -} - -fun main() { - val a = User(1).apply { nickname = "иван" } - val b = User(1).apply { nickname = "пётр" } - println(a == b) // true! nickname не сравнивается - println(a) // User(id=1) — nickname не печатается - println(a.copy()) // User(id=1) — nickname потерян, станет "" -} -``` -Если свойство важно для равенства - оно должно быть в конструкторе. - -## copy -Создаёт новый объект, меняя указанные свойства: -```kotlin -data class User(val id: Int, val name: String, val active: Boolean = true) - -fun main() { - val u = User(1, "Иван") - println(u.copy()) // User(id=1, name=Иван, active=true) - println(u.copy(name = "Пётр")) // User(id=1, name=Пётр, active=true) - println(u.copy(id = 2, active = false)) - println(u) // исходный не изменился -} -``` -`copy` - основной способ «изменить» неизменяемый объект. Аргументы почти всегда именованные. - -## Вложенные data class -```kotlin -data class Address(val city: String, val street: String) -data class Person(val name: String, val address: Address) - -fun main() { - val p = Person("Иван", Address("Москва", "Арбат")) - println(p == Person("Иван", Address("Москва", "Арбат"))) // true - - // замена вложенного свойства — copy внутри copy - val moved = p.copy(address = p.address.copy(city = "Казань")) - println(moved) // Person(name=Иван, address=Address(city=Казань, street=Арбат)) -} -``` -Для глубоких структур такие цепочки становятся громоздкими - это известная цена иммутабельности. - -## Переопределение сгенерированного -Любой из методов можно написать руками - свой вариант побеждает: -```kotlin -data class User(val id: Int, val name: String) { - override fun toString() = "#$id $name" -} - -fun main() { - println(User(1, "Иван")) // #1 Иван - println(User(1, "Иван") == User(1, "Иван")) // true — equals сгенерирован -} -``` - -Частый случай - равенство только по идентификатору: -```kotlin -data class User(val id: Int, val name: String) { - override fun equals(other: Any?) = other is User && other.id == id - override fun hashCode() = id -} - -fun main() { - println(User(1, "Иван") == User(1, "Пётр")) // true — сравниваем id -} -``` -Но если равенство нестандартное, `data class` уже почти не нужен - остаются только `copy` и `toString`. - -## В коллекциях -Корректный `equals`/`hashCode` делает `data class` пригодным для `Set` и ключей `Map`: -```kotlin -data class Key(val a: Int, val b: String) - -fun main() { - val map = mapOf(Key(1, "x") to "значение") - println(map[Key(1, "x")]) // значение — новый объект находит запись - println(setOf(Key(1, "x"), Key(1, "x")).size) // 1 — дубликат убран - println(listOf(Key(1, "x")).contains(Key(1, "x"))) // true -} -``` - -## Наследование -`data class` не может быть `open`, но может реализовывать интерфейсы и наследоваться от обычного класса: -```kotlin -interface Named { val name: String } - -data class User(override val name: String, val id: Int) : Named - -fun main() { - val n: Named = User("Иван", 1) - println(n.name) // Иван -} -``` -Часто `data class` реализуют варианты [sealed-иерархии](Sealed%20class%20и%20interface.md): -```kotlin -sealed interface Event -data class Click(val x: Int, val y: Int) : Event -data class KeyPress(val code: Int) : Event -data object Close : Event - -fun handle(e: Event): String = when (e) { - is Click -> "клик ${e.x},${e.y}" - is KeyPress -> "клавиша ${e.code}" - Close -> "закрытие" -} - -fun main() { - println(handle(Click(1, 2))) // клик 1,2 - println(handle(Close)) // закрытие -} -``` - -## data object -Для варианта без данных есть `data object` (Kotlin 1.9+) - он отличается от обычного `object` осмысленным `toString`: -```kotlin -object Plain -data object Loading - -fun main() { - println(Plain) // Plain@1b6d3586 - println(Loading) // Loading -} -``` -Подробнее: [Object и companion object](Object%20и%20companion%20object.md) - -## Когда не нужен -- **Сущность с идентичностью** (entity в БД): равенство по всем полям неверно, нужно по `id` -- **Класс с поведением**: если методов больше, чем свойств, это не носитель данных -- **Публичный API, который будет развиваться**: порядок свойств в конструкторе становится контрактом из-за `componentN` - -> [!info] Отличия от Java -> Ближайший аналог - `record` из Java 16: -> ```java -> record User(int id, String name) {} -> ``` -> -> | | Java record | Kotlin data class | -> |---|---|---| -> | `equals`/`hashCode`/`toString` | да | да | -> | копия с изменением | нет | `copy()` | -> | деструктуризация | только в pattern matching | везде | -> | изменяемые поля | нет, всегда final | `var` разрешён | -> | наследование от класса | нет | можно | -> | значения по умолчанию | нет | есть | -> | своё тело | можно | можно | -> -> Главное практическое различие - `copy()`. В Java для изменения одного поля record приходится вызывать конструктор со всеми аргументами: -> ```java -> var moved = new User(u.id(), "Пётр"); -> ``` -> -> Второе - `record` всегда неизменяем, `data class` допускает `var`. Это гибкость, которой лучше не пользоваться. -> -> До появления `record` в Java такие классы писали руками или генерировали Lombok-ом (`@Data`, `@Value`). Lombok генерировал ещё и `@Builder`, аналога которому в Kotlin нет - его заменяют значения по умолчанию и именованные аргументы. -> -> Сравнить: [Record в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Record.md) - -> [!warning] Подводные камни -> **Массив в `data class` ломает равенство:** -> ```kotlin -> data class Packet(val data: ByteArray) -> -> fun main() { -> println(Packet(byteArrayOf(1)) == Packet(byteArrayOf(1))) // false! -> } -> ``` -> Сгенерированный `equals` вызывает `data.equals(other.data)`, а у массива это сравнение ссылок. Нужно писать руками: -> ```kotlin -> class Packet(val data: ByteArray) { -> override fun equals(other: Any?) = -> other is Packet && data.contentEquals(other.data) -> override fun hashCode() = data.contentHashCode() -> } -> -> fun main() { -> println(Packet(byteArrayOf(1)) == Packet(byteArrayOf(1))) // true -> } -> ``` -> Подробнее: [Массивы](../Работа%20с%20данными/Массивы.md) -> -> **`var` в `data class` + использование как ключа `Map` = потерянная запись:** -> ```kotlin -> data class Key(var id: Int) -> -> fun main() { -> val key = Key(1) -> val map = hashMapOf(key to "значение") -> key.id = 2 // hashCode изменился -> println(map[key]) // null — запись потеряна -> println(map[Key(1)]) // null — старый бакет, другой equals -> println(map.size) // 1 — но она там есть -> } -> ``` -> Это худшая ошибка с `data class`. Поэтому свойства делают `val`. -> -> **`copy` копирует поверхностно.** Вложенный изменяемый объект остаётся общим: -> ```kotlin -> data class Box(val items: MutableList) -> -> fun main() { -> val a = Box(mutableListOf(1)) -> val b = a.copy() -> b.items.add(2) -> println(a.items) // [1, 2] — изменился и оригинал! -> } -> ``` -> -> **Свойства в теле класса не входят ни в `equals`, ни в `copy`, ни в `toString`** - значение теряется при копировании. -> -> **Порядок свойств - часть API** из-за `componentN`. Перестановка свойств одинакового типа не вызовет ошибки компиляции у клиентов, но сломает их деструктуризацию. Подробнее: [Деструктуризация](../Работа%20с%20данными/Деструктуризация.md) -> -> **`copy` до Kotlin 2.0 игнорировал видимость конструктора.** Приватный конструктор не защищал от создания невалидного объекта через `copy`. С Kotlin 2.1 `copy` получает ту же видимость, что конструктор, если включён `-Xconsistent-data-class-copy-visibility`; это станет поведением по умолчанию. -> -> **`toString` печатает всё, включая пароли и токены.** Логирование `data class` с секретами - реальная утечка: -> ```kotlin -> data class Credentials(val login: String, val password: String) { -> override fun toString() = "Credentials(login=$login, password=***)" -> } -> -> fun main() { println(Credentials("ivan", "secret")) } -> ``` - -Дополнительно: [equals, hashCode и toString](equals,%20hashCode%20и%20toString.md) -Далее: [Enum class](Enum%20class.md) diff --git a/Kotlin/Синтаксис и типы/Базовые типы ООП/Enum class.md b/Kotlin/Синтаксис и типы/Базовые типы ООП/Enum class.md deleted file mode 100644 index 1407990..0000000 --- a/Kotlin/Синтаксис и типы/Базовые типы ООП/Enum class.md +++ /dev/null @@ -1,298 +0,0 @@ ---- -title: Enum class -description: Перечисления со свойствами и методами, entries, when по enum и его границы -tags: - - kotlin - - oop ---- - -`enum class` - класс с фиксированным набором экземпляров. - -```kotlin -enum class Status { ACTIVE, BLOCKED, DELETED } - -fun main() { - val s = Status.ACTIVE - println(s) // ACTIVE - println(s.name) // ACTIVE — строка - println(s.ordinal) // 0 — позиция в объявлении -} -``` - -## Свойства и конструктор -Каждая константа - вызов конструктора: -```kotlin -enum class Planet(val radiusKm: Int, val hasRings: Boolean) { - EARTH(6371, false), - SATURN(58232, true), - MARS(3390, false); - - val radiusM: Int get() = radiusKm * 1000 - fun describe() = "$name: радиус $radiusKm км, кольца=$hasRings" -} - -fun main() { - println(Planet.SATURN.describe()) // SATURN: радиус 58232 км, кольца=true - println(Planet.EARTH.radiusM) // 6371000 -} -``` -Точка с запятой после последней константы обязательна, если дальше идёт тело класса. - -## Методы в константах -Константа может переопределить метод - это заменяет `when` по самой себе: -```kotlin -enum class Operation { - PLUS { override fun apply(a: Int, b: Int) = a + b }, - MINUS { override fun apply(a: Int, b: Int) = a - b }, - TIMES { override fun apply(a: Int, b: Int) = a * b }; - - abstract fun apply(a: Int, b: Int): Int -} - -fun main() { - println(Operation.PLUS.apply(2, 3)) // 5 - println(Operation.TIMES.apply(2, 3)) // 6 - println(Operation.entries.joinToString { "${it.name}=${it.apply(6, 3)}" }) - // PLUS=9, MINUS=3, TIMES=18 -} -``` -Каждая такая константа компилируется в анонимный подкласс. - -## Реализация интерфейса -```kotlin -interface Describable { fun describe(): String } - -enum class Level(val weight: Int) : Describable { - LOW(1), HIGH(10); - - override fun describe() = "$name (вес $weight)" -} - -fun main() { - val items: List = Level.entries - println(items.joinToString { it.describe() }) // LOW (вес 1), HIGH (вес 10) -} -``` - -## entries, values, valueOf -```kotlin -enum class Status { ACTIVE, BLOCKED } - -fun main() { - println(Status.entries) // [ACTIVE, BLOCKED] — список - println(Status.entries.size) // 2 - println(Status.valueOf("ACTIVE")) // ACTIVE - println(Status.entries.first().name) // ACTIVE - // println(Status.valueOf("НЕТ")) // IllegalArgumentException -} -``` - -`entries` (Kotlin 1.9+) заменяет `values()`. Разница практическая: - -| | `values()` | `entries` | -|---|---|---| -| тип | `Array` | `EnumEntries` (это `List`) | -| аллокация | **новый массив при каждом вызове** | один неизменяемый список | -| изменяемость | элементы можно подменить | нельзя | - -```kotlin -enum class Status { ACTIVE, BLOCKED } - -fun main() { - @Suppress("DEPRECATION") - println(Status.values().contentToString()) // [ACTIVE, BLOCKED] - println(Status.entries) // [ACTIVE, BLOCKED] -} -``` -В новом коде всегда `entries`. - -Безопасный разбор строки: -```kotlin -enum class Status { ACTIVE, BLOCKED } - -fun parseStatus(s: String): Status? = - Status.entries.find { it.name == s } - -fun main() { - println(parseStatus("ACTIVE")) // ACTIVE - println(parseStatus("нет")) // null — без исключения -} -``` -Через `enumValueOf` то же самое, но с исключением. Для внешних данных (JSON, БД, параметры запроса) нужна версия с `null`. - -## when по enum -Главное применение. Компилятор проверяет полноту, и `else` не нужен: -```kotlin -enum class Status { ACTIVE, BLOCKED, DELETED } - -fun message(s: Status): String = when (s) { - Status.ACTIVE -> "работает" - Status.BLOCKED -> "заблокирован" - Status.DELETED -> "удалён" -} - -fun main() { - println(message(Status.BLOCKED)) // заблокирован -} -``` -Если добавить константу `ARCHIVED`, этот `when` перестанет компилироваться - компилятор найдёт все места, которые нужно обновить. Поэтому `else` в `when` по enum - вредная привычка: он скрывает именно те ошибки, от которых enum защищает. - -Импорт констант сокращает запись: -```kotlin -enum class Status { ACTIVE, BLOCKED } - -fun message(s: Status): String { - return when (s) { - Status.ACTIVE -> "работает" - Status.BLOCKED -> "заблокирован" - } -} - -fun main() = println(message(Status.ACTIVE)) -``` - -## Сравнение и порядок -```kotlin -enum class Level { LOW, MEDIUM, HIGH } - -fun main() { - println(Level.LOW == Level.LOW) // true - println(Level.LOW < Level.HIGH) // true — по ordinal - println(Level.entries.sorted()) // [LOW, MEDIUM, HIGH] - println(Level.HIGH.ordinal) // 2 - println(Level.entries.maxOf { it.ordinal }) // 2 -} -``` -`enum` реализует `Comparable` по `ordinal`. Сравнивать через `==` безопасно: экземпляр единственный, поэтому `==` и `===` дают одно и то же. - -## EnumMap и EnumSet -Специализированные коллекции для enum - массив вместо хеш-таблицы: -```kotlin -import java.util.EnumMap -import java.util.EnumSet - -enum class Day { MON, TUE, WED } - -fun main() { - val hours = EnumMap(Day::class.java) - hours[Day.MON] = 8 - hours[Day.TUE] = 6 - println(hours) // {MON=8, TUE=6} - - val workdays = EnumSet.of(Day.MON, Day.TUE) - println(workdays.contains(Day.WED)) // false - println(EnumSet.complementOf(workdays)) // [WED] -} -``` -Это классы JDK; в stdlib Kotlin аналогов нет. Для небольших enum разница с `HashMap` несущественна. - -## companion object -```kotlin -enum class Status(val code: Int) { - ACTIVE(1), BLOCKED(2); - - companion object { - private val byCode = entries.associateBy { it.code } - fun fromCode(code: Int): Status? = byCode[code] - } -} - -fun main() { - println(Status.fromCode(2)) // BLOCKED - println(Status.fromCode(99)) // null -} -``` -Поиск по коду через заранее построенную `Map` - стандартный приём. Подробнее: [Object и companion object](Object%20и%20companion%20object.md) - -## Когда enum, а когда sealed -```kotlin -enum class Status { OK, ERROR } // варианты без данных - -sealed interface Result2 -data class Ok(val value: Int) : Result2 // варианты с разными данными -data class Error(val message: String) : Result2 -``` -`enum` - фиксированный набор **значений**, у всех одинаковая структура. `sealed` - фиксированный набор **типов**, у каждого свои свойства. Подробнее: [Sealed class и interface](Sealed%20class%20и%20interface.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `enum Status { ACTIVE }` | `enum class Status { ACTIVE }` | -> | `Status.values()` | `Status.entries` | -> | `Status.valueOf(s)` | `Status.valueOf(s)` или `entries.find` | -> | `switch` без проверки полноты до Java 21 | `when` проверяет всегда | -> | `EnumMap`/`EnumSet` | то же (из JDK) | -> -> Устройство одинаковое - на JVM это тот же `java.lang.Enum`. Два практических различия: -> -> **`entries` вместо `values()`.** Java-метод `values()` возвращает новый массив при каждом вызове, потому что массив изменяем и вернуть общий нельзя. `entries` возвращает неизменяемый список, поэтому экземпляр один. В цикле это заметная разница. -> -> **Проверка полноты.** В Java `switch` по enum без `default` долго не проверялся: добавление константы молча менялo поведение. Kotlin проверяет `when`-выражение с момента появления языка, а с Kotlin 2.0 - и `when`-инструкцию. -> -> Сравнить: [Enum в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Enum.md) - -> [!warning] Подводные камни -> **`ordinal` нельзя сохранять в БД или в файл.** Он меняется при вставке константы в середину: -> ```kotlin -> enum class Status { ACTIVE, BLOCKED } // BLOCKED.ordinal == 1 -> // добавили NEW в середину: -> // enum class Status { ACTIVE, NEW, BLOCKED } // BLOCKED.ordinal == 2 -> ``` -> Все сохранённые единицы станут означать `NEW`. Для сериализации используйте `name` или свой стабильный `code`. -> -> **`name` тоже хрупок при переименовании** - но его хотя бы нельзя изменить случайно. Самый надёжный вариант - явное свойство: -> ```kotlin -> enum class Status(val code: String) { -> ACTIVE("active"), BLOCKED("blocked") -> } -> -> fun main() { println(Status.ACTIVE.code) } // active -> ``` -> -> **`valueOf` бросает исключение на незнакомой строке**, а не возвращает `null`. Для внешних данных нужна своя обёртка через `entries.find`. -> -> **`values()` аллоцирует массив на каждый вызов:** -> ```kotlin -> enum class Status { ACTIVE } -> -> fun main() { -> @Suppress("DEPRECATION") -> repeat(3) { Status.values() } // три разных массива -> repeat(3) { Status.entries } // один и тот же список -> println("ok") -> } -> ``` -> -> **`else` в `when` по enum отключает проверку полноты.** Самая частая ошибка: добавили константу, компилятор промолчал, в рантайме сработала ветка `else`. -> -> **Enum нельзя наследовать и расширять.** Если набор вариантов должен быть расширяемым (например, плагинами), enum не подходит - нужен `sealed interface` или обычная иерархия. -> -> **Enum с изменяемым состоянием - глобальная переменная:** -> ```kotlin -> enum class Counter { -> INSTANCE; -> var count = 0 // общее состояние на всю программу -> } -> -> fun main() { -> Counter.INSTANCE.count++ -> println(Counter.INSTANCE.count) // 1 — и так во всех потоках -> } -> ``` -> Константы enum - синглтоны, поэтому `var` в них небезопасен при многопоточности. -> -> **`when` по nullable enum всё равно требует `else` или ветки `null`:** -> ```kotlin -> enum class Status { ACTIVE, BLOCKED } -> -> fun message(s: Status?): String = when (s) { -> Status.ACTIVE -> "работает" -> Status.BLOCKED -> "заблокирован" -> null -> "не задан" -> } -> -> fun main() { println(message(null)) } // не задан -> ``` - -Дополнительно: [Sealed class и interface](Sealed%20class%20и%20interface.md) -Далее: [Sealed class и interface](Sealed%20class%20и%20interface.md) diff --git a/Kotlin/Синтаксис и типы/Базовые типы ООП/Object и companion object.md b/Kotlin/Синтаксис и типы/Базовые типы ООП/Object и companion object.md deleted file mode 100644 index f639034..0000000 --- a/Kotlin/Синтаксис и типы/Базовые типы ООП/Object и companion object.md +++ /dev/null @@ -1,347 +0,0 @@ ---- -title: Object и companion object -description: Синглтон через object, объект-выражение, companion object вместо static и фабрики -tags: - - kotlin - - oop ---- - -Слово `object` в Kotlin означает три разные вещи: объявление синглтона, выражение-анонимный объект и компаньон класса. - -## object - синглтон -```kotlin -object Config { - val appName = "MyApp" - var debug = false - - fun describe() = "$appName debug=$debug" -} - -fun main() { - println(Config.appName) // MyApp - Config.debug = true - println(Config.describe()) // MyApp debug=true -} -``` -Экземпляр создаётся **один** и **лениво** - при первом обращении. Потокобезопасность обеспечивает JVM через механизм инициализации классов. - -```kotlin -object Lazy2 { - init { println("инициализация") } - val value = 42 -} - -fun main() { - println("до обращения") // до обращения - println(Lazy2.value) // инициализация, затем 42 - println(Lazy2.value) // 42 — повторной инициализации нет -} -``` - -Объект может наследоваться и реализовывать интерфейсы: -```kotlin -interface Logger { fun log(msg: String) } - -object ConsoleLogger : Logger { - override fun log(msg: String) = println("[LOG] $msg") -} - -fun main() { - val l: Logger = ConsoleLogger - l.log("привет") // [LOG] привет -} -``` -Это главное отличие от набора статических методов: синглтон - полноценный объект, его можно передать туда, где ожидается интерфейс. - -У `object` не может быть конструктора - передать ему параметры при создании невозможно. - -## data object -Для случаев, где нужен осмысленный `toString` (обычно в [sealed-иерархиях](Sealed%20class%20и%20interface.md)): -```kotlin -object Plain -data object Loading - -fun main() { - println(Plain) // Plain@1b6d3586 - println(Loading) // Loading - println(Loading == Loading) // true - println(Loading.hashCode() == Loading.hashCode()) // true -} -``` -`data object` генерирует `toString`, `equals` и `hashCode`. Для синглтона `equals` и так работал по ссылке - полезен именно `toString`. - -## Объект-выражение -Анонимный объект создаётся на месте: -```kotlin -interface ClickListener { - fun onClick(x: Int, y: Int) -} - -fun main() { - val listener = object : ClickListener { - override fun onClick(x: Int, y: Int) = println("клик $x,$y") - } - listener.onClick(1, 2) // клик 1,2 -} -``` -В отличие от объявления `object`, выражение создаёт **новый** экземпляр при каждом вычислении: -```kotlin -private fun makeCounter() = object { - var count = 0 - fun inc() = ++count -} - -fun main() { - val a = makeCounter() - val b = makeCounter() - a.inc(); a.inc() - println("${a.inc()} ${b.inc()}") // 3 1 — независимые объекты -} -``` - -Можно реализовать несколько интерфейсов сразу и даже ни одного: -```kotlin -fun main() { - val point = object { - val x = 1 - val y = 2 - override fun toString() = "($x, $y)" - } - println(point) // (1, 2) - println(point.x) // 1 -} -``` -Тип такого объекта анонимен, поэтому за пределы локальной области он не выходит: -```kotlin -class Holder { - private fun make() = object { val x = 1 } // private — тип виден - fun use() = make().x - - // fun bad() = object { val x = 1 } // public: тип станет Any, -} // и x будет недоступен - -fun main() { println(Holder().use()) } // 1 -``` - -Объект-выражение видит и может менять локальные переменные: -```kotlin -fun main() { - var clicks = 0 - val listener = object : Runnable { - override fun run() { clicks++ } - } - listener.run(); listener.run() - println(clicks) // 2 -} -``` - -## companion object -Члены, привязанные к классу, а не к экземпляру: -```kotlin -class User private constructor(val name: String) { - companion object { - const val MAX_NAME_LENGTH = 50 - - fun create(name: String): User? = - if (name.length <= MAX_NAME_LENGTH) User(name) else null - } -} - -fun main() { - println(User.MAX_NAME_LENGTH) // 50 - println(User.create("Иван")?.name) // Иван - println(User.create("x".repeat(99))) // null -} -``` -Обращение идёт через имя класса - как к статическим членам. Приватный конструктор + фабрика в компаньоне - стандартный способ контролировать создание объектов. - -Компаньон может быть только один, и у него может быть имя: -```kotlin -class User { - companion object Factory { - fun create() = User() - } -} - -fun main() { - println(User.create()::class.simpleName) // User - println(User.Factory.create()::class.simpleName) // User — через имя -} -``` -Имя нужно редко - в основном для вызова из Java. - -### Компаньон - это объект -Поэтому он может реализовывать интерфейсы: -```kotlin -interface Parser { - fun parse(s: String): T? -} - -class Color2(val rgb: Int) { - companion object : Parser { - override fun parse(s: String): Color2? = - s.removePrefix("#").toIntOrNull(16)?.let { Color2(it) } - } -} - -fun main() { - println(Color2.parse("#ff0000")?.rgb) // 16711680 - val p: Parser = Color2 // компаньон как значение - println(p.parse("#00ff00")?.rgb) // 65280 -} -``` -Статические методы в Java так передать нельзя - это реальное преимущество. - -### Расширения компаньона -```kotlin -class User(val name: String) { - companion object -} - -fun User.Companion.anonymous() = User("анонимный") - -fun main() { - println(User.anonymous().name) // анонимный -} -``` -Пустой `companion object` объявляют именно для этого - чтобы к классу можно было добавить «статическую» фабрику извне. - -## const и @JvmStatic -```kotlin -class Config { - companion object { - const val VERSION = "1.0" // константа компиляции - val runtime = System.nanoTime() // вычисляется при инициализации - - @JvmStatic - fun helper() = "вызывается из Java как Config.helper()" - } -} - -fun main() { - println(Config.VERSION) - println(Config.helper()) -} -``` -`const` работает только для примитивов и `String`, и только на верхнем уровне, в `object` или в `companion object`. Значение подставляется в место использования при компиляции. - -`@JvmStatic` делает член настоящим статическим в байт-коде. Без неё Java-код обращается через `Config.Companion.helper()`. Подробнее: [Kotlin из Java](../../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md) - -## object или функции верхнего уровня -```kotlin -// вариант 1: функции файла -fun formatDate(d: Long) = "дата $d" - -// вариант 2: object -object DateUtils { - fun format(d: Long) = "дата $d" -} -``` -Если состояния нет и интерфейс реализовывать не нужно - функции верхнего уровня проще: не нужно писать префикс, и в байт-коде нет лишнего экземпляра. `object` оправдан, когда есть состояние или нужна реализация интерфейса. - -## Три формы рядом -| Форма | Экземпляров | Когда | -|---|---|---| -| `object Name { }` | один, ленивый | синглтон: конфиг, реестр, реализация интерфейса | -| `object : Type { }` | новый каждый раз | колбэк, одноразовая реализация | -| `companion object` | один на класс | фабрики, константы, «статика» | - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `static` поля и методы | `companion object` или функции файла | -> | `static final` константа | `const val` | -> | паттерн Singleton (enum/holder) | `object` | -> | анонимный класс `new Iface() {}` | `object : Iface {}` | -> | `static` блок инициализации | `init` в `object`/`companion object` | -> | статику нельзя передать как объект | компаньон реализует интерфейсы | -> | лямбда только для SAM | `object` для любого числа методов | -> -> **Ключевое: в Kotlin нет `static`.** Это сделано намеренно - статика не участвует в полиморфизме и не реализует интерфейсы. Вместо неё либо функции верхнего уровня (для чистых утилит), либо `companion object` (когда член логически принадлежит классу). -> -> **Синглтон через `object` решает классическую задачу Java.** Корректный синглтон в Java требует либо `enum` с единственной константой, либо holder-класса с двойной проверкой блокировки: -> ```java -> class Config { -> private Config() {} -> private static class Holder { static final Config INSTANCE = new Config(); } -> static Config getInstance() { return Holder.INSTANCE; } -> } -> ``` -> В Kotlin это `object Config { }` - с той же ленивостью и потокобезопасностью. -> -> **Анонимный класс Java не может реализовать два интерфейса**, объект-выражение Kotlin - может. -> -> Сравнить: [Singleton в Java](../../../Программирование%20на%20Java/Паттерны%20проектирования/Singleton.md), [Вложенные классы в Java](../../../Программирование%20на%20Java/ООП/Вложенные%20классы.md) - -> [!warning] Подводные камни -> **`object` - глобальное изменяемое состояние, если в нём есть `var`:** -> ```kotlin -> object Counter { -> var count = 0 // общий на всю программу, не потокобезопасен -> } -> -> fun main() { -> Counter.count++ -> println(Counter.count) // 1 -> } -> ``` -> Инкремент из нескольких потоков потеряет обновления. Нужен `AtomicInteger` или синхронизация. Подробнее: [Разделяемое состояние](../../Корутины/Разделяемое%20состояние.md) -> -> **Синглтон затрудняет тестирование.** Состояние `object` переживает тесты, и порядок их выполнения начинает влиять на результат. Для подменяемых зависимостей лучше внедрение через конструктор. -> -> **Инициализация `object` ленивая - и порядок может удивить:** -> ```kotlin -> object A { -> val value = B.value + 1 -> } -> object B { -> val value = 10 -> } -> -> fun main() { println(A.value) } // 11 -> ``` -> При круговой зависимости между объектами одно из значений окажется `null` или `0`, без всякой ошибки. -> -> **`const val` нельзя применить к сложному типу:** -> ```kotlin -> object Config { -> const val NAME = "app" // ok -> // const val LIST = listOf(1) // ошибка: только примитивы и String -> val LIST = listOf(1) // так -> } -> -> fun main() { println("${Config.NAME} ${Config.LIST}") } -> ``` -> -> **Компаньон не наследуется.** У подкласса нет доступа к компаньону родителя как к своему: -> ```kotlin -> open class Base { -> companion object { fun create() = "из Base" } -> } -> class Child : Base() -> -> fun main() { -> println(Base.create()) // из Base -> // println(Child.create()) // ошибка: unresolved reference -> } -> ``` -> -> **Тип объекта-выражения анонимен.** Если функция публичная, её тип результата схлопнется до супертипа (или `Any`), и дополнительные члены станут недоступны. -> -> **Из Java члены компаньона видны через `Companion`:** -> ```kotlin -> class Config { -> companion object { -> fun helper() = "без JvmStatic" -> @JvmStatic fun helper2() = "с JvmStatic" -> } -> } -> -> fun main() { println("${Config.helper()} / ${Config.helper2()}") } -> ``` -> Java-код вызывает первую как `Config.Companion.helper()`, вторую - как `Config.helper2()`. -> -> **`object` внутри класса - не то же, что `companion object`.** Вложенный `object` требует обращения через своё имя: `Outer.Inner.member`. - -Дополнительно: [Sealed class и interface](Sealed%20class%20и%20interface.md) -Далее: [Typealias](Typealias.md) diff --git a/Kotlin/Синтаксис и типы/Базовые типы ООП/Sealed class и interface.md b/Kotlin/Синтаксис и типы/Базовые типы ООП/Sealed class и interface.md deleted file mode 100644 index 0c1a756..0000000 --- a/Kotlin/Синтаксис и типы/Базовые типы ООП/Sealed class и interface.md +++ /dev/null @@ -1,294 +0,0 @@ ---- -title: Sealed class и interface -description: Закрытые иерархии, исчерпывающий when, выбор между sealed class и interface -tags: - - kotlin - - oop ---- - -`sealed` означает: все наследники известны компилятору. Новый наследник может появиться только в том же модуле и пакете. - -```kotlin -sealed interface Result2 -data class Success(val data: String) : Result2 -data class Failure(val error: Throwable) : Result2 -data object Loading : Result2 - -fun render(r: Result2): String = when (r) { - is Success -> "данные: ${r.data}" - is Failure -> "ошибка: ${r.error.message}" - Loading -> "загрузка" -} - -fun main() { - println(render(Success("ok"))) // данные: ok - println(render(Failure(RuntimeException("сбой")))) // ошибка: сбой - println(render(Loading)) // загрузка -} -``` -`else` не нужен: компилятор видит все три варианта. Добавление четвёртого сломает компиляцию во всех `when` - это и есть цель. - -## Зачем это нужно -`sealed` - способ выразить «одно из» в системе типов. Без него альтернативой был бы класс со всеми полями сразу и флагом: -```kotlin -// плохо: половина полей всегда null -class BadResult(val data: String?, val error: Throwable?, val loading: Boolean) -``` -Такой класс позволяет создать бессмысленные состояния: данные и ошибка одновременно. `sealed` их запрещает на уровне типов. - -## sealed class или sealed interface -```kotlin -sealed class Shape { - abstract val area: Double // общее состояние и поведение -} -class Circle(val r: Double) : Shape() { - override val area get() = Math.PI * r * r -} -class Square(val side: Double) : Shape() { - override val area get() = side * side -} - -fun main() { - val shapes: List = listOf(Circle(1.0), Square(2.0)) - println(shapes.sumOf { it.area }) // 7.141592653589793 -} -``` - -| | `sealed class` | `sealed interface` | -|---|---|---| -| состояние (свойства с полем) | можно | нет | -| конструктор | есть | нет | -| наследник может входить в другую иерархию | нет (один супертип-класс) | да | -| `data class` как наследник | да | да | - -Правило: берите `sealed interface`, если общего состояния нет. Он гибче - один тип может реализовать несколько sealed-интерфейсов: -```kotlin -sealed interface Error2 -sealed interface Retryable - -data class Timeout(val seconds: Int) : Error2, Retryable -data class NotFound(val id: Int) : Error2 - -fun describe(e: Error2) = when (e) { - is Timeout -> "таймаут, можно повторить: ${e is Retryable}" - is NotFound -> "не найдено ${e.id}" -} - -fun main() { - println(describe(Timeout(30))) // таймаут, можно повторить: true - println(describe(NotFound(1))) // не найдено 1 -} -``` - -## Где можно объявлять наследников -С Kotlin 1.5 наследники должны быть в том же **модуле** и **пакете**, но не обязательно в том же файле: -```kotlin -// файл Result.kt -sealed interface Result2 -// файл Success.kt — тот же пакет, тот же модуль -// data class Success(val data: String) : Result2 -``` -Раньше требовался один файл. Сейчас обычно всё равно держат в одном - иерархию видно целиком. - -Вложенное объявление - распространённый стиль: -```kotlin -sealed interface NetworkResult { - data class Ok(val body: String) : NetworkResult - data class HttpError(val code: Int) : NetworkResult - data object Offline : NetworkResult -} - -fun handle(r: NetworkResult) = when (r) { - is NetworkResult.Ok -> r.body - is NetworkResult.HttpError -> "HTTP ${r.code}" - NetworkResult.Offline -> "нет сети" -} - -fun main() { - println(handle(NetworkResult.HttpError(404))) // HTTP 404 -} -``` -Плюс: имена не загрязняют пакет и читаются как `NetworkResult.Offline`. - -## Исчерпывающий when -Проверка работает и для `when`-инструкции (без результата) - с Kotlin 2.0: -```kotlin -sealed interface Event -data object Start : Event -data object Stop : Event - -fun log(e: Event) { - when (e) { // инструкция, не выражение - Start -> println("старт") - Stop -> println("стоп") - } // else не нужен, полнота проверяется -} - -fun main() { log(Start) } // старт -``` - -Для nullable-типа нужна ветка `null`: -```kotlin -sealed interface Event -data object Start : Event - -fun log(e: Event?) = when (e) { - Start -> "старт" - null -> "ничего" -} - -fun main() { println(log(null)) } // ничего -``` - -## Иерархия глубже одного уровня -Промежуточный узел тоже может быть `sealed`: -```kotlin -sealed interface Expr -data class Num(val value: Int) : Expr -sealed interface BinOp : Expr { - val left: Expr - val right: Expr -} -data class Add(override val left: Expr, override val right: Expr) : BinOp -data class Mul(override val left: Expr, override val right: Expr) : BinOp - -fun eval(e: Expr): Int = when (e) { - is Num -> e.value - is Add -> eval(e.left) + eval(e.right) - is Mul -> eval(e.left) * eval(e.right) -} - -fun main() { - println(eval(Add(Num(2), Mul(Num(3), Num(4))))) // 14 -} -``` -Можно сопоставлять и на промежуточном уровне: -```kotlin -sealed interface Expr -data class Num(val value: Int) : Expr -sealed interface BinOp : Expr -data class Add(val l: Expr, val r: Expr) : BinOp -data class Mul(val l: Expr, val r: Expr) : BinOp - -fun kind(e: Expr) = when (e) { - is Num -> "литерал" - is BinOp -> "операция" // покрывает Add и Mul -} - -fun main() { println(kind(Add(Num(1), Num(2)))) } // операция -``` - -## Перебор наследников -```kotlin -sealed interface Status { - data object Active : Status - data object Blocked : Status -} - -fun main() { - println(Status::class.sealedSubclasses.map { it.simpleName }) - // [Active, Blocked] -} -``` -Работает через рефлексию и требует `kotlin-reflect`. Для фиксированных объектов проще держать список руками. Подробнее: [Рефлексия](../../Рефлексия/Рефлексия.md) - -## Типичные применения -**Результат операции** вместо исключений: -```kotlin -sealed interface Parsed { - data class Ok(val value: T) : Parsed - data class Err(val message: String) : Parsed -} - -fun parse(s: String): Parsed = - s.toIntOrNull()?.let { Parsed.Ok(it) } ?: Parsed.Err("не число: $s") - -fun main() { - println(when (val r = parse("abc")) { - is Parsed.Ok -> "значение ${r.value}" - is Parsed.Err -> r.message - }) // не число: abc -} -``` -Обратите внимание на `Parsed` для варианта без данных - это работает благодаря `out T` и тому, что `Nothing` подтип всего. Подробнее: [Any, Unit, Nothing](Any,%20Unit,%20Nothing.md), [Вариантность](../../Дженерики/Вариантность.md) - -**Состояние UI**, **события**, **команды**, **узлы AST** - везде, где набор вариантов фиксирован и у каждого свои данные. - -> [!info] Отличия от Java -> В Java аналог появился в 17 - `sealed` с явным `permits`: -> ```java -> sealed interface Result permits Success, Failure {} -> record Success(String data) implements Result {} -> record Failure(Throwable error) implements Result {} -> ``` -> -> | Java 17+ | Kotlin | -> |---|---| -> | `sealed interface X permits A, B` | `sealed interface X` (список выводится) | -> | наследники в том же пакете/модуле | то же | -> | `switch` с pattern matching (Java 21) | `when` с `is` | -> | проверка полноты в `switch` (Java 21) | есть всегда | -> | `non-sealed` для открытия ветки | нет аналога | -> -> Kotlin не требует перечислять наследников: компилятор находит их сам в пределах модуля. Это удобнее при добавлении варианта - править объявление не нужно. -> -> До Java 17 то же выражали абстрактным классом с приватным конструктором или паттерном Visitor. Visitor даёт проверку полноты (интерфейс с методом на каждый тип), но требует много кода и меняет все реализации при добавлении варианта. -> -> Сравнить: [Абстрактные классы в Java](../../../Программирование%20на%20Java/ООП/Абстрактные%20классы.md), [Полиморфизм подтипов](../../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм%20подтипов.md) - -> [!warning] Подводные камни -> **`else` в `when` убивает проверку полноты.** Главная ошибка: -> ```kotlin -> sealed interface Event -> data object Start : Event -> data object Stop : Event -> -> fun log(e: Event) = when (e) { -> Start -> "старт" -> else -> "прочее" // добавление варианта пройдёт незамеченным -> } -> -> fun main() { println(log(Stop)) } // прочее -> ``` -> Если ветвей много и большинство обрабатывается одинаково, лучше перечислить их через запятую, чем писать `else`. -> -> **Проверка полноты не работает из Java.** Java-код, читающий sealed-иерархию Kotlin, обязан иметь `default` в `switch`. -> -> **`when` по `is` без исчерпывающего контекста не проверяется.** Полнота требуется только там, где `when` - выражение или где компилятор знает, что иерархия закрыта. Для не-sealed типа проверки нет вообще. -> -> **`sealed class` требует вызова конструктора в наследнике:** -> ```kotlin -> sealed class Base(val id: Int) -> class Child(id: Int) : Base(id) // скобки обязательны -> -> fun main() { println(Child(1).id) } // 1 -> ``` -> У `sealed interface` конструктора нет - на одну причину для ошибки меньше. -> -> **Наследник в другом модуле не скомпилируется**, даже если пакет совпадает. Это ограничивает расширяемость библиотечного API: если вариантов должно быть больше, `sealed` не подходит. -> -> **`sealed` класс сам абстрактен** - создать его нельзя: -> ```kotlin -> sealed class Shape -> // val s = Shape() // ошибка: sealed-класс абстрактный -> class Circle : Shape() -> -> fun main() { println(Circle()::class.simpleName) } // Circle -> ``` -> -> **Для варианта без данных нужен `data object`, а не `class`:** -> ```kotlin -> sealed interface State -> class Loading1 : State // каждый экземпляр разный -> data object Loading2 : State // единственный экземпляр -> -> fun main() { -> println(Loading1() == Loading1()) // false — ловушка -> println(Loading2 == Loading2) // true -> } -> ``` -> -> **Дженерик-наследник sealed-типа требует внимания к вариантности.** Без `out` вариант без данных нельзя объявить как `Err : Parsed`. - -Дополнительно: [Enum class](Enum%20class.md) -Далее: [Value class](Value%20class.md) diff --git a/Kotlin/Синтаксис и типы/Базовые типы ООП/Typealias.md b/Kotlin/Синтаксис и типы/Базовые типы ООП/Typealias.md deleted file mode 100644 index 9a6fe8d..0000000 --- a/Kotlin/Синтаксис и типы/Базовые типы ООП/Typealias.md +++ /dev/null @@ -1,228 +0,0 @@ ---- -title: Typealias -description: Псевдоним типа для сокращения записи, его отличие от value class и границы применения -tags: - - kotlin - - types ---- - -`typealias` даёт типу другое имя. **Нового типа не создаётся** - это сокращение для чтения. - -```kotlin -typealias Name = String - -fun greet(name: Name) = "Привет, $name" - -fun main() { - println(greet("Иван")) // Привет, Иван — обычная строка подходит - val n: Name = "Пётр" - val s: String = n // взаимозаменяемы - println(s) -} -``` - -## Где полезно -**Длинные дженерик-типы:** -```kotlin -typealias UsersByCity = Map>> - -fun process(data: UsersByCity) = data.keys.sorted() - -fun main() { - val data: UsersByCity = mapOf("Москва" to listOf(1 to "Иван")) - println(process(data)) // [Москва] -} -``` - -**Функциональные типы:** -```kotlin -typealias ClickHandler = (x: Int, y: Int) -> Unit -typealias Validator = (T) -> String? - -fun onClick(handler: ClickHandler) = handler(1, 2) - -fun main() { - onClick { x, y -> println("клик $x,$y") } // клик 1,2 - - val notEmpty: Validator = - { if (it.isEmpty()) "пусто" else null } - println(notEmpty("")) // пусто - println(notEmpty("ok")) // null -} -``` -Имена параметров в псевдониме функционального типа - документация: `(x: Int, y: Int) -> Unit` понятнее, чем `(Int, Int) -> Unit`. - -**Разрешение конфликта имён при импорте:** -```kotlin -import java.util.Date -typealias SqlDate = java.sql.Date - -fun main() { - val a: Date = Date() - val b: SqlDate = SqlDate(0) - println("${a::class.simpleName} ${b::class.simpleName}") // Date Date -} -``` -Два класса с одинаковым простым именем нельзя импортировать оба - псевдоним решает это без полных имён в коде. - -**Сокращение вложенных имён:** -```kotlin -class Outer { - class Inner { - class Deep { fun hello() = "привет" } - } -} - -typealias Deep = Outer.Inner.Deep - -fun main() { - println(Deep().hello()) // привет -} -``` - -## Дженерики -Псевдоним может иметь свои параметры типа: -```kotlin -typealias Handler = (T) -> Unit -typealias Dict = Map -typealias IntDict = Dict // частичная подстановка - -fun main() { - val h: Handler = { println(it.uppercase()) } - h("привет") // ПРИВЕТ - - val d: IntDict = mapOf("a" to 1) - println(d["a"]) // 1 -} -``` - -## Только верхний уровень -```kotlin -typealias Ok = String // так - -class Wrong { - // typealias Bad = String // ошибка: псевдоним только на верхнем уровне -} - -fun main() { - val x: Ok = "ok" - println(x) -} -``` -Внутри класса, функции или `object` объявить псевдоним нельзя. - -## typealias и value class -Это часто путают, а задачи у них противоположные: - -```kotlin -typealias UserId = Int -@JvmInline value class AccountId(val value: Int) - -fun loadUser(id: UserId) = "user $id" -fun loadAccount(id: AccountId) = "account ${id.value}" - -fun main() { - println(loadUser(42)) // 42 — любое Int подойдёт - println(loadAccount(AccountId(42))) // нужен именно AccountId - // println(loadAccount(42)) // не скомпилируется -} -``` - -| | `typealias` | `value class` | -|---|---|---| -| новый тип | нет | да | -| ошибку в аргументах ловит | нет | да | -| накладные расходы | нет | обычно нет | -| нужна распаковка (`.value`) | нет | да | -| можно добавить методы | нет | да | -| видно в байт-коде | нет | mangling имён | - -Правило: если нужна **читаемость** - `typealias`. Если нужна **защита от перепутанных аргументов** - `value class`. Подробнее: [Value class](Value%20class.md) - -## Псевдоним не скрывает реализацию -```kotlin -typealias Cache = HashMap - -fun main() { - val c: Cache = HashMap() - println(c is HashMap<*, *>) // true — это и есть HashMap - println(c::class.simpleName) // HashMap -} -``` -`typealias` нельзя использовать как абстракцию: клиент видит настоящий тип, полагается на его методы, и заменить реализацию не получится. Для этого нужен интерфейс. - -## Когда не стоит -```kotlin -typealias S = String // экономия букв без смысла -typealias MyList = List // List и так понятен -``` -Псевдоним добавляет уровень косвенности: читатель должен найти объявление, чтобы понять настоящий тип. Он оправдан, когда сокращает реально длинную запись или даёт имя смысловой роли (`Validator`, `ClickHandler`), а не когда просто короче. - -> [!info] Отличия от Java -> В Java псевдонимов типов нет вообще. Длинные дженерик-типы приходится либо выписывать целиком, либо прятать за интерфейсом или наследованием: -> ```java -> // единственный способ "сократить" — создать настоящий тип -> class UsersByCity extends HashMap>> {} -> ``` -> Наследование от коллекции - плохая практика: тип получает весь API родителя и наружу просачивается реализация. Частичное облегчение даёт `var` (Java 10+), но только для локальных переменных: -> ```java -> var data = new HashMap>(); // тип не пишем дважды -> ``` -> -> | Java | Kotlin | -> |---|---| -> | нет аналога | `typealias X = Y` | -> | `extends HashMap<...>` как сокращение | `typealias` без наследования | -> | `var` для локальных переменных | вывод типов + `typealias` | -> | полное имя при конфликте импортов | `typealias` для одного из классов | -> -> Сравнить: [Параметрический полиморфизм в Java](../../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md) - -> [!warning] Подводные камни -> **Типобезопасности нет.** Это главное заблуждение: -> ```kotlin -> typealias Meters = Int -> typealias Seconds = Int -> -> fun speed(distance: Meters, time: Seconds) = distance / time -> -> fun main() { -> println(speed(100, 10)) // 10 -> val t: Seconds = 10 -> val d: Meters = 100 -> println(speed(t, d)) // 0 — аргументы перепутаны, компилятор молчит -> } -> ``` -> Для настоящей защиты нужен `value class`. -> -> **Псевдоним не наследуется и не расширяется.** Добавить метод «к `Meters`» нельзя - расширение попадёт на `Int`: -> ```kotlin -> typealias Meters = Int -> -> fun Meters.describe() = "$this м" // это расширение Int! -> -> fun main() { -> println(42.describe()) // 42 м — работает для любого Int -> } -> ``` -> -> **В сообщениях компилятора и в стектрейсах видно настоящий тип**, а не псевдоним. При разборе ошибки `expected Map>>` придётся сопоставлять его с `UsersByCity` вручную. -> -> **Из Java псевдоним не виден.** В сигнатурах, доступных Java-коду, стоит раскрытый тип. -> -> **Псевдоним на nullable-тип легко спутать:** -> ```kotlin -> typealias MaybeName = String? -> -> fun greet(n: MaybeName) = "Привет, ${n ?: "гость"}" -> -> fun main() { -> println(greet(null)) // Привет, гость -> } -> ``` -> По имени `MaybeName` не очевидно, что тип nullable. Лучше, когда `?` виден в сигнатуре. -> -> **Нельзя объявить внутри класса или функции** - только на верхнем уровне файла. - -Дополнительно: [Value class](Value%20class.md) -Далее: [equals, hashCode и toString](equals,%20hashCode%20и%20toString.md) diff --git a/Kotlin/Синтаксис и типы/Базовые типы ООП/Value class.md b/Kotlin/Синтаксис и типы/Базовые типы ООП/Value class.md deleted file mode 100644 index e5eeb82..0000000 --- a/Kotlin/Синтаксис и типы/Базовые типы ООП/Value class.md +++ /dev/null @@ -1,265 +0,0 @@ ---- -title: Value class -description: Обёртка без аллокации, типобезопасность примитивов, mangling имён и границы инлайнинга -tags: - - kotlin - - oop - - performance ---- - -`value class` - обёртка над одним значением, которая в байт-коде обычно **исчезает**: вместо объекта остаётся само значение. - -```kotlin -@JvmInline -value class UserId(val value: Int) - -fun load(id: UserId) = "пользователь ${id.value}" - -fun main() { - val id = UserId(42) - println(load(id)) // пользователь 42 - println(id.value) // 42 -} -``` -В скомпилированном коде `load` принимает `int`, а не объект. Аллокации нет. - -Аннотация `@JvmInline` обязательна на JVM - без неё компилятор выдаст ошибку. - -## Зачем -Основная причина - **типобезопасность без накладных расходов**. Классическая проблема: -```kotlin -fun transfer(from: Int, to: Int, amount: Int) = "$from -> $to: $amount" - -fun main() { - println(transfer(100, 1, 2)) // перепутали порядок — компилятор молчит -} -``` -Все параметры `Int`, поэтому их легко перепутать. С `value class` такая ошибка становится ошибкой компиляции: -```kotlin -@JvmInline value class AccountId(val value: Int) -@JvmInline value class Amount(val cents: Int) - -fun transfer(from: AccountId, to: AccountId, amount: Amount) = - "${from.value} -> ${to.value}: ${amount.cents}" - -fun main() { - println(transfer(AccountId(1), AccountId(2), Amount(100))) - // println(transfer(Amount(100), AccountId(1), AccountId(2))) // не скомпилируется -} -``` -Цена - ноль: в байт-коде всё те же три `int`. - -## Что можно внутри -```kotlin -@JvmInline -value class Email(val value: String) { - init { - require(value.contains("@")) { "неверный email: $value" } - } - - val domain: String get() = value.substringAfter("@") - fun masked() = value.replaceRange(0, value.indexOf("@"), "***") - - companion object { - fun parse(s: String): Email? = if (s.contains("@")) Email(s) else null - } -} - -fun main() { - val e = Email("ivan@example.com") - println(e.domain) // example.com - println(e.masked()) // ***@example.com - println(Email.parse("нет")) // null -} -``` -Разрешены: `init`, свойства с геттером, функции, `companion object`, реализация интерфейсов. - -Запрещены: второе свойство с полем, `var`-свойства с полем, наследование, блоки инициализации полей, `lateinit`. - -```kotlin -// @JvmInline value class Bad(val a: Int, val b: Int) // ошибка: только одно поле -// @JvmInline value class Bad2(var a: Int) // ошибка: нужен val -``` - -## Реализация интерфейса -```kotlin -interface Printable { fun print2(): String } - -@JvmInline -value class Code(val value: Int) : Printable { - override fun print2() = "код $value" -} - -fun main() { - println(Code(7).print2()) // код 7 - val p: Printable = Code(7) // упаковка! см. ниже - println(p.print2()) // код 7 -} -``` - -## Когда обёртка всё-таки создаётся -Инлайнинг не бесплатен в смысле предсказуемости: в некоторых позициях объект приходится создать. - -| Позиция | Инлайнится | -|---|---| -| параметр и результат функции | да | -| локальная переменная | да | -| nullable-тип (`UserId?`) | **нет** | -| приведение к супертипу/интерфейсу | **нет** | -| элемент коллекции (`List`) | **нет** - дженерики стираются | -| поле дженерик-класса | **нет** | - -```kotlin -@JvmInline value class UserId(val value: Int) - -fun main() { - val a: UserId = UserId(1) // int, без объекта - val b: UserId? = UserId(1) // объект UserId - val c: List = listOf(UserId(1)) // объекты внутри списка - println("${a.value} ${b?.value} ${c.size}") -} -``` -То есть `List` по памяти не лучше `List`. Выгода - в типобезопасности, а не в аллокациях. - -## Mangling имён -Чтобы разные `value class` над одним типом не давали одинаковые JVM-сигнатуры, компилятор добавляет суффикс к имени метода: -```kotlin -@JvmInline value class AccountId(val value: Int) - -fun process(id: AccountId) = id.value // в байт-коде: process-<хеш>(I)I - -fun main() { println(process(AccountId(1))) } // 1 -``` -Из Java такую функцию вызвать практически нельзя - имя непредсказуемо. Решение - `@JvmName`: -```kotlin -@JvmInline value class AccountId(val value: Int) - -@JvmName("processAccount") -fun process(id: AccountId) = id.value - -fun main() { println(process(AccountId(1))) } // 1 -``` -Подробнее: [Kotlin из Java](../../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md) - -## Равенство -Сравнение идёт по содержимому, причём без упаковки: -```kotlin -@JvmInline value class UserId(val value: Int) - -fun main() { - println(UserId(1) == UserId(1)) // true - println(UserId(1) == UserId(2)) // false - println(setOf(UserId(1), UserId(1)).size) // 1 -} -``` -`equals`/`hashCode` генерируются автоматически и переопределять их нельзя. - -## value class и data class -```kotlin -@JvmInline value class Meters(val value: Double) -data class Point(val x: Int, val y: Int) -``` -`value class` - одно поле, без аллокации, нет `copy`. `data class` - любое число полей, всегда объект, есть `copy` и `componentN`. - -Для одного поля `data class` тоже работает, но создаёт объект: -```kotlin -data class SlowId(val value: Int) // объект на каждый вызов -@JvmInline value class FastId(val value: Int) // int - -fun main() { - println(SlowId(1) == SlowId(1)) // true, но два объекта были созданы - println(FastId(1) == FastId(1)) // true, объектов не было -} -``` - -## typealias - не замена -```kotlin -typealias UserId2 = Int - -fun load(id: UserId2) = id - -fun main() { - println(load(42)) // 42 — любое Int подходит, защиты нет -} -``` -`typealias` - только псевдоним, он не создаёт новый тип и не защищает от путаницы. Подробнее: [Typealias](Typealias.md) - -> [!info] Отличия от Java -> Прямого аналога нет. Java-разработчик выбирает между: -> - **обычным классом-обёрткой** - типобезопасно, но аллокация на каждый объект -> - **примитивом с комментарием** - быстро, но ошибки порядка аргументов не ловятся -> -> ```java -> // обёртка: безопасно, но объект -> final class UserId { -> private final int value; -> UserId(int value) { this.value = value; } -> // equals, hashCode, геттер... -> } -> ``` -> -> В Java это закрывает Project Valhalla (`value class` в JEP 401), но он ещё не вышел в релиз. Пока единственный способ получить безопасность без аллокации - `value class` Kotlin. -> -> | Java | Kotlin | -> |---|---| -> | класс-обёртка + аллокация | `@JvmInline value class` | -> | `int` напрямую | `value class` с тем же байт-кодом | -> | Valhalla (в разработке) | работает с Kotlin 1.5 | -> -> Сравнить: [Классы-обертки в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Классы-обертки.md) - -> [!warning] Подводные камни -> **`@JvmInline` обязательна.** Без неё на JVM компилятор выдаст ошибку, а не предупреждение: -> ```kotlin -> // value class Bad(val x: Int) // error: value class without @JvmInline -> @JvmInline value class Good(val x: Int) -> -> fun main() { println(Good(1).x) } -> ``` -> -> **Nullable-тип отменяет инлайнинг.** `UserId?` - настоящий объект: -> ```kotlin -> @JvmInline value class UserId(val value: Int) -> -> fun find(id: UserId?): String = id?.value?.toString() ?: "нет" -> -> fun main() { println(find(UserId(1))) } // 1 — но обёртка была создана -> ``` -> В горячем коде вместо `UserId?` лучше использовать значение-маркер. -> -> **В коллекциях выгоды нет.** `List` хранит объекты из-за стирания типов. -> -> **`init` вызывается, но не защищает от Java.** Java-код может создать экземпляр в обход проверки, потому что на уровне байт-кода это просто `int`: -> ```kotlin -> @JvmInline -> value class Age(val value: Int) { -> init { require(value >= 0) { "возраст отрицательный" } } -> } -> -> fun main() { -> println(Age(5).value) // 5 -> // Age(-1) // IllegalArgumentException из Kotlin -> } -> ``` -> -> **Mangling ломает вызов из Java и из фреймворков, работающих через рефлексию.** Jackson, Spring, JPA требуют отдельной настройки для `value class`; иногда он просто не поддерживается. -> -> **Переопределить `equals`, `hashCode` нельзя** - они генерируются. `toString` переопределить можно. -> -> **Два `value class` над одним типом взаимозаменяемы в байт-коде.** Приведение через `Any` обойдёт проверку типов: -> ```kotlin -> @JvmInline value class Meters(val v: Int) -> @JvmInline value class Feet(val v: Int) -> -> fun main() { -> val m = Meters(10) -> val any: Any = m -> println((any as Meters).v) // 10 -> // println((any as Feet).v) // ClassCastException — на уровне Kotlin проверка есть -> } -> ``` -> -> **Нельзя наследоваться и нельзя быть `open`** - иерархии из `value class` не построить. - -Дополнительно: [Typealias](Typealias.md) -Далее: [Object и companion object](Object%20и%20companion%20object.md) diff --git a/Kotlin/Синтаксис и типы/Базовые типы ООП/equals, hashCode и toString.md b/Kotlin/Синтаксис и типы/Базовые типы ООП/equals, hashCode и toString.md deleted file mode 100644 index a5c5a65..0000000 --- a/Kotlin/Синтаксис и типы/Базовые типы ООП/equals, hashCode и toString.md +++ /dev/null @@ -1,320 +0,0 @@ ---- -title: equals, hashCode и toString -description: Контракт равенства, == против ===, правила hashCode и как не сломать HashMap -tags: - - kotlin - - oop ---- - -Три метода объявлены в [Any](Any,%20Unit,%20Nothing.md) и есть у каждого объекта. Их реализация по умолчанию - по ссылке и по адресу в памяти. - -```kotlin -class Point(val x: Int, val y: Int) - -fun main() { - val a = Point(1, 2) - val b = Point(1, 2) - println(a == b) // false — equals по ссылке - println(a) // Point@1b6d3586 — toString по хешу -} -``` - -## == и === -```kotlin -fun main() { - val a = "Kotlin" - val b = StringBuilder("Kotlin").toString() - println(a == b) // true — структурное равенство, вызывает equals - println(a === b) // false — ссылочное равенство, тот же объект? - println(a !== b) // true -} -``` - -| Оператор | Что делает | -|---|---| -| `==` | `a?.equals(b) ?: (b === null)` | -| `===` | сравнение ссылок (для примитивов - значений) | - -`==` безопасен для `null`: левый операнд может быть `null`, NPE не будет. -```kotlin -fun main() { - val a: String? = null - println(a == "x") // false — без исключения - println(a == null) // true - println(null == null) // true -} -``` - -## Контракт equals -Реализация должна быть: -- **рефлексивной**: `a == a` всегда `true` -- **симметричной**: `a == b` тогда и только тогда, когда `b == a` -- **транзитивной**: `a == b` и `b == c` влечёт `a == c` -- **согласованной**: результат не меняется, пока объекты не изменились -- `a == null` всегда `false` - -```kotlin -class Point(val x: Int, val y: Int) { - override fun equals(other: Any?): Boolean { - if (this === other) return true // быстрый путь - if (other !is Point) return false // проверка типа и null сразу - return x == other.x && y == other.y - } - - override fun hashCode(): Int = 31 * x + y - - override fun toString() = "Point($x, $y)" -} - -fun main() { - val a = Point(1, 2) - println(a == Point(1, 2)) // true - println(a == null) // false - println(a) // Point(1, 2) -} -``` -`other !is Point` закрывает и `null`, и неверный тип: `is` для `null` даёт `false`. - -Параметр обязан иметь тип `Any?` - иначе это не переопределение, а перегрузка: -```kotlin -class Bad(val x: Int) { - fun equals(other: Bad) = x == other.x // перегрузка! override не стоит -} - -fun main() { - val a: Any = Bad(1) - println(a == Bad(1)) // false — вызвался equals из Any -} -``` -Компилятор не позволит поставить `override` на такую функцию - это и есть подсказка. - -## Контракт hashCode -Единственное жёсткое правило: **равные объекты обязаны иметь равный хеш**. - -```kotlin -class Broken(val x: Int) { - override fun equals(other: Any?) = other is Broken && other.x == x - // hashCode не переопределён — контракт нарушен -} - -fun main() { - val a = Broken(1) - val set = hashSetOf(a) - println(set.contains(Broken(1))) // false — объект "потерян" - println(a == Broken(1)) // true — хотя equals говорит "равны" -} -``` -`HashSet` сначала ищет бакет по хешу. Хеши разные - поиск даже не доходит до `equals`. - -Обратное неверно: разные объекты **могут** иметь одинаковый хеш (коллизия) - это нормально и лишь замедляет поиск. - -```kotlin -class Pair2(val a: Int, val b: Int) { - override fun equals(other: Any?) = other is Pair2 && other.a == a && other.b == b - override fun hashCode() = 31 * a + b -} - -fun main() { - println(Pair2(1, 32).hashCode()) // 63 - println(Pair2(2, 1).hashCode()) // 63 — коллизия, но всё корректно - println(Pair2(1, 32) == Pair2(2, 1)) // false -} -``` - -Стандартный способ считать хеш по нескольким полям: -```kotlin -class User(val id: Int, val name: String, val tags: List) { - override fun equals(other: Any?) = - other is User && other.id == id && other.name == name && other.tags == tags - - override fun hashCode(): Int { - var result = id - result = 31 * result + name.hashCode() - result = 31 * result + tags.hashCode() - return result - } -} - -fun main() { - val a = User(1, "Иван", listOf("admin")) - println(a == User(1, "Иван", listOf("admin"))) // true - println(a.hashCode() == User(1, "Иван", listOf("admin")).hashCode()) // true -} -``` -Множитель 31 - соглашение из JDK: простое число, а умножение на него оптимизируется в сдвиг и вычитание. - -Короче - через `Objects.hash`, но он создаёт массив: -```kotlin -import java.util.Objects - -class User(val id: Int, val name: String) { - override fun equals(other: Any?) = other is User && other.id == id && other.name == name - override fun hashCode() = Objects.hash(id, name) -} - -fun main() { println(User(1, "a") == User(1, "a")) } // true -``` - -## data class делает это за вас -```kotlin -data class Point(val x: Int, val y: Int) - -fun main() { - val a = Point(1, 2) - println(a == Point(1, 2)) // true - println(a) // Point(x=1, y=2) - println(hashSetOf(a).contains(Point(1, 2))) // true -} -``` -В большинстве случаев ручная реализация не нужна - достаточно `data class`. Писать руками приходится, когда: равенство только по части полей, в классе есть массив, или класс - сущность с идентификатором. Подробнее: [Data class](Data%20class.md) - -## toString -Единственный из трёх, у которого нет контракта - но есть ожидания. Он попадает в логи и в сообщения об ошибках: -```kotlin -class Order(val id: Int, val items: List) { - override fun toString() = "Order(id=$id, items=${items.size})" -} - -fun main() { - println(Order(1, listOf("a", "b"))) // Order(id=1, items=2) - println("заказ: ${Order(1, emptyList())}") // заказ: Order(id=1, items=0) - println(listOf(Order(1, emptyList()))) // [Order(id=1, items=0)] -} -``` -Строковый шаблон, конкатенация и печать коллекции - все вызывают `toString`. Поэтому класс без нормального `toString` превращает лог в набор адресов. - -## Равенство и наследование -Строгая симметричность при наследовании достигается проверкой точного класса: -```kotlin -open class Base(val x: Int) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - return x == (other as Base).x - } - override fun hashCode() = x -} - -class Derived(x: Int, val y: Int) : Base(x) { - override fun equals(other: Any?) = - super.equals(other) && y == (other as Derived).y - override fun hashCode() = 31 * super.hashCode() + y -} - -fun main() { - println(Base(1) == Derived(1, 2)) // false — разные классы - println(Derived(1, 2) == Derived(1, 2)) // true -} -``` -С `other is Base` вместо сравнения классов получилась бы асимметрия: `base == derived` истина, `derived == base` ложь. Это нарушение контракта, которое ломает коллекции. - -Проще всего не наследоваться там, где важно равенство - именно поэтому `data class` нельзя сделать `open`. - -## Равенство примитивов и упаковка -```kotlin -fun main() { - val a: Int = 1000 - val b: Int = 1000 - println(a == b) // true - println(a === b) // true — примитивы, сравнение значений - - val c: Int? = 1000 - val d: Int? = 1000 - println(c == d) // true - println(c === d) // false! разные объекты Integer -} -``` -Nullable-число упаковывается в `Integer`, и `===` сравнивает ссылки. Для значений от -128 до 127 работает кеш JVM, и `===` даёт `true` - то есть результат зависит от величины числа. Для чисел используйте только `==`. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `a.equals(b)` | `a == b` | -> | `a == b` (ссылки) | `a === b` | -> | `Objects.equals(a, b)` для null | `a == b` | -> | `@Override boolean equals(Object o)` | `override fun equals(other: Any?)` | -> | `o instanceof Point p` (Java 16+) | `other is Point` + умное приведение | -> | `Objects.hash(a, b)` | то же, или вручную | -> | генерация в IDE или Lombok | `data class` | -> | `String.valueOf(obj)` для null | `obj.toString()` работает на `null?` | -> -> **Главное: `==` в Kotlin вызывает `equals`.** Это убирает самый частый класс ошибок Java - сравнение строк через `==`, которое иногда «работает» из-за пула строк и ломается на строках, собранных в рантайме. -> -> **`==` не бросает NPE**, в отличие от `a.equals(b)` в Java. Обёртка `Objects.equals` больше не нужна. -> -> **`toString()` можно звать на `null`:** -> ```kotlin -> fun main() { -> val s: String? = null -> println(s.toString()) // "null" — расширение Any?.toString() -> } -> ``` -> В Java `null.toString()` - это NPE, нужен `String.valueOf`. -> -> **`equals` в Kotlin не перегрузить случайно.** Параметр с типом `Any?` обязателен для `override`, поэтому ошибка «написал `equals(Point)` вместо `equals(Object)`» ловится компилятором. -> -> Сравнить: [Контракт между equals и hashCode](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Контракт%20между%20equals%20и%20hashCode.md), [HashMap в Java](../../../Программирование%20на%20Java/Коллекции/HashMap.md) - -> [!warning] Подводные камни -> **Переопределить `equals` без `hashCode`** - значит получить объект, который теряется в `HashSet` и `HashMap`. IDE предупреждает, компилятор - нет. -> -> **Изменяемый объект как ключ `HashMap`:** -> ```kotlin -> data class Key(var id: Int) -> -> fun main() { -> val key = Key(1) -> val map = hashMapOf(key to "значение") -> key.id = 2 -> println(map[key]) // null — ищем в новом бакете -> println(map.keys.first()) // Key(id=2) — запись на месте, но недоступна -> println(map.size) // 1 -> } -> ``` -> Запись становится невидимой и неудаляемой. Ключи обязаны быть неизменяемыми. -> -> **Массив внутри класса ломает `equals` по содержимому:** -> ```kotlin -> class Packet(val data: ByteArray) { -> override fun equals(other: Any?) = -> other is Packet && data.contentEquals(other.data) -> override fun hashCode() = data.contentHashCode() -> } -> -> fun main() { -> println(Packet(byteArrayOf(1)) == Packet(byteArrayOf(1))) // true -> } -> ``` -> Без `contentEquals`/`contentHashCode` сравнение идёт по ссылке. Подробнее: [Массивы](../Работа%20с%20данными/Массивы.md) -> -> **`===` для чисел зависит от значения:** -> ```kotlin -> fun main() { -> val a: Int? = 127 -> val b: Int? = 127 -> println(a === b) // true — из кеша Integer -> val c: Int? = 128 -> val d: Int? = 128 -> println(c === d) // false — за пределами кеша -> } -> ``` -> Это не особенность Kotlin, а кеш `Integer.valueOf` в JVM. Вывод: `===` на числах не использовать. -> -> **`equals` с `is` вместо проверки класса даёт асимметрию при наследовании** - и вместе с ней непредсказуемое поведение коллекций. -> -> **`hashCode`, зависящий от изменяемого поля, нарушает согласованность.** Объект должен давать один и тот же хеш всё время, пока лежит в хеш-коллекции. -> -> **`toString` печатает всё, включая секреты:** -> ```kotlin -> data class Token(val value: String) { -> override fun toString() = "Token(***)" -> } -> -> fun main() { println(Token("secret-abc")) } // Token(***) -> ``` -> `data class` с паролем или токеном попадёт в лог целиком, если не переопределить `toString`. -> -> **`hashCode` не обязан быть стабильным между запусками JVM.** `Object.hashCode` зависит от адреса, `String.hashCode` стабилен по спецификации, но для своего класса гарантий нет - сохранять хеш в БД нельзя. - -Дополнительно: [Data class](Data%20class.md) -Далее: [ООП](../../ООП/ООП.md) diff --git a/Kotlin/Синтаксис и типы/Базовый синтаксис/Hello World!.md b/Kotlin/Синтаксис и типы/Базовый синтаксис/Hello World!.md deleted file mode 100644 index 8c82e26..0000000 --- a/Kotlin/Синтаксис и типы/Базовый синтаксис/Hello World!.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -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 deleted file mode 100644 index 24a1267..0000000 --- a/Kotlin/Синтаксис и типы/Базовый синтаксис/Операторы и выражения.md +++ /dev/null @@ -1,182 +0,0 @@ ---- -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 deleted file mode 100644 index 7a1ffa5..0000000 --- a/Kotlin/Синтаксис и типы/Базовый синтаксис/Пакеты и импорты.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -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`. -> -> Сравнить: [Модификаторы в Java](../../../Программирование%20на%20Java/ООП/Инкапсуляция/Модификаторы.md) - -> [!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 deleted file mode 100644 index b559ff6..0000000 --- a/Kotlin/Синтаксис и типы/Базовый синтаксис/Переменные.md +++ /dev/null @@ -1,154 +0,0 @@ ---- -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 deleted file mode 100644 index 9af13cb..0000000 --- a/Kotlin/Синтаксис и типы/Базовый синтаксис/Строковые шаблоны и вывод.md +++ /dev/null @@ -1,191 +0,0 @@ ---- -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 deleted file mode 100644 index 7a082af..0000000 --- a/Kotlin/Синтаксис и типы/Базовый синтаксис/Типы данных.md +++ /dev/null @@ -1,154 +0,0 @@ ---- -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 deleted file mode 100644 index 46e3a9a..0000000 --- a/Kotlin/Синтаксис и типы/Базовый синтаксис/Условные конструкции.md +++ /dev/null @@ -1,175 +0,0 @@ ---- -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 deleted file mode 100644 index 24e41a9..0000000 --- a/Kotlin/Синтаксис и типы/Базовый синтаксис/Циклы и диапазоны.md +++ /dev/null @@ -1,159 +0,0 @@ ---- -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) diff --git a/Kotlin/Синтаксис и типы/Работа с данными/Char.md b/Kotlin/Синтаксис и типы/Работа с данными/Char.md deleted file mode 100644 index b1602c5..0000000 --- a/Kotlin/Синтаксис и типы/Работа с данными/Char.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: Char -description: Char как отдельный тип, код символа, проверки категорий и работа с цифрами -tags: - - kotlin - - stdlib ---- - -`Char` - один символ в одинарных кавычках. Хранит кодовую единицу UTF-16 (16 бит). - -```kotlin -val c: Char = 'A' -val tab = '\t' -val quote = '\'' -val unicode = '\u00A9' // © -``` - -Ключевое отличие от Java: **`Char` не является числом**. Арифметику с ним делать можно, но неявного превращения в `Int` нет. - -```kotlin -val c = 'A' -// val n: Int = c // ошибка компиляции -val n: Int = c.code // 65 — явно -println(c + 1) // B — Char + Int = Char -println('z' - 'a') // 25 — Char - Char = Int -println(c.toString()) // A -``` - -| Выражение | Результат | Тип | -|---|---|---| -| `'A' + 1` | `'B'` | `Char` | -| `'z' - 'a'` | `25` | `Int` | -| `'A'.code` | `65` | `Int` | -| `65.toChar()` | `'A'` | `Char` | - -## Проверки категорий -```kotlin -println('a'.isLetter()) // true -println('5'.isDigit()) // true -println('a'.isLetterOrDigit()) // true -println(' '.isWhitespace()) // true -println('a'.isLowerCase()) // true -println('A'.isUpperCase()) // true -``` -Все эти функции учитывают Unicode: `'я'.isLetter()` тоже `true`. - -## Регистр -```kotlin -println('a'.uppercaseChar()) // A — возвращает Char -println('A'.lowercaseChar()) // a -println('a'.uppercase()) // "A" — возвращает String -println('ß'.uppercase()) // "SS" — два символа, поэтому String -``` -Есть две версии: `uppercaseChar()` даёт `Char`, `uppercase()` - `String`. Вторая нужна, потому что в Unicode регистр не всегда меняется один-к-одному. - -## Цифры -```kotlin -println('7'.digitToInt()) // 7 — число, а не код символа -println('f'.digitToInt(16)) // 15 -println('x'.digitToIntOrNull()) // null — безопасная версия -println(7.digitToChar()) // '7' -``` -Это главный способ разобрать цифру. Старый трюк из Java (`c - '0'`) тоже работает, но `digitToInt()` понятнее и проверяет корректность: -```kotlin -println('7' - '0') // 7 — работает -// println('x'.digitToInt()) // IllegalArgumentException -``` - -## Диапазоны и сравнение -```kotlin -println('a' in 'a'..'z') // true -println('5' in '0'..'9') // true -for (c in 'a'..'e') print(c) // abcde -println('a' < 'b') // true — по коду -println(('a'..'e').toList()) // [a, b, c, d, e] -``` - -## Строки и Char -```kotlin -val s = "Kotlin" -println(s[0]) // K — элемент строки это Char -println(s.first()) // K -println(s.toCharArray().size) // 6 -println(String(charArrayOf('a','b'))) // ab — собрать строку из массива -println(s.toList()) // [K, o, t, l, i, n] -println("abc".map { it + 1 }) // [b, c, d] -println(charArrayOf('a','b').concatToString()) // ab -``` - -Приписать `Char` к строке можно сложением - слева обязательно строка: -```kotlin -println("ab" + 'c') // abc -// println('c' + "ab") // ошибка: Char.plus ожидает Int -println("${'c'}ab") // cab — через шаблон -``` - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `char` - числовой тип | `Char` - отдельный тип | -> | `int n = c;` работает | нужен `c.code` | -> | `Character.isDigit(c)` | `c.isDigit()` | -> | `Character.toUpperCase(c)` | `c.uppercaseChar()` | -> | `Character.getNumericValue(c)` | `c.digitToInt()` | -> | `c - '0'` для цифры | `c.digitToInt()` | -> | `switch (c)` по числам | `when (c)` по символам | -> -> В Java `char` участвует в арифметике как число, из-за чего `System.out.println('a' + 1)` печатает `98`, а не `b`. В Kotlin тот же код даёт `b` - тип сохраняется. -> -> Все утилиты из `Character` стали методами `Char`, поэтому пишутся через точку и находятся автодополнением. -> -> Сравнить: [Character в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Character.md) - -> [!warning] Подводные камни -> **`Char` не вмещает символы за пределами BMP.** Эмодзи и редкие иероглифы кодируются двумя `Char` (surrogate pair): -> ```kotlin -> val emoji = "😀" -> println(emoji.length) // 2 -> println(emoji[0].isLetter()) // false — половина пары бессмысленна -> println(emoji.codePointAt(0)) // 128512 — полный код символа -> ``` -> Для посимвольной обработки текста с эмодзи нужны кодовые точки, а не `Char`. -> -> **`digitToInt()` бросает исключение, а не возвращает -1:** -> ```kotlin -> println('x'.digitToIntOrNull()) // null — безопасно -> // println('x'.digitToInt()) // IllegalArgumentException -> ``` -> -> **`'1'.code` - это 49, а не 1.** Путать код символа с его числовым значением - классическая ошибка: -> ```kotlin -> println('1'.code) // 49 -> println('1'.digitToInt()) // 1 -> ``` -> -> **`Char` в `when` требует `else`** - перечислить все 65 536 значений компилятор не может. -> -> **`uppercase()` возвращает `String`, и это не придирка.** `'ß'.uppercase()` даёт `"SS"` - два символа. Если нужен именно `Char`, используйте `uppercaseChar()`, но помните, что для `'ß'` он вернёт сам `'ß'`. - -Далее: [Числа и переполнение](Числа%20и%20переполнение.md) diff --git a/Kotlin/Синтаксис и типы/Работа с данными/Деструктуризация.md b/Kotlin/Синтаксис и типы/Работа с данными/Деструктуризация.md deleted file mode 100644 index c87e99a..0000000 --- a/Kotlin/Синтаксис и типы/Работа с данными/Деструктуризация.md +++ /dev/null @@ -1,218 +0,0 @@ ---- -title: Деструктуризация -description: Разбор объекта на переменные, componentN, деструктуризация в лямбдах и циклах -tags: - - kotlin - - syntax ---- - -Деструктуризация - разбор объекта на отдельные переменные одной конструкцией. - -```kotlin -data class Point(val x: Int, val y: Int) - -fun main() { - val (x, y) = Point(1, 2) - println("$x $y") // 1 2 -} -``` - -Компилятор превращает это в вызовы `component1()`, `component2()` и так далее: -```kotlin -data class Point(val x: Int, val y: Int) - -fun main() { - val p = Point(1, 2) - val x = p.component1() - val y = p.component2() - println("$x $y") // 1 2 -} -``` - -## Что можно деструктурировать -Любой тип, у которого есть функции `componentN()`. Автоматически они появляются у: -- [data class](../Базовые%20типы%20ООП/Data%20class.md) - по свойствам основного конструктора -- `Pair` и `Triple` -- `Map.Entry` - ключ и значение -- списков и массивов (первые 5 элементов) - -```kotlin -fun main() { - val (a, b) = Pair(1, "x") - val (c, d, e) = Triple(1, 2, 3) - val (first, second) = listOf("a", "b", "c") // третий игнорируется - println("$a $b | $c $d $e | $first $second") -} -``` - -## Свой componentN -Для обычного класса операторы объявляются вручную: -```kotlin -class Range2(val from: Int, val to: Int) { - operator fun component1() = from - operator fun component2() = to -} - -fun main() { - val (from, to) = Range2(1, 10) - println("$from..$to") // 1..10 -} -``` -Слово `operator` обязательно. - -## Пропуск компонентов -```kotlin -data class User(val id: Int, val name: String, val email: String) - -fun main() { - val (_, name, _) = User(1, "Иван", "i@x.ru") - println(name) // Иван -} -``` -Подчёркивание означает «этот компонент не нужен» - `componentN()` для него даже не вызовется. - -Если нужны только первые компоненты, лишние просто не перечисляют: -```kotlin -data class User(val id: Int, val name: String, val email: String) - -fun main() { - val (id, name) = User(1, "Иван", "i@x.ru") // email не берём - println("$id $name") // 1 Иван -} -``` - -## В циклах -```kotlin -fun main() { - val map = mapOf("a" to 1, "b" to 2) - for ((key, value) in map) { - println("$key=$value") // a=1, затем b=2 - } - - val list = listOf("x", "y") - for ((index, item) in list.withIndex()) { - println("$index:$item") // 0:x, затем 1:y - } -} -``` -Это самое частое применение деструктуризации. - -## В лямбдах -```kotlin -fun main() { - val map = mapOf("a" to 1, "b" to 2) - map.forEach { (k, v) -> println("$k$v") } // a1 b2 - println(map.map { (k, v) -> "$k=$v" }) // [a=1, b=2] - map.forEach { (_, v) -> print(v) } // 12 - println() - - val points = listOf(1 to 2, 3 to 4) - println(points.map { (x, y) -> x + y }) // [3, 7] -} -``` -Параметр в скобках `{ (k, v) -> }` - это **один** параметр, который разбирается, а не два отдельных. Без скобок `{ k, v -> }` лямбда ожидала бы два аргумента, и код не скомпилировался бы. - -## Возврат нескольких значений -Классическое применение - функция, которая должна вернуть пару значений: -```kotlin -data class Result2(val value: Int, val error: String?) - -fun parse(s: String): Result2 = - s.toIntOrNull()?.let { Result2(it, null) } ?: Result2(0, "не число") - -fun main() { - val (value, error) = parse("42") - println("$value $error") // 42 null - - val (v2, e2) = parse("abc") - println("$v2 $e2") // 0 не число -} -``` -`Pair` для этого тоже подходит, но свой `data class` лучше: имена компонентов осмысленны, а не `first`/`second`. - -## Деструктуризация по именам (Kotlin 2.3+) -Классическая деструктуризация позиционная - порядок переменных должен совпадать с порядком свойств. Это хрупко: перестановка свойств в `data class` молча меняет смысл кода. - -С Kotlin 2.3 разрабатывается деструктуризация по именам. Синтаксис меняется так: -- `val [a, b] = obj` - **квадратные** скобки, позиционно (как было) -- `val (name, email) = obj` - **круглые** скобки, теперь **по именам** свойств - -```kotlin -data class User(val name: String, val email: String) - -fun main() { - val user = User("ИМЯ", "ПОЧТА") - - val (email, name) = user // по именам — порядок не важен - println("name=$name email=$email") // name=ИМЯ email=ПОЧТА - - val [first, second] = user // позиционно - println("first=$first") // first=ИМЯ -} -``` - -Фича пока за флагом компилятора: -```bash -kotlinc -Xname-based-destructuring=complete App.kt -``` -Значения флага: `only-syntax` (включить синтаксис `[]` и полную форму), `name-mismatch` (предупреждать, когда имена в позиционной деструктуризации не совпадают со свойствами), `complete` (включить короткую форму по именам). - -> [!warning] Это ломающее изменение -> Круглые скобки меняют смысл: был порядок, станут имена. Код `val (email, name) = user` сегодня присвоит `email` первое свойство, а после включения фичи - свойство с именем `email`. Пока флаг не включён, вся деструктуризация в вашем коде позиционная. - -> [!info] Отличия от Java -> В Java деструктуризации объектов нет. Ближайший аналог - record patterns из Java 21: -> ```java -> record Point(int x, int y) {} -> if (obj instanceof Point(int x, int y)) { ... } -> ``` -> Но это работает только в `instanceof` и `switch`, а не при обычном объявлении переменных. Разбор `Map.Entry` в цикле в Java невозможен - нужно писать `e.getKey()` и `e.getValue()`. -> -> | Java | Kotlin | -> |---|---| -> | `var k = e.getKey(); var v = e.getValue();` | `val (k, v) = e` | -> | `for (var e : map.entrySet())` + геттеры | `for ((k, v) in map)` | -> | `p.x(); p.y();` | `val (x, y) = p` | -> | нет возврата нескольких значений | `Pair`, `Triple`, `data class` | -> -> Сравнить: [Record в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Record.md) - -> [!warning] Подводные камни -> **Деструктуризация позиционная, а не по именам.** Это главная опасность: -> ```kotlin -> data class User(val name: String, val email: String) -> -> fun main() { -> val (name, email) = User("Иван", "i@x.ru") -> println("$name / $email") // Иван / i@x.ru -> } -> ``` -> Если позже в `data class` поменять порядок свойств на `(email, name)`, этот код **продолжит компилироваться**, но переменные получат чужие значения. Компилятор не поможет, потому что типы совпадают. Поэтому порядок свойств в `data class` - часть публичного API. -> -> **У обычного класса деструктуризации нет** - только у `data class` или при ручном объявлении `componentN()`. -> -> **В лямбде нужны скобки:** -> ```kotlin -> fun main() { -> val map = mapOf("a" to 1) -> map.forEach { (k, v) -> println("$k$v") } // верно: один параметр-пара -> map.forEach { e -> println("${e.key}${e.value}") } // тоже верно -> } -> ``` -> Запись `{ k, v -> }` для `forEach` на `Map` не скомпилируется. -> -> **Список короче числа переменных - исключение в рантайме:** -> ```kotlin -> fun main() { -> val list = listOf(1) -> val (a, b) = list // компилируется! -> println("$a $b") // IndexOutOfBoundsException -> } -> ``` -> У `List` есть `component1()`..`component5()`, и компилятор не знает фактическую длину. С `data class` такой проблемы нет. -> -> **`component1()` вызывается даже для неиспользуемой переменной** - если это не `_`. Для свойства с тяжёлым геттером лишняя работа. -> -> **Деструктуризация `Pair` теряет смысл имён.** `val (a, b) = getCoordinates()` не говорит, что где. Лучше `data class Coordinates(val lat: Double, val lon: Double)`. - -Далее: [Приведение типов](Приведение%20типов.md) diff --git a/Kotlin/Синтаксис и типы/Работа с данными/Лямбды и функциональные типы.md b/Kotlin/Синтаксис и типы/Работа с данными/Лямбды и функциональные типы.md deleted file mode 100644 index 976a504..0000000 --- a/Kotlin/Синтаксис и типы/Работа с данными/Лямбды и функциональные типы.md +++ /dev/null @@ -1,273 +0,0 @@ ---- -title: Лямбды и функциональные типы -description: Синтаксис лямбд, it, функциональные типы, ссылки на функции и анонимные функции -tags: - - kotlin - - functional ---- - -Функциональный тип описывает функцию как значение: `(параметры) -> результат`. - -```kotlin -val square: (Int) -> Int = { x -> x * x } -val greet: (String) -> String = { "Привет, $it" } -val action: () -> Unit = { println("сделано") } -val sum: (Int, Int) -> Int = { a, b -> a + b } - -fun main() { - println(square(4)) // 16 - println(greet("мир")) // Привет, мир - action() // сделано - println(sum(2, 3)) // 5 -} -``` - -## Синтаксис лямбды -Лямбда всегда в фигурных скобках: -```kotlin -{ параметры -> тело } -``` -```kotlin -val f = { x: Int, y: Int -> x + y } // типы указаны в лямбде -val g: (Int) -> Int = { it * 2 } // тип указан у переменной -``` -Тип нужно указать либо у переменной, либо у параметров лямбды - где-то компилятор должен его увидеть. - -Результат - **последнее выражение** тела, `return` не нужен и в обычной лямбде запрещён: -```kotlin -val describe: (Int) -> String = { n -> - val prefix = if (n > 0) "плюс" else "минус" - "$prefix $n" // это и есть результат -} - -fun main() { println(describe(5)) } // плюс 5 -``` - -## it - неявный параметр -Если параметр один, его можно не объявлять: -```kotlin -val double: (Int) -> Int = { it * 2 } -println(listOf(1, 2, 3).map { it * 10 }) // [10, 20, 30] -``` -`it` удобен в короткой лямбде. Если тело длинное или лямбды вложены - давайте имя: -```kotlin -val users = listOf("Иван", "Пётр") -println(users.map { name -> name.uppercase() }) // [ИВАН, ПЁТР] -``` - -## Лямбда последним аргументом -Если последний параметр функции - функция, лямбду выносят за скобки: -```kotlin -fun measure(name: String, block: () -> Unit) { - val start = System.nanoTime() - block() - println("$name занял ${System.nanoTime() - start} нс") -} - -fun main() { - measure("цикл") { // лямбда вне скобок - repeat(1000) { } - } - measure("пусто", { }) // так тоже можно, но не принято -} -``` -Когда функция принимает **только** лямбду, круглые скобки опускаются совсем. Именно на этом правиле построены `forEach`, `let`, `apply` и все DSL: -```kotlin -listOf(1, 2).forEach { println(it) } -``` -Подробнее: [DSL](../../Функциональный%20Kotlin/DSL.md) - -## Неиспользуемые параметры -```kotlin -val map = mapOf("a" to 1) -map.forEach { (_, value) -> println(value) } // 1 — ключ не нужен -``` -Подчёркивание говорит компилятору, что параметр намеренно игнорируется. - -## Ссылки на функции - :: -Вместо лямбды, которая только вызывает существующую функцию, используют ссылку: -```kotlin -fun isEven(n: Int) = n % 2 == 0 - -fun main() { - val nums = listOf(1, 2, 3, 4) - println(nums.filter { isEven(it) }) // [2, 4] — лямбда-обёртка - println(nums.filter(::isEven)) // [2, 4] — ссылка, короче - nums.forEach(::println) -} -``` - -Виды ссылок: -```kotlin -class User(val name: String) { - fun greet() = "Привет, $name" -} - -fun main() { - val byName: (User) -> String = User::name // ссылка на свойство - val greeter: (User) -> String = User::greet // на метод класса - val factory: (String) -> User = ::User // на конструктор - - val u = User("Иван") - println(byName(u)) // Иван - println(greeter(u)) // Привет, Иван - println(factory("Пётр").name) // Пётр - - val bound = u::greet // привязанная к объекту - println(bound()) // Привет, Иван -} -``` - -## Анонимные функции -Когда нужен явный `return` или явный тип результата: -```kotlin -val parse = fun(s: String): Int? { - if (s.isBlank()) return null // return работает - return s.toIntOrNull() -} - -fun main() { - println(parse(" ")) // null - println(parse("42")) // 42 -} -``` -Отличие от лямбды: `return` выходит из самой анонимной функции, а в лямбде он запрещён (или выходит из внешней функции, если лямбда inline). - -## Функциональные типы в подробностях -```kotlin -val a: () -> Unit // без параметров, без результата -val b: (Int) -> String // один параметр -val c: (Int, Int) -> Int // два параметра -val d: ((Int) -> Int) -> Int // принимает функцию -val e: (Int) -> ((Int) -> Int) // возвращает функцию -val f: String.(Int) -> String // с получателем (receiver) -val g: ((Int) -> Int)? // nullable -val h: suspend () -> Unit // suspend-функция -``` - -Тип с получателем вызывается как метод: -```kotlin -val repeatTimes: String.(Int) -> String = { n -> this.repeat(n) } - -fun main() { - println("ab".repeatTimes(2)) // abab -} -``` -На этом построены `apply`, `buildString` и DSL. Подробнее: [Receivers и context parameters](../../Функциональный%20Kotlin/Receivers%20и%20context%20parameters.md) - -## Замыкания -Лямбда захватывает переменные из окружения - **включая изменяемые**: -```kotlin -fun counter(): () -> Int { - var count = 0 // захвачена по ссылке - return { ++count } -} - -fun main() { - val next = counter() - println(next()) // 1 - println(next()) // 2 - println(next()) // 3 -} -``` -Это важное отличие от Java, где захватить можно только effectively final переменную. - -```kotlin -fun main() { - var sum = 0 - listOf(1, 2, 3).forEach { sum += it } // в Java так нельзя - println(sum) // 6 -} -``` - -## Вызов -```kotlin -val f: (Int) -> Int = { it + 1 } - -fun main() { - println(f(1)) // 2 — обычный вызов - println(f.invoke(1)) // 2 — то же самое - val g: ((Int) -> Int)? = null - println(g?.invoke(1)) // null — безопасный вызов nullable-функции -} -``` - -## Что происходит под капотом -Функциональный тип компилируется в интерфейс `FunctionN`: -```kotlin -val f: (Int) -> Int = { it * 2 } // объект класса Function1 -``` -Это значит аллокацию на каждую лямбду, если она захватывает переменные. Для функций высшего порядка в горячем коде существует `inline` - он подставляет тело лямбды в место вызова, и объекта не создаётся вовсе. - -Подробнее: [Inline-функции](../../Функциональный%20Kotlin/Inline-функции.md), [Kotlin в байт-коде](../../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `x -> x * 2` | `{ x -> x * 2 }` или `{ it * 2 }` | -> | `Function` | `(Int) -> Int` | -> | `BiFunction` | `(A, B) -> C` | -> | `Supplier` | `() -> T` | -> | `Consumer` | `(T) -> Unit` | -> | `Predicate` | `(T) -> Boolean` | -> | `Runnable` | `() -> Unit` | -> | `String::length` | `String::length` | -> | нет неявного параметра | `it` | -> | захват только effectively final | захват изменяемых переменных | -> | лямбда только для SAM-интерфейса | функциональный тип первого класса | -> -> В Java лямбда - это реализация функционального интерфейса, поэтому для каждой формы нужен свой тип (`Function`, `BiFunction`, `Supplier`, `Consumer`, `Predicate`…). В Kotlin тип функции записывается напрямую: `(Int, String) -> Boolean` не требует придумывать имя. -> -> Захват изменяемых переменных - практическая разница. Java требует `final` или effectively final, вынуждая использовать массив-обёртку `int[] sum = {0}`. В Kotlin `var sum = 0` захватывается как есть (компилятор оборачивает его в `Ref`-объект). -> -> Сравнить: [Функции в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md) - -> [!warning] Подводные камни -> **`return` в лямбде запрещён** - он относился бы к внешней функции: -> ```kotlin -> fun find(items: List): Int? { -> items.forEach { -> if (it > 1) return it // выход из find, а не из лямбды! -> } // работает только потому, что forEach inline -> return null -> } -> -> fun main() { println(find(listOf(1, 2, 3))) } // 2 -> ``` -> Это «нелокальный return», он разрешён только в inline-функциях. Для выхода из самой лямбды нужна метка: -> ```kotlin -> fun main() { -> listOf(1, 2, 3).forEach { -> if (it == 2) return@forEach // пропустить итерацию, как continue -> println(it) // 1, затем 3 -> } -> } -> ``` -> -> **`it` во вложенных лямбдах затеняется:** -> ```kotlin -> fun main() { -> val nested = listOf(listOf(1, 2)) -> // println(nested.map { it.map { it * 2 } }) // непонятно, какое it -> println(nested.map { row -> row.map { v -> v * 2 } }) // [[2, 4]] -> } -> ``` -> -> **Каждая не-inline лямбда с захватом - объект.** В цикле на миллион итераций это миллион аллокаций. Стандартные операции (`map`, `filter`, `forEach`) объявлены `inline`, поэтому бесплатны; свои функции высшего порядка в горячем коде тоже стоит делать `inline`. -> -> **Лямбда, захватившая `this`, держит объект в памяти** - классическая утечка в Android, когда колбэк переживает активити. -> -> **Ссылка на перегруженную функцию неоднозначна:** -> ```kotlin -> fun show(x: Int) = "int" -> fun show(x: String) = "str" -> -> fun main() { -> // val f = ::show // ошибка: какая из двух? -> val f: (Int) -> String = ::show // тип разрешает неоднозначность -> println(f(1)) // int -> } -> ``` - -Дополнительно: [Функции высшего порядка](../../Функциональный%20Kotlin/Функции%20высшего%20порядка.md) -Далее: [Деструктуризация](Деструктуризация.md) diff --git a/Kotlin/Синтаксис и типы/Работа с данными/Массивы.md b/Kotlin/Синтаксис и типы/Работа с данными/Массивы.md deleted file mode 100644 index a4e53f3..0000000 --- a/Kotlin/Синтаксис и типы/Работа с данными/Массивы.md +++ /dev/null @@ -1,249 +0,0 @@ ---- -title: Массивы -description: Array, специализированные IntArray, сравнение через contentEquals и почему обычно нужен List -tags: - - kotlin - - stdlib ---- - -Массив - фиксированная по размеру последовательность элементов. В Kotlin это класс `Array`, а не синтаксическая конструкция. - -```kotlin -val a = arrayOf(1, 2, 3) -val b = Array(3) { it * 2 } // [0, 2, 4] — по лямбде от индекса -val c = arrayOfNulls(3) // [null, null, null] -val d = emptyArray() - -println(a[0]) // 1 -a[0] = 10 -println(a.size) // 3 -println(a.joinToString()) // 10, 2, 3 -``` -Размер задаётся при создании и не меняется. Элементы изменяемы всегда - даже если массив объявлен через `val`. - -## Специализированные массивы -Для чисел есть отдельные классы без упаковки: -```kotlin -val ints = intArrayOf(1, 2, 3) -val zeros = IntArray(5) // [0, 0, 0, 0, 0] — заполнен нулями -val squares = IntArray(5) { it * it } // [0, 1, 4, 9, 16] -val doubles = doubleArrayOf(1.0, 2.0) -val chars = charArrayOf('a', 'b') -val bools = BooleanArray(3) // [false, false, false] -``` - -| Класс | Байт-код | Аналог в Java | -|---|---|---| -| `IntArray` | `int[]` | `int[]` | -| `LongArray` | `long[]` | `long[]` | -| `DoubleArray` | `double[]` | `double[]` | -| `CharArray` | `char[]` | `char[]` | -| `BooleanArray` | `boolean[]` | `boolean[]` | -| `Array` | `Integer[]` | `Integer[]` | - -Разница принципиальна: `Array` хранит **упакованные** объекты, `IntArray` - примитивы. В горячем коде это заметно по памяти и скорости. - -```kotlin -val boxed: Array = arrayOf(1, 2, 3) // Integer[] — аллокация на элемент -val raw: IntArray = intArrayOf(1, 2, 3) // int[] — без упаковки -println("${boxed.size} ${raw.size}") -``` - -## Сравнение - только contentEquals -`==` на массивах сравнивает **ссылки**, а не содержимое. Это главная ловушка: -```kotlin -val a = arrayOf(1, 2, 3) -val b = arrayOf(1, 2, 3) -println(a == b) // false — разные объекты! -println(a.contentEquals(b)) // true — так правильно -println(a.contentHashCode()) // хеш по содержимому -``` -Для вложенных массивов - глубокие версии: -```kotlin -val x = arrayOf(arrayOf(1), arrayOf(2)) -val y = arrayOf(arrayOf(1), arrayOf(2)) -println(x.contentEquals(y)) // false — сравнивает ссылки вложенных -println(x.contentDeepEquals(y)) // true -``` - -## Печать -`toString()` массива бесполезен: -```kotlin -val a = arrayOf(1, 2, 3) -println(a) // [Ljava.lang.Integer;@1b6d3586 -println(a.contentToString()) // [1, 2, 3] -println(a.joinToString()) // 1, 2, 3 -println("$a".startsWith("[L")) // true — шаблон вызывает тот же toString -``` -Внутри строкового шаблона всегда нужен `contentToString()` или `joinToString()`. - -## Обход -```kotlin -val a = arrayOf("x", "y", "z") -for (item in a) print(item) // xyz -for (i in a.indices) print(a[i]) // xyz -for ((i, v) in a.withIndex()) print("$i$v ") // 0x 1y 2z -a.forEach { print(it) } // xyz -a.forEachIndexed { i, v -> print("$i$v ") } -println() -println(a.lastIndex) // 2 -``` - -## Операции -Массивы поддерживают те же операции, что коллекции: -```kotlin -val a = intArrayOf(3, 1, 2) -println(a.sorted()) // [1, 2, 3] — возвращает List! -println(a.sortedArray()) // новый IntArray -a.sort() // сортировка на месте -println(a.contentToString()) // [1, 2, 3] -println(a.sum()) // 6 -println(a.average()) // 2.0 -println(a.max()) // 3 -println(a.filter { it > 1 }) // [2, 3] — List -println(a.map { it * 2 }) // [2, 4, 6] — List -println(a.reversed()) // [3, 2, 1] — List -println(a.reversedArray().contentToString()) // [3, 2, 1] -println(a.contains(2)) // true -println(2 in a) // true -println(a.indexOf(2)) // 1 -``` -Обратите внимание: `filter`, `map`, `sorted`, `reversed` возвращают `List`, а не массив. Версии, сохраняющие тип, имеют суффикс `Array`. - -## Копирование и изменение размера -Размер массива неизменен, поэтому «добавление» - это создание нового: -```kotlin -val a = intArrayOf(1, 2, 3) -println((a + 4).contentToString()) // [1, 2, 3, 4] — новый массив -println(a.copyOf().contentToString()) // [1, 2, 3] -println(a.copyOf(5).contentToString()) // [1, 2, 3, 0, 0] — дополнен нулями -println(a.copyOf(2).contentToString()) // [1, 2] — обрезан -println(a.copyOfRange(1, 3).contentToString()) // [2, 3] -println(a.sliceArray(0..1).contentToString()) // [1, 2] -a.fill(0) -println(a.contentToString()) // [0, 0, 0] -``` - -## Преобразование в коллекции и обратно -```kotlin -val a = arrayOf(1, 2, 3) -val list: List = a.toList() -val set = a.toSet() -val back: Array = list.toTypedArray() -val raw: IntArray = list.toIntArray() -println("${list} ${set} ${back.size} ${raw.size}") -``` - -## Многомерные массивы -Отдельного типа нет - это массив массивов: -```kotlin -val grid = Array(3) { IntArray(4) } // 3x4, заполнен нулями -grid[1][2] = 5 -println(grid[1].contentToString()) // [0, 0, 5, 0] - -val table = Array(2) { r -> IntArray(2) { c -> r * 2 + c } } -println(table.joinToString { it.contentToString() }) // [0, 1], [2, 3] -``` -Строки могут иметь разную длину (рваный массив): -```kotlin -val jagged = arrayOf(intArrayOf(1), intArrayOf(1, 2, 3)) -println(jagged.joinToString { it.size.toString() }) // 1, 3 -``` - -## Когда массив, а когда List -Массив нужен редко. Используйте его, если: -- важна производительность и нужны примитивы без упаковки (`IntArray`) -- работаете с API, требующим массив (`vararg`, Java-библиотеки) -- размер фиксирован и известен заранее - -В остальных случаях - `List`: он не даёт менять себя случайно, имеет больше операций и корректный `equals`. - -```kotlin -val list = listOf(1, 2, 3) -println(list == listOf(1, 2, 3)) // true — работает из коробки -``` -Подробнее: [Коллекции](../../Коллекции/Коллекции.md) - -## vararg -Параметр с переменным числом аргументов внутри функции - массив: -```kotlin -fun sum(vararg nums: Int): Int { - var total = 0 - for (n in nums) total += n // nums это IntArray - return total -} - -fun main() { - println(sum(1, 2, 3)) // 6 - println(sum()) // 0 - val arr = intArrayOf(1, 2) - println(sum(*arr)) // 3 — spread-оператор -} -``` -Подробнее: [Функции](Функции.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `int[] a = {1,2,3}` | `val a = intArrayOf(1,2,3)` | -> | `new int[5]` | `IntArray(5)` | -> | `a.length` | `a.size` | -> | `Arrays.toString(a)` | `a.contentToString()` | -> | `Arrays.equals(a,b)` | `a.contentEquals(b)` | -> | `Arrays.deepEquals(a,b)` | `a.contentDeepEquals(b)` | -> | `Arrays.sort(a)` | `a.sort()` | -> | `Arrays.copyOf(a,n)` | `a.copyOf(n)` | -> | `Arrays.fill(a,0)` | `a.fill(0)` | -> | `int[]` и `Integer[]` | `IntArray` и `Array` | -> | `f(arr)` для varargs | `f(*arr)` — нужен spread | -> -> Статические методы `Arrays` стали методами массива - находятся автодополнением, а не поиском по документации. -> -> Разделение `IntArray` / `Array` сделано намеренно: в Java `Integer[]` и `int[]` тоже разные, но это легко не заметить. В Kotlin разные имена не позволяют спутать упакованный массив с примитивным. -> -> **Массивы в Kotlin инвариантны**, в Java - ковариантны. Java разрешает `Object[] o = new String[1]; o[0] = 1;` и падает в рантайме (`ArrayStoreException`). В Kotlin `Array = Array` не скомпилируется. Подробнее: [Вариантность](../../Дженерики/Вариантность.md) -> -> Сравнить: [Массивы в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Массивы.md) - -> [!warning] Подводные камни -> **`==` на массивах сравнивает ссылки.** Самая частая ошибка: -> ```kotlin -> val a = arrayOf(1, 2) -> println(a == arrayOf(1, 2)) // false -> println(a.contentEquals(arrayOf(1, 2))) // true -> ``` -> По этой же причине массив нельзя использовать как ключ `Map` или в `Set` - `hashCode` считается по ссылке. -> -> **Массив внутри `data class` ломает `equals`:** -> ```kotlin -> data class Packet(val data: ByteArray) -> -> fun main() { -> val p1 = Packet(byteArrayOf(1)) -> println(p1 == Packet(byteArrayOf(1))) // false! -> } -> ``` -> Сгенерированный `equals` сравнивает массив по ссылке, а не по содержимому. Поэтому в `data class` с массивом `equals`/`hashCode` пишут руками. Подробнее: [Data class](../Базовые%20типы%20ООП/Data%20class.md) -> -> **`println(array)` печатает мусор** вида `[Ljava.lang.Integer;@1b6d3586`. То же в строковых шаблонах. -> -> **`val` не защищает элементы:** -> ```kotlin -> val a = intArrayOf(1, 2) -> a[0] = 99 // разрешено -> println(a.contentToString()) // [99, 2] -> ``` -> Неизменяемость даёт `List`, а не `val`. -> -> **`Array` вместо `IntArray` - скрытая просадка.** Каждый элемент упакован в `Integer`: больше памяти и промахи кеша. -> -> **`arrayOfNulls` и `Array(n)` для непримитивных типов требуют разного подхода:** -> ```kotlin -> val a = arrayOfNulls(3) // Array -> val b = Array(3) { "" } // Array — инициализатор обязателен -> println("${a.size} ${b.size}") -> ``` -> Создать `Array` без инициализатора нельзя - компилятор не позволит оставить non-null элементы пустыми. - -Дополнительно: [Коллекции](../../Коллекции/Коллекции.md) -Далее: [Функции](Функции.md) diff --git a/Kotlin/Синтаксис и типы/Работа с данными/Приведение типов.md b/Kotlin/Синтаксис и типы/Работа с данными/Приведение типов.md deleted file mode 100644 index e8f5a45..0000000 --- a/Kotlin/Синтаксис и типы/Работа с данными/Приведение типов.md +++ /dev/null @@ -1,254 +0,0 @@ ---- -title: Приведение типов -description: Проверка is, приведение as и as?, числовые преобразования и стирание типов -tags: - - kotlin - - types ---- - -## Проверка типа - is -```kotlin -fun describe(x: Any): String = when { - x is String -> "строка длины ${x.length}" - x is Int -> "число ${x + 1}" - x !is Boolean -> "не логическое" - else -> "логическое" -} - -fun main() { - println(describe("ab")) // строка длины 2 - println(describe(5)) // число 6 - println(describe(1.0)) // не логическое -} -``` -После успешной проверки компилятор сам приводит тип - это [умное приведение](../Null-безопасность/Умные%20приведения.md), явный каст не нужен. - -## Небезопасное приведение - as -```kotlin -fun main() { - val x: Any = "text" - val s = x as String // ok - println(s.length) // 4 - - val n: Any = 42 - // val bad = n as String // ClassCastException -} -``` -`as` используют, когда тип известен точно, а компилятор об этом знать не может. - -## Безопасное приведение - as? -Возвращает `null` вместо исключения: -```kotlin -fun main() { - val x: Any = "text" - println(x as? Int) // null - println(x as? String) // text - println((x as? Int) ?: 0) // 0 — частая связка с elvis -} -``` -Результат всегда nullable, даже при приведении к non-null типу. - -Сравнение способов: - -| Запись | При несовпадении типа | -|---|---| -| `x is T` | `false` | -| `x as T` | `ClassCastException` | -| `x as? T` | `null` | - -## Приведение к nullable -```kotlin -fun main() { - val a: Any? = null - println(a as? String) // null - // println(a as String) // NPE: null cannot be cast to non-null type - println(a as String?) // null — приведение к nullable разрешено -} -``` - -## Числовые преобразования -Для чисел `as` не применяется - только явные функции: -```kotlin -fun main() { - val i = 42 - println(i.toLong()) // 42 - println(i.toDouble()) // 42.0 - println(i.toByte()) // 42 - println(i.toChar()) // * - println(3.99.toInt()) // 3 — обрезание, не округление - println("42".toInt()) // 42 -} -``` -Неявных преобразований между числовыми типами нет: -```kotlin -fun main() { - val i: Int = 42 - // val l: Long = i // ошибка компиляции - val l: Long = i.toLong() // так -} -``` - -Потеря данных происходит молча: -```kotlin -fun main() { - println(300.toByte()) // 44 — старшие биты отброшены - println(1e20.toInt()) // 2147483647 — обрезано - println(Double.NaN.toInt()) // 0 -} -``` -Подробнее: [Числа и переполнение](Числа%20и%20переполнение.md) - -## Стирание типов и дженерики -Параметры типа стираются при компиляции, поэтому проверить их в рантайме нельзя: -```kotlin -fun check(x: Any) { - if (x is List<*>) { // можно: звёздочка вместо типа - println("список из ${x.size}") - } - // if (x is List) // ошибка: cannot check for instance - // // of erased type -} - -fun main() { check(listOf(1, 2)) } // список из 2 -``` -`List<*>` - это star-проекция: «список чего-то неизвестного». Элементы из него читаются как `Any?`. - -Приведение к конкретному дженерику компилируется с предупреждением и небезопасно: -```kotlin -@Suppress("UNCHECKED_CAST") -fun castList(x: Any): List = x as List // unchecked cast - -fun main() { - val nums: List = castList(listOf("a", "b")) // не упадёт здесь! - // println(nums.first() + 1) // ClassCastException только при обращении - println(nums.size) // 2 — работает, типы не проверяются -} -``` -Это отложенное падение: ошибка проявится далеко от места приведения. - -Обойти стирание можно через `reified` в inline-функциях: -```kotlin -inline fun Any.safeCast(): T? = this as? T - -fun main() { - println("text".safeCast()) // text - println("text".safeCast()) // null -} -``` -Подробнее: [Reified и inline](../../Дженерики/Reified%20и%20inline.md) - -## Приведение в иерархии -```kotlin -open class Animal(val name: String) -class Dog(name: String) : Animal(name) { - fun bark() = "Гав" -} - -fun main() { - val a: Animal = Dog("Рекс") // вверх по иерархии — неявно - println(a.name) // Рекс - - val d = a as Dog // вниз — только явно - println(d.bark()) // Гав - - val other: Animal = Animal("кто-то") - println((other as? Dog)?.bark()) // null — безопасно -} -``` -Приведение вверх (к супертипу) всегда безопасно и неявно. Приведение вниз требует `as` или `as?`. - -## Коллекции и вариантность -```kotlin -fun main() { - val strings: List = listOf("a") - val anys: List = strings // ok — List ковариантен по out T - println(anys.size) // 1 - - val mStrings: MutableList = mutableListOf("a") - // val mAnys: MutableList = mStrings // ошибка — MutableList инвариантен -} -``` -`List` разрешает такое присваивание, `MutableList` - нет, иначе можно было бы положить `Int` в список строк. Подробнее: [Вариантность](../../Дженерики/Вариантность.md) - -## Получение типа в рантайме -```kotlin -fun main() { - val x: Any = "text" - println(x::class) // class kotlin.String - println(x::class.simpleName) // String - println(x::class.java) // class java.lang.String — Java-класс - println(x is CharSequence) // true — проверка по интерфейсу -} -``` -Подробнее: [Рефлексия](../../Рефлексия/Рефлексия.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `x instanceof String` | `x is String` | -> | `(String) x` | `x as String` | -> | нет безопасного каста | `x as? String` | -> | `(int) doubleValue` | `doubleValue.toInt()` | -> | `long l = intValue;` неявно | `intValue.toLong()` явно | -> | `x.getClass()` | `x::class` или `x::class.java` | -> | после `instanceof` нужен каст | умное приведение | -> | массивы ковариантны | массивы инвариантны | -> -> Главные приобретения: `as?` вместо пары `instanceof` + каст, и запрет неявных числовых преобразований, из-за которых в Java `long total = intA * intB` тихо переполняется. -> -> Ковариантность массивов в Java - известная дыра в типизации: -> ```java -> Object[] objs = new String[1]; -> objs[0] = 42; // компилируется, падает в рантайме -> ``` -> В Kotlin `Array` инвариантен, и такой код не соберётся. -> -> Сравнить: [Приведение ссылочных типов в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Приведение%20ссылочных%20типов.md) - -> [!warning] Подводные камни -> **`as` на null даёт NPE, а не ClassCastException:** -> ```kotlin -> fun main() { -> val a: Any? = null -> // println(a as String) // NullPointerException -> println(a as String?) // null — ok -> } -> ``` -> -> **`as?` маскирует обе причины.** `null` в результате означает либо неверный тип, либо что значение изначально было `null` - различить невозможно: -> ```kotlin -> fun main() { -> val a: Any? = null -> val b: Any = 42 -> println("${a as? String} ${b as? String}") // null null -> } -> ``` -> -> **Unchecked cast падает не там, где написан:** -> ```kotlin -> @Suppress("UNCHECKED_CAST") -> fun main() { -> val list = listOf("a") as List // здесь не падает -> println(list.size) // 1 — и здесь тоже -> // println(list[0] + 1) // ClassCastException только тут -> } -> ``` -> Поэтому `@Suppress("UNCHECKED_CAST")` - всегда повод задуматься. -> -> **`toInt()` на `Double` обрезает дробную часть, а не округляет.** `3.99.toInt()` даёт `3`. Для округления - `roundToInt()`. -> -> **`toByte()` и `toShort()` молча теряют старшие биты:** `300.toByte()` возвращает `44`. -> -> **`is` не проверяет параметры дженерика.** `x is List` не скомпилируется, а `x is List<*>` ничего не говорит об элементах. Проверять придётся вручную: -> ```kotlin -> fun main() { -> val x: Any = listOf(1, "a") -> val strings = (x as? List<*>)?.filterIsInstance() -> println(strings) // [a] — только элементы нужного типа -> } -> ``` -> -> **`x::class` и `x::class.java` - разные вещи.** Первое - `KClass` Kotlin, второе - `Class` Java. Для `Int` они могут отличаться (`int` против `Integer`). - -Дополнительно: [Умные приведения](../Null-безопасность/Умные%20приведения.md) -Далее: [Any, Unit, Nothing](../Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md) diff --git a/Kotlin/Синтаксис и типы/Работа с данными/Строки.md b/Kotlin/Синтаксис и типы/Работа с данными/Строки.md deleted file mode 100644 index 57ef3e4..0000000 --- a/Kotlin/Синтаксис и типы/Работа с данными/Строки.md +++ /dev/null @@ -1,222 +0,0 @@ ---- -title: Строки -description: String как неизменяемый тип, полезные методы, StringBuilder и buildString -tags: - - kotlin - - stdlib ---- - -`String` - неизменяемый тип. Любая «модификация» создаёт новую строку. - -```kotlin -val s = "Kotlin" -println(s.length) // 6 -println(s[0]) // K — доступ по индексу, как у массива -println(s.uppercase()) // KOTLIN -println(s) // Kotlin — исходная строка не изменилась -``` - -На JVM это тот же `java.lang.String`, со всем его поведением: пул строк, интернирование, представление в LATIN-1 или UTF-16. - -## Сравнение -```kotlin -val a = "Kotlin" -val b = StringBuilder("Kotlin").toString() -println(a == b) // true — сравнение содержимого -println(a === b) // false — разные объекты -println(a.equals("KOTLIN", ignoreCase = true)) // true -``` -`==` вызывает `equals()`, поэтому работает правильно всегда. Это главное практическое отличие от Java, где `==` на строках сравнивает ссылки. - -Для сортировки и порядка - `compareTo`: -```kotlin -println("abc" < "abd") // true -println("a".compareTo("b")) // -1 -println(listOf("b", "a", "c").sorted()) // [a, b, c] -``` - -## Итерация -Строка - последовательность `Char`: -```kotlin -for (c in "abc") print("$c.") // a.b.c. -println("abc".toList()) // [a, b, c] -println("hello".count { it == 'l' }) // 2 -println("hello".filter { it != 'l' }) // heo -println("abc".map { it.uppercaseChar() }) // [A, B, C] -println("abc".reversed()) // cba -``` -Работают все операции над коллекциями: `filter`, `map`, `any`, `count`. Только `filter` и `reversed` возвращают `String`, а `map` - `List`. - -## Поиск и проверки -```kotlin -val s = "Hello, Kotlin" -println(s.contains("Kotlin")) // true -println("Kotlin" in s) // true — то же самое -println(s.startsWith("Hello")) // true -println(s.endsWith("lin")) // true -println(s.indexOf("o")) // 4 — первое вхождение, -1 если нет -println(s.lastIndexOf("o")) // 8 -println(s.isEmpty()) // false — длина 0 -println(" ".isBlank()) // true — пусто или только пробелы -println(s.isNotEmpty()) // true -``` -Для nullable-строк есть безопасные версии, их можно звать без `?.`: -```kotlin -val n: String? = null -println(n.isNullOrEmpty()) // true -println(n.isNullOrBlank()) // true -println(n.orEmpty()) // "" — пустая строка вместо null -``` - -## Извлечение частей -```kotlin -val s = "Hello, Kotlin" -println(s.substring(7)) // Kotlin -println(s.substring(0, 5)) // Hello — [0, 5) -println(s.take(5)) // Hello — первые 5 -println(s.takeLast(6)) // Kotlin -println(s.drop(7)) // Kotlin — отбросить первые 7 -println(s.dropLast(7)) // Hello -println(s.first()) // H -println(s.last()) // n -println(s.getOrNull(100)) // null — вместо исключения -``` -`take` и `drop` безопасны: если запросить больше длины, вернётся вся строка, а не исключение. `substring` за границами падает. - -```kotlin -println("abc".take(100)) // abc -// println("abc".substring(100)) // StringIndexOutOfBoundsException -``` - -## Изменение -Функции `capitalize()` и `decapitalize()` устарели и удалены. Замена пишется так: -```kotlin -fun String.capitalize2() = - replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } - -fun main() { println("hello".capitalize2()) } // Hello -``` - -Остальные методы возвращают новую строку: -```kotlin -fun String.capitalize2() = - replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } - -val s = " Hello, Kotlin " -println(s.trim()) // "Hello, Kotlin" — с обеих сторон -println(s.trimStart()) // "Hello, Kotlin " -println(s.trimEnd()) // " Hello, Kotlin" -println("Hello".replace("l", "L")) // HeLLo — все вхождения -println("Hello".replaceFirst("l", "L")) // HeLlo -println("Hello".lowercase()) // hello -println("hello".uppercase()) // HELLO -println("hello".capitalize2()) // Hello -println("ab".repeat(3)) // ababab -println("5".padStart(3, '0')) // 005 -println("5".padEnd(3)) // "5 " -``` - -## Разбиение и сборка -```kotlin -println("a,b,c".split(",")) // [a, b, c] -println("a1b2c".split("1", "2")) // [a, b, c] — несколько разделителей -println("a,b,c".split(",", limit = 2)) // [a, b,c] -println("a\nb".lines()) // [a, b] — по переводам строк -println(listOf("a","b").joinToString("-")) // a-b -println(listOf(1,2,3).joinToString( - separator = ", ", prefix = "[", postfix = "]")) // [1, 2, 3] -println("hello".chunked(2)) // [he, ll, o] -``` -`joinToString` - основной способ собрать строку из коллекции, у него есть и `transform`: -```kotlin -println(listOf(1, 2).joinToString { "#$it" }) // #1, #2 -``` - -## Преобразование в число -```kotlin -println("42".toInt()) // 42 -println("42".toIntOrNull()) // 42 -println("abc".toIntOrNull()) // null — не бросает исключение -println("3.14".toDouble()) // 3.14 -println("ff".toInt(16)) // 255 — с основанием -// println("abc".toInt()) // NumberFormatException -``` -В коде, который разбирает внешние данные, используйте только версии `*OrNull`. - -## StringBuilder и buildString -Для сборки строки в цикле конкатенация создаёт мусор - на каждой итерации новый объект. `StringBuilder` меняет один буфер: -```kotlin -val sb = StringBuilder() -for (i in 1..3) sb.append(i).append(",") -println(sb.toString()) // 1,2,3, -``` -Идиоматичнее - `buildString`, он сам создаёт билдер и возвращает результат: -```kotlin -val s = buildString { - append("Привет") - append(", ") - append("Kotlin") - appendLine("!") - append("Строка 2") -} -println(s) -``` -Полезные методы билдера: `append`, `appendLine`, `insert(i, x)`, `deleteAt(i)`, `setLength(n)`, `reverse()`, `clear()`. - -## Многострочные строки -```kotlin -val json = """ - {"name": "Kotlin"} -""".trimIndent() -``` -Подробнее: [Строковые шаблоны и вывод](../Базовый%20синтаксис/Строковые%20шаблоны%20и%20вывод.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `s.equals(t)` | `s == t` | -> | `s == t` (ссылки) | `s === t` | -> | `s.charAt(0)` | `s[0]` | -> | `s.isEmpty()` | `s.isEmpty()`, плюс `isBlank()`, `isNullOrBlank()` | -> | `s.toUpperCase()` | `s.uppercase()` | -> | `new StringBuilder()` + `toString()` | `buildString { }` | -> | `String.join(",", list)` | `list.joinToString(",")` | -> | `Integer.parseInt(s)` | `s.toInt()` / `s.toIntOrNull()` | -> | `s.split(",")` → `String[]` | → `List` | -> | `s.substring(100)` падает | `s.take(100)` безопасен | -> -> Главное приобретение - `*OrNull`-функции и безопасные `take`/`drop` вместо исключений на каждой опечатке во входных данных. -> -> `uppercase()` вместо `toUpperCase()` - не просто переименование: старая версия Java зависела от системной локали, из-за чего `"i".toUpperCase()` в турецкой локали давало `İ`. Kotlin по умолчанию использует `Locale.ROOT`. -> -> Сравнить: [Строки в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Строки.md) - -> [!warning] Подводные камни -> **`length` считает кодовые единицы UTF-16, а не символы.** Эмодзи и редкие иероглифы занимают две: -> ```kotlin -> println("😀".length) // 2, а не 1 -> println("😀".count()) // 2 — то же самое -> println("😀".codePointCount(0, "😀".length)) // 1 — настоящее число символов -> ``` -> Разрезать такую строку по индексу - значит получить битые символы. -> -> **`split` с пустым разделителем и пустой строкой дают неожиданный результат:** -> ```kotlin -> println("".split(",")) // [] — один пустой элемент, размер 1 -> println("".split(",").size) // 1 -> println("a,,b".split(",")) // [a, , b] — пустой элемент сохраняется -> ``` -> -> **`trimIndent` не обрезает пробелы внутри строк** - только общий отступ. -> -> **`replace` принимает строку, а не regex** - в отличие от Java: -> ```kotlin -> println("a.b".replace(".", "-")) // a-b — точка как символ -> println("a.b".replace(Regex("."), "-")) // --- — regex -> ``` -> В Java `String.replaceAll` работает с regex, и это частый источник путаницы при переходе. -> -> **Строка в `when` сравнивается по значению, но полнота не проверяется** - для `String` всегда нужен `else`. - -Дополнительно: [Char](Char.md) -Далее: [Char](Char.md) diff --git a/Kotlin/Синтаксис и типы/Работа с данными/Функции.md b/Kotlin/Синтаксис и типы/Работа с данными/Функции.md deleted file mode 100644 index 7d4b200..0000000 --- a/Kotlin/Синтаксис и типы/Работа с данными/Функции.md +++ /dev/null @@ -1,280 +0,0 @@ ---- -title: Функции -description: Объявление функций, значения по умолчанию, именованные аргументы, vararg и локальные функции -tags: - - kotlin - - syntax ---- - -Функция объявляется словом `fun`. В Kotlin она может жить на верхнем уровне файла, внутри класса или внутри другой функции. - -```kotlin -fun greet(name: String): String { - return "Привет, $name" -} - -fun main() { - println(greet("Kotlin")) // Привет, Kotlin -} -``` -Порядок: `fun` → имя → параметры → `:` тип результата → тело. - -## Выражение вместо тела -Если функция - одно выражение, фигурные скобки и `return` убираются: -```kotlin -fun square(x: Int): Int = x * x -fun greet(name: String) = "Привет, $name" // тип выведен: String -fun max(a: Int, b: Int) = if (a > b) a else b -``` -Это основная форма в идиоматичном Kotlin. Тип результата можно опустить - компилятор выведет его. Но для публичного API его лучше писать явно: это контракт, и его случайное изменение не должно быть незаметным. - -## Unit - отсутствие результата -```kotlin -fun log(message: String): Unit { - println(message) -} - -fun log2(message: String) { // : Unit можно опустить - println(message) -} -``` -`Unit` - настоящий тип с единственным значением, а не пустота. Подробнее: [Any, Unit, Nothing](../Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md) - -## Параметры -Параметры всегда **только для чтения** - это `val`, присвоить им нельзя: -```kotlin -fun bad(x: Int) { - // x = 5 // ошибка компиляции - val y = x + 1 - println(y) -} -``` -Тип параметра обязателен - вывода типов здесь нет. - -## Значения по умолчанию -```kotlin -fun connect(host: String, port: Int = 8080, secure: Boolean = false) { - println("$host:$port secure=$secure") -} - -fun main() { - connect("localhost") // localhost:8080 secure=false - connect("localhost", 443) // localhost:443 secure=false - connect("localhost", 443, true) // localhost:443 secure=true -} -``` -Это заменяет перегрузки: вместо трёх версий метода - одна функция. Значением по умолчанию может быть любое выражение, включая вызов функции и другие параметры: -```kotlin -fun repeat2(text: String, times: Int = text.length) = text.repeat(times) - -fun main() { - println(repeat2("ab")) // abab — times = 2 - println(repeat2("ab", 3)) // ababab -} -``` -Выражение вычисляется при каждом вызове, в котором аргумент опущен. - -## Именованные аргументы -Аргумент можно передать по имени параметра: -```kotlin -fun connect(host: String, port: Int = 80, secure: Boolean = false, timeout: Int = 30) { - println("$host:$port s=$secure t=$timeout") -} - -fun main() { - connect("localhost", secure = true) // пропустили port - connect(host = "localhost", timeout = 60) - connect(port = 443, host = "localhost") // порядок не важен -} -``` -Две причины использовать: -1. **Пропустить** часть параметров со значениями по умолчанию -2. **Читаемость** на вызовах с непонятными литералами - -```kotlin -// что здесь true? -// createUser("Иван", true, false, true) -// так понятно: -// createUser("Иван", admin = true, banned = false, verified = true) -``` - -Правило: позиционные аргументы идут до именованных. После первого именованного все следующие тоже должны быть именованными - кроме `vararg` со spread. - -## vararg -Переменное число аргументов: -```kotlin -fun sum(vararg nums: Int): Int { - var total = 0 - for (n in nums) total += n // nums имеет тип IntArray - return total -} - -fun main() { - println(sum(1, 2, 3)) // 6 - println(sum()) // 0 - val arr = intArrayOf(1, 2, 3) - println(sum(*arr)) // 6 — spread-оператор обязателен -} -``` -Правила: -- `vararg` может быть **только один** в функции -- он не обязан быть последним, но тогда остальные передаются по имени -- внутри функции это массив (`Array` или специализированный) -- для передачи готового массива нужен `*` (spread) - -```kotlin -fun tag(vararg classes: String, id: String) = "$id: ${classes.joinToString()}" - -fun main() { - println(tag("a", "b", id = "x")) // x: a, b -} -``` - -## Локальные функции -Функция внутри функции - видит переменные внешней: -```kotlin -fun process(items: List): List { - val prefix = ">> " // захватывается - - fun format(x: Int): String = "$prefix$x" // локальная функция - - return items.map { format(it) } -} - -fun main() { - println(process(listOf(1, 2))) // [>> 1, >> 2] -} -``` -Нужны, когда логика используется только внутри одной функции и не стоит того, чтобы выносить её в класс или в приватную функцию файла. - -## Перегрузка -Функции с одним именем и разными параметрами: -```kotlin -fun show(x: Int) = "число $x" -fun show(x: String) = "строка $x" -fun show(x: Int, y: Int) = "пара $x,$y" - -fun main() { - println(show(1)) // число 1 - println(show("a")) // строка a - println(show(1, 2)) // пара 1,2 -} -``` -Сигнатуру образуют имя и типы параметров. Тип результата в неё **не входит** - две функции, отличающиеся только им, не скомпилируются. - -Чаще всего перегрузка не нужна: её заменяют значения по умолчанию. - -## Передача аргументов -Аргументы передаются **по значению**, как в Java: -- примитив - копируется значение -- объект - копируется ссылка - -Значит, переприсвоить параметр внутри функции нельзя (он `val`), но изменяемый объект изменить можно: -```kotlin -fun addItem(list: MutableList) { - list.add(99) // объект изменён — виден снаружи -} - -fun main() { - val items = mutableListOf(1) - addItem(items) - println(items) // [1, 99] -} -``` - -## Однострочные функции-расширения -Функции можно добавлять к существующим типам: -```kotlin -fun String.shout() = uppercase() + "!" - -fun main() { - println("привет".shout()) // ПРИВЕТ! -} -``` -Подробнее: [Функции-расширения](../../Функциональный%20Kotlin/Функции-расширения.md) - -## Функции как значения -Функция может быть параметром и результатом: -```kotlin -fun apply2(x: Int, f: (Int) -> Int) = f(x) - -fun main() { - println(apply2(3) { it * 2 }) // 6 - println(apply2(3, ::square)) // 9 — ссылка на функцию -} - -fun square(x: Int) = x * x -``` -Подробнее: [Лямбды и функциональные типы](Лямбды%20и%20функциональные%20типы.md), [Функции высшего порядка](../../Функциональный%20Kotlin/Функции%20высшего%20порядка.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | метод только внутри класса | функция на верхнем уровне | -> | `int f(int x) { return x; }` | `fun f(x: Int) = x` | -> | `void` | `Unit` (можно опустить) | -> | нет значений по умолчанию | `fun f(x: Int = 0)` | -> | нет именованных аргументов | `f(x = 1)` | -> | `int... nums` | `vararg nums: Int` | -> | `f(arr)` для varargs | `f(*arr)` | -> | параметр можно переприсвоить | параметр - `val` | -> | перегрузки для опциональных параметров | одна функция со значениями по умолчанию | -> -> Значения по умолчанию + именованные аргументы убирают главный источник шума в Java API - «телескопические» перегрузки: -> ```java -> void connect(String host) { connect(host, 8080); } -> void connect(String host, int port) { connect(host, port, false); } -> void connect(String host, int port, boolean secure) { ... } -> ``` -> В Kotlin это одна строка. Для вызова из Java есть `@JvmOverloads`, который генерирует такие перегрузки автоматически - см. [Kotlin из Java](../../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md). -> -> Сравнить: [Функции в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md) - -> [!warning] Подводные камни -> **Изменяемое значение по умолчанию вычисляется на каждом вызове** - в отличие от Python, где оно создаётся один раз. Это как раз правильное поведение: -> ```kotlin -> fun add(item: Int, list: MutableList = mutableListOf()): List { -> list.add(item) -> return list -> } -> -> fun main() { -> println(add(1)) // [1] -> println(add(2)) // [2] — новый список, а не [1, 2] -> } -> ``` -> -> **Значения по умолчанию не видны из Java.** Без `@JvmOverloads` Java-код обязан передавать все аргументы. -> -> **Переопределённый метод не может изменить значения по умолчанию:** -> ```kotlin -> open class A { open fun f(x: Int = 1) = x } -> class B : A() { -> override fun f(x: Int) = x * 2 // значение по умолчанию наследуется, -> } // переобъявить его нельзя -> ``` -> -> **`vararg` без spread принимает массив как один элемент:** -> ```kotlin -> fun show(vararg items: Any) = items.size -> -> fun main() { -> val arr = arrayOf(1, 2, 3) -> println(show(arr)) // 1 — массив целиком как один аргумент -> println(show(*arr)) // 3 — распакован -> } -> ``` -> -> **Перегрузка + значения по умолчанию = неоднозначность.** Компилятор выберет наиболее специфичную перегрузку, а не ту, что со значениями по умолчанию: -> ```kotlin -> fun f(x: Int, y: Int = 0) = "два" -> fun f(x: Int) = "один" -> -> fun main() { -> println(f(1)) // "один" — перегрузка без default побеждает -> } -> ``` -> Такого сочетания лучше избегать. - -Дополнительно: [Лямбды и функциональные типы](Лямбды%20и%20функциональные%20типы.md) -Далее: [Лямбды и функциональные типы](Лямбды%20и%20функциональные%20типы.md) diff --git a/Kotlin/Синтаксис и типы/Работа с данными/Числа и переполнение.md b/Kotlin/Синтаксис и типы/Работа с данными/Числа и переполнение.md deleted file mode 100644 index 59aded9..0000000 --- a/Kotlin/Синтаксис и типы/Работа с данными/Числа и переполнение.md +++ /dev/null @@ -1,231 +0,0 @@ ---- -title: Числа и переполнение -description: Переполнение целых, точность Double, BigDecimal для денег и математические функции -tags: - - kotlin - - stdlib ---- - -## Переполнение целых -Kotlin, как и Java, **не проверяет переполнение**. При выходе за границы значение «заворачивается»: -```kotlin -println(Int.MAX_VALUE) // 2147483647 -println(Int.MAX_VALUE + 1) // -2147483648 — тихо -println(Int.MIN_VALUE - 1) // 2147483647 -``` -Это не исключение и даже не предупреждение - просто неверный результат. - -Границы типов: -```kotlin -println("${Byte.MIN_VALUE}..${Byte.MAX_VALUE}") // -128..127 -println("${Int.MIN_VALUE}..${Int.MAX_VALUE}") // -2147483648..2147483647 -println("${Long.MIN_VALUE}..${Long.MAX_VALUE}") // -9223372036854775808..9223372036854775807 -``` - -### Где это кусает -Самый частый случай - умножение `Int`, результат которого не влезает: -```kotlin -val ms = 1000 * 60 * 60 * 24 * 30 // -1702967296 — переполнение! -val msOk = 1000L * 60 * 60 * 24 * 30 // 2592000000 — первый операнд Long -println("$ms $msOk") -``` -Правило: если промежуточный результат может превысить 2 млрд, делайте `Long` **первый** операнд. Приписать `L` к последнему множителю недостаточно - умножение идёт слева направо. - -### Проверяемая арифметика -Явные функции, бросающие исключение при переполнении: -```kotlin -println(Math.addExact(1, 2)) // 3 -// println(Math.addExact(Int.MAX_VALUE, 1)) // ArithmeticException -// println(Math.multiplyExact(100000, 100000)) // ArithmeticException -``` -Доступны `addExact`, `subtractExact`, `multiplyExact`, `incrementExact`, `toIntExact`. Это функции JDK, в stdlib Kotlin аналогов нет. - -## Деление -```kotlin -println(7 / 2) // 3 — целочисленное, дробная часть отброшена -println(-7 / 2) // -3 — округление к нулю, не к меньшему -println(7 % 2) // 1 -println(-7 % 2) // -1 — знак от делимого -println(7.0 / 2) // 3.5 -``` -Деление на целочисленный ноль - исключение, на вещественный - нет: -```kotlin -// println(1 / 0) // ArithmeticException: / by zero -println(1.0 / 0) // Infinity -println(-1.0 / 0) // -Infinity -println(0.0 / 0) // NaN -``` - -### Остаток для отрицательных -`%` в Kotlin даёт остаток со знаком делимого, что ломает индексацию по кругу: -```kotlin -println(-1 % 3) // -1 — не 2! -println((-1).mod(3)) // 2 — математический модуль -println((-1).rem(3)) // -1 — то же, что % -``` -Для циклических индексов нужен `mod`, а не `%`. - -## Вещественные числа -`Float` и `Double` следуют IEEE 754 и **не представляют десятичные дроби точно**: -```kotlin -println(0.1 + 0.2) // 0.30000000000000004 -println(0.1 + 0.2 == 0.3) // false -println(1.0 - 0.9) // 0.09999999999999998 -``` - -Сравнивать нужно с допуском: -```kotlin -fun almostEqual(a: Double, b: Double, eps: Double = 1e-9) = - kotlin.math.abs(a - b) < eps - -println(almostEqual(0.1 + 0.2, 0.3)) // true -``` - -### NaN и бесконечности -```kotlin -val nan = Double.NaN -println(nan == nan) // false! NaN не равен ничему, включая себя -println(nan.isNaN()) // true — единственный верный способ проверки -println(Double.POSITIVE_INFINITY) // Infinity -println(1.0.isFinite()) // true -println(0.0 == -0.0) // true -``` - -`NaN` ведёт себя по-разному в операторах и в коллекциях - это редкая, но болезненная ловушка: -```kotlin -println(Double.NaN == Double.NaN) // false — оператор -println(Double.NaN.equals(Double.NaN)) // true — метод equals -println(listOf(Double.NaN).contains(Double.NaN)) // true — через equals -``` - -## Деньги - BigDecimal -Для денег `Double` использовать нельзя: ошибки округления накапливаются. -```kotlin -import java.math.BigDecimal -import java.math.RoundingMode - -fun main() { - val a = BigDecimal("0.1") // только из строки! - val b = BigDecimal("0.2") - println(a + b) // 0.3 — точно - println(a.add(b) == BigDecimal("0.3")) // true - - val price = BigDecimal("100.555") - println(price.setScale(2, RoundingMode.HALF_UP)) // 100.56 - - // операторы перегружены, работают привычно - println(BigDecimal("10") * BigDecimal("3")) // 30 - println(BigDecimal("10").divide(BigDecimal("4"))) // 2.5 -} -``` -Создавать **только из строки**: `BigDecimal(0.1)` подхватит уже испорченное значение `Double`: -```kotlin -import java.math.BigDecimal - -fun main() { - println(BigDecimal(0.1)) // 0.1000000000000000055511151231257827... - println(BigDecimal("0.1")) // 0.1 -} -``` - -`BigDecimal.equals` сравнивает и масштаб, поэтому `2.0` и `2.00` не равны: -```kotlin -import java.math.BigDecimal - -fun main() { - println(BigDecimal("2.0") == BigDecimal("2.00")) // false - println(BigDecimal("2.0").compareTo(BigDecimal("2.00")) == 0) // true -} -``` -Для сравнения значений - `compareTo`, не `==`. - -## Математические функции -```kotlin -import kotlin.math.* - -fun main() { - println(abs(-5)) // 5 - println(max(3, 7)) // 7 - println(min(3, 7)) // 3 - println(sqrt(16.0)) // 4.0 - println(2.0.pow(10)) // 1024.0 - println(PI) // 3.141592653589793 - println(round(2.5)) // 2.0 — не 3.0! округление к чётному - println(round(3.5)) // 4.0 - println(floor(2.9)) // 2.0 - println(ceil(2.1)) // 3.0 - println(truncate(2.9)) // 2.0 - println(2.567.roundToInt()) // 3 - println(hypot(3.0, 4.0)) // 5.0 -} -``` -Пакет `kotlin.math` нужно импортировать - автоматически он не подключается. - -Обратите внимание на `round`: при ровной половине он округляет **к ближайшему чётному** (IEEE 754 round-half-even), поэтому `round(2.5)` даёт `2.0`, а `round(3.5)` - `4.0`. Если нужно привычное «половина вверх», берите `roundToInt()`: `2.5.roundToInt()` даёт `3`. - -### Ограничение диапазона -```kotlin -println(15.coerceIn(0, 10)) // 10 -println((-5).coerceIn(0, 10)) // 0 -println(5.coerceAtLeast(0)) // 5 -println(15.coerceAtMost(10)) // 10 -``` -Удобнее, чем `max(0, min(10, x))`. - -## Преобразования и потеря данных -```kotlin -import kotlin.math.roundToInt - -fun main() { - println(300.toByte()) // 44 — старшие биты отброшены - println(3.99.toInt()) // 3 — отбрасывание, не округление - println((-3.99).toInt()) // -3 - println(3.99.roundToInt()) // 4 — настоящее округление - println(Double.NaN.toInt()) // 0 — тихо! - println(1e20.toInt()) // 2147483647 — обрезано до MAX_VALUE -} -``` -`toInt()` на `Double` никогда не падает - он тихо даёт неверный результат. Для контроля используйте `roundToInt()` и проверку `isFinite()`. - -## Представление и биты -```kotlin -println(255.toString(2)) // 11111111 — в двоичной -println(255.toString(16)) // ff -println("ff".toInt(16)) // 255 -println(5.countOneBits()) // 2 — число единичных битов -println(Integer.toBinaryString(-1)) // 32 единицы -``` - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `Math.abs(x)` | `kotlin.math.abs(x)` | -> | `Math.pow(2, 10)` | `2.0.pow(10)` | -> | `Integer.MAX_VALUE` | `Int.MAX_VALUE` | -> | `Integer.parseInt(s)` | `s.toInt()` | -> | `Integer.toBinaryString(n)` | `n.toString(2)` | -> | `Math.floorMod(-1, 3)` | `(-1).mod(3)` | -> | неявное `int` → `long` | только явный `toLong()` | -> | `Math.addExact` | то же (из JDK) | -> -> Поведение переполнения, деления и IEEE 754 полностью унаследовано от JVM - здесь Kotlin ничего не меняет и не защищает. Выигрыш только в читаемости: `2.0.pow(10)` вместо `Math.pow(2, 10)`, `coerceIn` вместо вложенных `min`/`max`. -> -> Запрет неявных преобразований - реальная защита: в Java `long total = intA * intB;` переполняется до присваивания, и компилятор молчит. В Kotlin такой код не соберётся без явного `toLong()`. -> -> Сравнить: [Типы данных в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Типы%20данных.md) - -> [!warning] Подводные камни -> **`1000 * 60 * 60 * 24 * 30` переполняется** - самая частая реальная ошибка с числами. Все операнды `Int`, результат считается в `Int`, и только потом присваивается в `Long`. -> -> **`Double` для денег даёт расхождения в копейках**, которые накапливаются на больших объёмах. Только `BigDecimal` из строки или целые копейки в `Long`. -> -> **`NaN == NaN` ложно, а `listOf(NaN).contains(NaN)` истинно.** Сортировка и `distinct()` с `NaN` ведут себя неинтуитивно. -> -> **`toInt()` на `Double` обрезает, а `NaN` превращает в 0** - без единого предупреждения. -> -> **`%` для отрицательных чисел даёт отрицательный остаток.** Код `arr[(i - 1) % n]` упадёт на `i = 0`; нужен `arr[(i - 1).mod(n)]`. -> -> **`BigDecimal("2.0") != BigDecimal("2.00")`** - `equals` учитывает масштаб. Сравнивайте через `compareTo`. - -Дополнительно: [Типы данных](../Базовый%20синтаксис/Типы%20данных.md) -Далее: [Массивы](Массивы.md) diff --git a/Kotlin/Тестирование/Kotest.md b/Kotlin/Тестирование/Kotest.md deleted file mode 100644 index 460f35f..0000000 --- a/Kotlin/Тестирование/Kotest.md +++ /dev/null @@ -1,722 +0,0 @@ ---- -title: Kotest -description: Спецификации, матчеры, data-driven тесты, property-based тестирование, жизненный цикл и изоляция -tags: - - kotlin - - testing - - kotest ---- - -Kotest - полноценный фреймворк тестирования, написанный на Kotlin и для Kotlin. Он даёт три независимо применимых вещи: DSL спецификаций вместо `@Test`, богатый набор матчеров и property-based тестирование. - -```kotlin -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.string.shouldContain - -class StringSpecDemo : StringSpec({ - "длина строки" { - "абвг".length shouldBe 4 - } - "матчеры строк" { - "привет мир" shouldContain "мир" - } -}) -``` -Тест - это строка-описание и лямбда. Никаких аннотаций, никаких ограничений на имена. - -## Подключение -```kotlin -// build.gradle.kts -dependencies { - testImplementation("io.kotest:kotest-runner-junit5:6.0.4") - testImplementation("io.kotest:kotest-assertions-core:6.0.4") - testImplementation("io.kotest:kotest-property:6.0.4") -} - -kotlin { jvmToolchain(21) } - -tasks.test { useJUnitPlatform() } -``` -| Артефакт | Зачем | -|---|---| -| `kotest-runner-junit5` | движок + спецификации | -| `kotest-assertions-core` | матчеры `shouldBe` и остальные | -| `kotest-property` | property-based тестирование | -| `kotest-framework-datatest` | `withData` (входит в runner) | -| `kotest-assertions-json` | матчеры для JSON | -| `kotest-extensions-testcontainers` | интеграция с Testcontainers | - -Три части используются независимо: матчеры работают в обычных `@Test`-классах JUnit, а спецификации - без матчеров Kotest. - -## Стили спецификаций -Kotest предлагает семь стилей. Все эквивалентны по возможностям - выбор только о читаемости. - -### StringSpec - самый лаконичный -```kotlin -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.collections.shouldHaveSize - -class StringSpecStyle : StringSpec({ - "пустой список имеет размер ноль" { - emptyList() shouldHaveSize 0 - } - "сложение работает" { - (2 + 3) shouldBe 5 - } -}) -``` - -### FunSpec - с явным `test` и группами -```kotlin -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.collections.shouldHaveSize - -class FunSpecStyle : FunSpec({ - test("сложение") { - (2 + 3) shouldBe 5 - } - context("операции со списком") { - test("размер") { - listOf(1, 2) shouldHaveSize 2 - } - test("первый элемент") { - listOf(1, 2).first() shouldBe 1 - } - } -}) -``` - -### DescribeSpec - в стиле Jest и RSpec -```kotlin -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.matchers.shouldBe - -class DescribeSpecStyle : DescribeSpec({ - describe("калькулятор") { - it("складывает") { - (1 + 1) shouldBe 2 - } - context("при делении на ноль") { - it("бросает ArithmeticException") { - shouldThrow { 1 / 0 } - } - } - } -}) -``` - -### BehaviorSpec - given/when/then -```kotlin -import io.kotest.core.spec.style.BehaviorSpec -import io.kotest.matchers.collections.shouldHaveSize - -class BehaviorSpecStyle : BehaviorSpec({ - given("пустой список") { - `when`("добавляем элемент") { - then("размер становится один") { - mutableListOf().apply { add(1) } shouldHaveSize 1 - } - } - } -}) -``` -`when` - ключевое слово Kotlin, поэтому обратные кавычки обязательны. - -### Остальные три -```kotlin -import io.kotest.core.spec.style.ShouldSpec -import io.kotest.core.spec.style.WordSpec -import io.kotest.core.spec.style.FreeSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.collections.shouldHaveSize - -class ShouldSpecStyle : ShouldSpec({ - should("работать без обёрток") { - true shouldBe true - } -}) - -class WordSpecStyle : WordSpec({ - "список" When { - "пуст" should { - "иметь размер 0" { - emptyList() shouldHaveSize 0 - } - } - } -}) - -class FreeSpecStyle : FreeSpec({ - "группа" - { - "вложенный тест" { - 1 shouldBe 1 - } - } -}) -``` -| Стиль | Синтаксис теста | Когда удобен | -|---|---|---| -| `StringSpec` | `"имя" { }` | простые плоские тесты | -| `FunSpec` | `test("имя") { }`, `context { }` | универсальный выбор | -| `DescribeSpec` | `describe`, `context`, `it` | привычен после Jest | -| `BehaviorSpec` | `given`, `when`, `then` | BDD-сценарии | -| `ShouldSpec` | `should("имя") { }` | плоские тесты с акцентом на поведение | -| `WordSpec` | `"x" When { "y" should { } }` | многословные описания | -| `FreeSpec` | `"группа" - { "тест" { } }` | произвольная вложенность | - -В отчёте вложенность отображается деревом: -``` -└─ Kotest ✔ - ├─ DescribeSpecStyle ✔ - │ └─ калькулятор ✔ - │ ├─ складывает ✔ - │ └─ при делении на ноль ✔ - │ └─ бросает ArithmeticException ✔ - └─ BehaviorSpecStyle ✔ - └─ Given: пустой список ✔ - └─ When: добавляем элемент ✔ - └─ Then: размер становится один ✔ -``` - -## Матчеры -```kotlin -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.kotest.matchers.string.shouldContain -import io.kotest.matchers.string.shouldStartWith -import io.kotest.matchers.string.shouldEndWith -import io.kotest.matchers.string.shouldMatch -import io.kotest.matchers.string.shouldBeBlank -import io.kotest.matchers.collections.shouldContainExactly -import io.kotest.matchers.collections.shouldContainAll -import io.kotest.matchers.collections.shouldHaveSize -import io.kotest.matchers.collections.shouldBeSorted -import io.kotest.matchers.collections.shouldBeEmpty -import io.kotest.matchers.ints.shouldBeGreaterThan -import io.kotest.matchers.ints.shouldBeInRange -import io.kotest.matchers.doubles.shouldBeWithinPercentageOf -import io.kotest.matchers.nulls.shouldBeNull -import io.kotest.matchers.nulls.shouldNotBeNull -import io.kotest.matchers.types.shouldBeInstanceOf -import io.kotest.matchers.maps.shouldContainKey -import io.kotest.matchers.result.shouldBeSuccess - -class MatchersDemo : StringSpec({ - "равенство" { - 5 shouldBe 5 - 5 shouldNotBe 4 - } - "строки" { - "привет мир" shouldContain "мир" - "привет мир" shouldStartWith "прив" - "привет мир" shouldEndWith "мир" - "abc123" shouldMatch Regex("[a-z]+\\d+") - " ".shouldBeBlank() - } - "коллекции" { - listOf(1, 2, 3) shouldContainExactly listOf(1, 2, 3) - listOf(1, 2, 3) shouldContainAll listOf(3, 1) - listOf(1, 2) shouldHaveSize 2 - listOf(1, 2, 3).shouldBeSorted() - emptyList().shouldBeEmpty() - } - "числа" { - 5 shouldBeGreaterThan 3 - 5 shouldBeInRange 1..10 - (0.1 + 0.2).shouldBeWithinPercentageOf(0.3, 0.001) - } - "null и типы" { - null.shouldBeNull() - "есть".shouldNotBeNull() - "текст".shouldBeInstanceOf() - } - "map и Result" { - mapOf("k" to 1) shouldContainKey "k" - runCatching { 42 }.shouldBeSuccess() - } -}) -``` -| Группа | Примеры | -|---|---| -| общие | `shouldBe`, `shouldNotBe`, `shouldBeSameInstanceAs` | -| строки | `shouldContain`, `shouldMatch`, `shouldBeBlank`, `shouldHaveLength` | -| коллекции | `shouldContainExactly`, `shouldContainAll`, `shouldBeSorted`, `shouldBeUnique` | -| числа | `shouldBeGreaterThan`, `shouldBeInRange`, `shouldBeWithinPercentageOf` | -| null | `shouldBeNull`, `shouldNotBeNull` | -| типы | `shouldBeInstanceOf`, `shouldBeTypeOf` | -| map | `shouldContainKey`, `shouldContainValue` | -| `Result` | `shouldBeSuccess`, `shouldBeFailure` | -| файлы | `shouldExist`, `shouldBeADirectory` | - -Матчеры инфиксные, поэтому читаются слева направо: `actual shouldBe expected` - порядок обратный `assertEquals`, и это намеренно. - -### shouldNotBeNull даёт умное приведение -```kotlin -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.nulls.shouldNotBeNull -import io.kotest.matchers.shouldBe - -class SmartCastDemo : StringSpec({ - "после shouldNotBeNull тип известен" { - val value: String? = "текст" - value.shouldNotBeNull() - // компилятор знает, что value это String - value.length shouldBe 5 - } -}) -``` - -## Исключения -```kotlin -import io.kotest.core.spec.style.StringSpec -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.assertions.throwables.shouldThrowAny -import io.kotest.assertions.throwables.shouldNotThrowAny -import io.kotest.assertions.throwables.shouldThrowExactly -import io.kotest.matchers.shouldBe -import io.kotest.matchers.string.shouldContain - -class ExceptionsDemo : StringSpec({ - "shouldThrow возвращает исключение" { - val e = shouldThrow { - require(false) { "проверка не прошла" } - } - e.message shouldBe "проверка не прошла" - } - "shouldThrowExactly требует точный тип" { - shouldThrowExactly { error("сбой") } - } - "shouldThrowAny ловит любое" { - val e = shouldThrowAny { 1 / 0 } - e.message.shouldContain("zero") - } - "shouldNotThrowAny для успешного пути" { - shouldNotThrowAny { listOf(1).first() } - } -}) -``` -`shouldThrow` принимает подтипы, `shouldThrowExactly` - только точный тип. Разница важна для иерархий исключений. Подробнее: [Система исключений](../Исключения/Система%20исключений.md) - -## assertSoftly и inspectors -```kotlin -import io.kotest.core.spec.style.StringSpec -import io.kotest.assertions.assertSoftly -import io.kotest.inspectors.forAll -import io.kotest.inspectors.forExactly -import io.kotest.inspectors.forAtLeastOne -import io.kotest.matchers.shouldBe -import io.kotest.matchers.ints.shouldBeGreaterThan -import io.kotest.matchers.string.shouldStartWith - -data class User(val id: Int, val name: String, val age: Int) - -class SoftAndInspectors : StringSpec({ - "assertSoftly проверяет все условия" { - val user = User(1, "Иван", 30) - assertSoftly { - user.id shouldBe 1 - user.name shouldBe "Иван" - user.age shouldBeGreaterThan 18 - } - } - "assertSoftly на объекте как receiver" { - val user = User(2, "Пётр", 25) - assertSoftly(user) { - id shouldBe 2 - name shouldStartWith "П" - } - } - "inspectors проверяют элементы коллекции" { - val users = listOf(User(1, "Иван", 30), User(2, "Пётр", 25)) - users.forAll { it.age shouldBeGreaterThan 18 } - users.forExactly(1) { it.name shouldBe "Иван" } - users.forAtLeastOne { it.age shouldBeGreaterThan 28 } - } -}) -``` -`assertSoftly` собирает все провалы в одно сообщение вместо остановки на первом. Inspectors - это проверки с кванторами: `forAll`, `forNone`, `forExactly(n)`, `forAtLeastOne`, `forAny`, `forSome`. - -## Data-driven тесты -```kotlin -import io.kotest.core.spec.style.FunSpec -import io.kotest.datatest.withData -import io.kotest.matchers.shouldBe - -data class AddCase(val a: Int, val b: Int, val sum: Int) - -class DataDriven : FunSpec({ - withData( - AddCase(2, 3, 5), - AddCase(0, 0, 0), - AddCase(-1, 1, 0), - AddCase(100, -50, 50), - ) { (a, b, sum) -> - (a + b) shouldBe sum - } - - withData( - nameFn = { "строка длины ${it.length} пуста" }, - "", " ", " ", - ) { s -> - s.isBlank() shouldBe true - } - - withData( - mapOf( - "положительное" to 5, - "ноль" to 0, - "отрицательное" to -5, - ), - ) { n -> - (n * 0) shouldBe 0 - } -}) -``` -Каждый случай становится отдельным тестом в отчёте: -``` -├─ DataDriven ✔ -│ ├─ AddCase(a=2, b=3, sum=5) ✔ -│ ├─ AddCase(a=0, b=0, sum=0) ✔ -│ ├─ AddCase(a=-1, b=1, sum=0) ✔ -│ └─ AddCase(a=100, b=-50, sum=50) ✔ -``` -Имя теста берётся из `toString()` аргумента, поэтому `data class` для случаев удобнее кортежей. Форма с `Map` даёт явные имена - предпочтительна, когда `toString()` неинформативен. - -## Property-based тестирование -Вместо конкретных примеров задаётся свойство, которое должно выполняться для любых входных данных. Kotest генерирует случайные значения и проверяет свойство на каждом. - -```kotlin -import io.kotest.core.spec.style.StringSpec -import io.kotest.property.Arb -import io.kotest.property.arbitrary.int -import io.kotest.property.arbitrary.string -import io.kotest.property.arbitrary.list -import io.kotest.property.arbitrary.filter -import io.kotest.property.arbitrary.map -import io.kotest.property.checkAll -import io.kotest.property.forAll -import io.kotest.matchers.shouldBe -import io.kotest.matchers.collections.shouldHaveSize - -class PropertyBased : StringSpec({ - "сложение коммутативно" { - checkAll { a, b -> - (a + b) shouldBe (b + a) - } - } - "reverse дважды даёт исходную строку" { - checkAll(Arb.string()) { s -> - s.reversed().reversed() shouldBe s - } - } - "forAll с булевым результатом" { - forAll(Arb.int(1..100), Arb.int(1..100)) { a, b -> - a + b > a - } - } - "sorted не меняет размер" { - checkAll(Arb.list(Arb.int(), 0..20)) { l -> - l.sorted() shouldHaveSize l.size - } - } - "производные генераторы" { - checkAll(Arb.int().filter { it != 0 }) { n -> - (n / n) shouldBe 1 - } - checkAll(Arb.int(1..100).map { it * 2 }) { even -> - (even % 2) shouldBe 0 - } - } -}) -``` -Разница `checkAll` и `forAll`: первый ожидает утверждений внутри лямбды, второй - возврата `Boolean`. - -| Генератор | Что даёт | -|---|---| -| `Arb.int()`, `Arb.int(1..100)` | числа, с диапазоном | -| `Arb.string()`, `Arb.string(5..10)` | строки | -| `Arb.boolean()`, `Arb.double()`, `Arb.long()` | примитивы | -| `Arb.list(gen, 0..10)`, `Arb.set(gen)` | коллекции | -| `Arb.enum()` | значения enum | -| `Arb.of(a, b, c)` | из фиксированного набора | -| `Arb.bind(g1, g2) { a, b -> ... }` | составные объекты | -| `gen.filter { }`, `gen.map { }` | производные | -| `Arb.int().orNull()` | с вероятностью `null` | - -### Shrinking -Главная ценность property-based тестов - автоматический поиск минимального контрпримера. При провале Kotest уменьшает входные данные: -``` -Attempting to shrink arg 960 -Shrink #1: 0 pass -Shrink #2: 1 pass -Shrink #3: 320 pass -Shrink #4: 480 pass -Shrink #5: 640 fail -... -Shrink result (after 124 shrinks) => 500 -Property failed after 3 attempts -Repeat this test by using seed 2234853214744756250 -``` -Вместо случайного `960` сообщение называет точную границу `500`. Seed позволяет воспроизвести тот же набор значений - его стоит сохранять в тесте при отладке: -```kotlin -import io.kotest.core.spec.style.StringSpec -import io.kotest.property.Arb -import io.kotest.property.PropTestConfig -import io.kotest.property.arbitrary.int -import io.kotest.property.checkAll -import io.kotest.matchers.ints.shouldBeGreaterThanOrEqual - -class SeedDemo : StringSpec({ - "воспроизводимый прогон" { - checkAll( - PropTestConfig(seed = 2234853214744756250, iterations = 50), - Arb.int(0..1000), - ) { n -> - n shouldBeGreaterThanOrEqual 0 - } - } -}) -``` -По умолчанию - 1000 итераций; `PropTestConfig(iterations = n)` меняет их число. - -## Жизненный цикл -```kotlin -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe - -class LifecycleDemo : FunSpec({ - var counter = 0 - - beforeSpec { counter = 100 } - beforeTest { counter += 1 } - afterTest { counter -= 1 } - afterSpec { counter = 0 } - - test("первый") { counter shouldBe 101 } - test("второй") { counter shouldBe 101 } -}) -``` -| Коллбэк | Когда | -|---|---| -| `beforeSpec` | один раз перед всеми тестами класса | -| `beforeTest` | перед каждым тестом | -| `beforeContainer` | перед группой (`context`, `describe`) | -| `afterTest` | после каждого теста | -| `afterSpec` | один раз после всех тестов | -| `beforeEach` / `afterEach` | только листовые тесты | -| `afterProject` | после всего прогона (в ProjectConfig) | - -Коллбэки - лямбды внутри конструктора, а не методы с аннотациями, поэтому `@JvmStatic` и `companion object` не нужны - в отличие от JUnit 5. - -## Изоляция - главная ловушка -По умолчанию Kotest создаёт **один** экземпляр спецификации на все тесты, и состояние протекает: -```kotlin -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe - -class DefaultIsolation : StringSpec({ - var state = 0 - - "первый" { - state += 1 - state shouldBe 1 - } - "второй видит изменения первого" { - state += 1 - state shouldBe 2 // не 1! - } -}) -``` -Это противоположно JUnit, где экземпляр создаётся на каждый тест. Исправляется явно: -```kotlin -import io.kotest.core.spec.style.StringSpec -import io.kotest.core.spec.IsolationMode -import io.kotest.matchers.shouldBe - -class IsolatedSpec : StringSpec({ - isolationMode = IsolationMode.InstancePerLeaf - var state = 0 - - "первый видит нулевое состояние" { - state += 1 - state shouldBe 1 - } - "второй тоже видит нулевое состояние" { - state += 1 - state shouldBe 1 - } -}) -``` -| Режим | Поведение | -|---|---| -| `SingleInstance` (по умолчанию) | один экземпляр на спецификацию | -| `InstancePerLeaf` | новый экземпляр на каждый листовой тест | -| `InstancePerRoot` | новый экземпляр на каждый корневой тест | - -`InstancePerLeaf` даёт поведение, привычное после JUnit, но замедляет прогон: конструктор спецификации выполняется заново на каждый тест. - -## Конфигурация тестов -```kotlin -import io.kotest.core.spec.style.FunSpec -import io.kotest.core.annotation.Ignored -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import kotlin.time.Duration.Companion.seconds - -class ConfigDemo : FunSpec({ - test("повторяется трижды").config(invocations = 3, timeout = 2.seconds) { - (1 + 1) shouldBe 2 - } - test("отключён").config(enabled = false) { - error("не выполняется") - } - test("по условию").config(enabledIf = { System.getenv("CI") == null }) { - true shouldBe true - } - test("помечен тегом").config(tags = setOf(Slow)) { - true shouldBe true - } -}) - -object Slow : io.kotest.core.Tag() - -@Ignored -class IgnoredSpec : StringSpec({ - "никогда не запускается" { error("нет") } -}) -``` -| Параметр `.config()` | Что делает | -|---|---| -| `invocations` | повторить N раз | -| `timeout` | ограничение времени | -| `enabled` | включить/выключить | -| `enabledIf` | условие по лямбде | -| `tags` | теги для фильтрации | -| `retries`, `retryDelay` | повторы при провале | -| `severity` | уровень важности для фильтрации | -| `coroutineDebugProbes` | трассировка корутин при зависании | - -## Конфигурация проекта -```kotlin -import io.kotest.core.config.AbstractProjectConfig -import io.kotest.core.spec.IsolationMode -import io.kotest.core.names.DuplicateTestNameMode -import io.kotest.engine.concurrency.SpecExecutionMode - -class ProjectConfig : AbstractProjectConfig() { - override val isolationMode = IsolationMode.InstancePerLeaf - override val specExecutionMode = SpecExecutionMode.LimitedConcurrency(4) - override val duplicateTestNameMode = DuplicateTestNameMode.Error - override val globalAssertSoftly = true -} -``` -Файл с классом, наследующим `AbstractProjectConfig`, Kotest находит автоматически. Это единственное место, где можно задать режим изоляции для всего проекта - иначе придётся повторять в каждой спецификации. - -| Свойство | Что задаёт | -|---|---| -| `isolationMode` | изоляция для всех спецификаций | -| `specExecutionMode` | `Sequential`, `Concurrent`, `LimitedConcurrency(n)` для спецификаций | -| `testExecutionMode` | то же для тестов внутри спецификации | -| `duplicateTestNameMode` | `Warn` (по умолчанию), `Error`, `Silent` | -| `globalAssertSoftly` | оборачивать каждый тест в `assertSoftly` | -| `timeout`, `invocationTimeout`, `projectTimeout` | таймауты | - -## Фильтрация и запуск -``` -./gradlew test -./gradlew test --tests "com.example.CacheSpec" -./gradlew test -Dkotest.tags="!Slow" -./gradlew test -Dkotest.tags="Integration & !Slow" -``` -```kotlin -// build.gradle.kts -tasks.test { - useJUnitPlatform() - systemProperty("kotest.tags", "!Slow") -} -``` -Движок Kotest работает поверх JUnit Platform, поэтому Gradle, IDE и CI видят тесты как обычные - отчёты, фильтры и интеграции работают без настройки. - -## Kotest и JUnit вместе -Матчеры Kotest не требуют его движка: -```kotlin -import kotlin.test.Test -import io.kotest.matchers.shouldBe -import io.kotest.matchers.string.shouldContain -import io.kotest.matchers.collections.shouldHaveSize - -class MixedTest { - @Test - fun `матчеры Kotest в обычном тесте JUnit`() { - "привет мир" shouldContain "мир" - listOf(1, 2) shouldHaveSize 2 - (2 + 3) shouldBe 5 - } -} -``` -Это разумный компромисс: читаемые матчеры без перехода на другой фреймворк. Достаточно `kotest-assertions-core`, `kotest-runner-junit5` не нужен. - -## Когда брать Kotest -**Стоит**, если нужны: вложенные спецификации с группировкой, data-driven тесты (`withData` короче `@ParameterizedTest`), property-based тестирование, богатые матчеры, тесты для Multiplatform. - -**Не стоит**, если: проект небольшой и `kotlin.test` хватает, команда привыкла к JUnit, важна минимальная зависимость. Подробнее: [kotlin.test и JUnit 5](kotlin.test%20и%20JUnit%205.md) - -Промежуточный вариант - `kotest-assertions-core` поверх JUnit: самая заметная часть выгоды при минимальных изменениях. - -> [!info] Отличия от Java -> | Java | Kotest | -> |---|---| -> | JUnit 5 + AssertJ | один фреймворк | -> | `@Test void x()` | `"описание" { }` | -> | `assertThat(x).isEqualTo(y)` | `x shouldBe y` | -> | `@ParameterizedTest` + `@CsvSource` | `withData(...)` | -> | jqwik для property-based | `kotest-property` встроен | -> | `@Nested static class` | вложенность через DSL | -> | `@BeforeAll static` | `beforeSpec { }` | -> | новый экземпляр на тест | один экземпляр по умолчанию | -> -> **DSL спецификаций возможен из-за лямбд с receiver.** Spock даёт похожий синтаксис в Groovy, но ценой динамической типизации и отдельного языка. Kotest строит DSL на типизированных лямбдах с получателем: `StringSpec({ ... })` - это конструктор, принимающий лямбду, внутри которой `String.invoke(block)` - оператор. Весь синтаксис - обычный Kotlin с проверкой типов. Подробнее: [DSL](../Функциональный%20Kotlin/DSL.md) -> -> **Матчеры читаются обратно `assertEquals`.** `x shouldBe y` ставит фактическое значение первым, `assertEquals(expected, actual)` - ожидаемое. Это следствие инфиксной формы: по-английски читается как предложение. При переходе с JUnit порядок приходится переучивать, зато перепутать его труднее - неправильный порядок звучит неестественно. -> -> **Property-based тестирование встроено, а не отдельная библиотека.** В Java это jqwik или QuickTheories - сторонние инструменты со своим стилем тестов. В Kotest `checkAll` работает внутри любой спецификации рядом с обычными тестами, и shrinking находит минимальный контрпример автоматически. Это снижает порог: добавить одно свойство к существующим тестам ничего не стоит. -> -> **Изоляция устроена противоположно JUnit, и это главный источник ошибок.** JUnit создаёт новый экземпляр тестового класса на каждый тест - изоляция по умолчанию. Kotest создаёт один экземпляр на спецификацию, потому что спецификация - это лямбда в конструкторе, и её повторное выполнение стоит дороже. Результат: переменные в теле спецификации разделяются между тестами, и порядок начинает иметь значение. Решается `IsolationMode.InstancePerLeaf`, но включать нужно осознанно. -> -> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md) - -> [!warning] Подводные камни -> **Изоляции по умолчанию нет.** `var` в теле спецификации разделяется между всеми тестами, и второй тест видит изменения первого. Это противоположно JUnit и ломает тесты неочевидным образом - особенно при параллельном запуске. Задавайте `IsolationMode.InstancePerLeaf` в `ProjectConfig`, если не уверены. -> -> **Kotest собран под JVM target 11.** Компиляция с дефолтным `-jvm-target 1.8` даёт ошибку «cannot inline bytecode built with JVM target 11 into bytecode that is being built with JVM target 1.8». Нужен `jvmToolchain(11)` или выше - для Gradle это обычно уже так, но при ручной компиляции флаг обязателен. Подробнее: [kotlinc](../Инструментарий/kotlinc.md) -> -> **`withData` даёт дублирующиеся имена при похожих значениях.** Строки `" "` и `" "` после обработки дают одинаковое имя теста, Kotest выводит «WARN: Duplicated test name» и добавляет префикс `(1)`. Используйте `nameFn = { ... }` или форму с `Map`, где имена заданы явно. -> -> **`when` требует обратных кавычек в `BehaviorSpec`.** `when("...")` не скомпилируется - это ключевое слово. Нужно `` `when`("...") ``. В `WordSpec` аналогично используется `When` с заглавной буквы. -> -> **`shouldBe` на массивах сравнивает ссылки.** Как и `assertEquals`: `intArrayOf(1) shouldBe intArrayOf(1)` провалится. Нужен `shouldContainExactly` или преобразование в список. Подробнее: [Массивы](../Синтаксис%20и%20типы/Работа%20с%20данными/Массивы.md) -> -> **`checkAll` по умолчанию делает 1000 итераций.** Для медленной проверки (обращение к БД, сложный алгоритм) это минуты. Задавайте `PropTestConfig(iterations = 100)` явно. -> -> **Генератор может не находить контрпример.** `Arb.int()` редко выдаёт конкретное значение вроде `Int.MAX_VALUE`, хотя Kotest и подмешивает граничные случаи. Если свойство ломается только на краях, добавьте их явно через `Arb.of(...)` или `Arb.int().withEdgecases(...)`. -> -> **Провал property-теста без сохранённого seed невоспроизводим.** Сообщение содержит seed - его нужно скопировать в `PropTestConfig(seed = ...)`, иначе следующий прогон сгенерирует другие значения, и ошибка «исчезнет». -> -> **`shouldThrow` ловит подтипы.** `shouldThrow` пройдёт на любом исключении, включая `NullPointerException` от ошибки в самом тесте. Указывайте конкретный тип или `shouldThrowExactly`. -> -> **`assertSoftly` не ловит исключения.** Он собирает провалы матчеров, но `NullPointerException` внутри блока прервёт тест сразу, и остальные проверки не выполнятся. -> -> **Вложенные `context` без тестов внутри молча пропускаются.** Группа без листовых тестов не считается провалом - она просто не появляется в отчёте, и легко не заметить, что проверки не выполняются. -> -> **Параллелизм спецификаций и тестов настраивается раздельно.** `specExecutionMode` влияет только на спецификации; тесты внутри одной спецификации по умолчанию идут последовательно, пока не задан `testExecutionMode = TestExecutionMode.Concurrent`. Старые `parallelism` и `.config(threads = n)` в Kotest 6 убраны - код из статей по версии 5 не скомпилируется. -> -> **Две версии Kotest в classpath дают неочевидные ошибки.** Артефакты `kotest-runner-junit5`, `kotest-assertions-core` и `kotest-property` должны быть одной версии; расхождение проявляется как `NoSuchMethodError` в рантайме. Держите версию в `libs.versions.toml`. Подробнее: [Gradle и Kotlin DSL](../Инструментарий/Gradle%20и%20Kotlin%20DSL.md) -> -> **`ProjectConfig` находится по наследованию, а не по имени.** Класс должен наследовать `AbstractProjectConfig` и лежать в тестовом source set; имя значения не имеет, но два таких класса в проекте дают неопределённое поведение. - -Далее: [MockK](MockK.md) diff --git a/Kotlin/Тестирование/MockK.md b/Kotlin/Тестирование/MockK.md deleted file mode 100644 index 511c177..0000000 --- a/Kotlin/Тестирование/MockK.md +++ /dev/null @@ -1,780 +0,0 @@ ---- -title: MockK -description: Моки, стабы, spyk, проверка вызовов, мокирование object, companion, функций верхнего уровня и suspend -tags: - - kotlin - - testing - - mockk ---- - -MockK - библиотека моков, написанная для Kotlin. Главное её отличие от Mockito в том, что финальные классы, `object`, функции верхнего уровня и `suspend`-функции мокаются без дополнительной настройки. - -```kotlin -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import kotlin.test.Test -import kotlin.test.assertEquals - -data class User(val id: Int, val name: String) - -interface UserRepo { - fun findById(id: Int): User? -} - -class FirstMockTest { - @Test - fun `мок возвращает заданное значение`() { - val repo = mockk() - every { repo.findById(1) } returns User(1, "Иван") - - assertEquals("Иван", repo.findById(1)?.name) - verify { repo.findById(1) } - } -} -``` -Три шага: `mockk()` создаёт заглушку, `every { } returns` задаёт поведение, `verify { }` проверяет вызов. - -## Подключение -```kotlin -// build.gradle.kts -dependencies { - testImplementation("io.mockk:mockk:1.14.6") -} - -tasks.test { useJUnitPlatform() } -``` -| Артефакт | Зачем | -|---|---| -| `io.mockk:mockk` | всё для JVM-тестов, включая JUnit 5 extension | -| `io.mockk:mockk-android` | инструментальные тесты Android | -| `io.mockk:mockk-agent-jvm` | подтягивается транзитивно, отдельно не нужен | - -Зависимость одна - `mockk` тянет `byte-buddy` и `objenesis` сам. - -## Создание моков -```kotlin -import io.mockk.every -import io.mockk.mockk -import kotlin.test.Test -import kotlin.test.assertEquals - -interface Repo { - fun count(): Int - fun name(): String - fun flag(): Boolean - fun items(): List - fun clear() -} - -class MockModesTest { - @Test - fun `строгий мок требует стаба на каждый вызов`() { - val repo = mockk() - every { repo.count() } returns 7 - assertEquals(7, repo.count()) - // repo.name() бросил бы MockKException - } - - @Test - fun `relaxed возвращает значения по умолчанию`() { - val repo = mockk(relaxed = true) - assertEquals(0, repo.count()) - assertEquals("", repo.name()) - assertEquals(false, repo.flag()) - assertEquals(emptyList(), repo.items()) - } - - @Test - fun `relaxUnitFun ослабляет только Unit-методы`() { - val repo = mockk(relaxUnitFun = true) - repo.clear() // ок - // repo.count() всё ещё бросит MockKException - } - - @Test - fun `имя мока попадает в сообщение об ошибке`() { - val repo = mockk(name = "репозиторий") - val message = runCatching { repo.count() }.exceptionOrNull()?.message - assertEquals(true, message?.contains("репозиторий")) - } -} -``` -| Форма | Поведение на нестабованном вызове | -|---|---| -| `mockk()` | `MockKException: no answer found` | -| `mockk(relaxed = true)` | значение по умолчанию: `0`, `""`, `false`, пустая коллекция, мок для ссылочного типа | -| `mockk(relaxUnitFun = true)` | `Unit`-методы проходят, остальные бросают | -| `mockk(name = "x")` | строгий, но с понятным именем в ошибке | - -`relaxed = true` для ссылочных типов возвращает не `null`, а дочерний мок - `repo.findById(1)` даст `User(child of #1#2)`. Это удобно для цепочек, но скрывает забытые стабы. - -## Задание поведения -```kotlin -import io.mockk.every -import io.mockk.just -import io.mockk.Runs -import io.mockk.mockk -import kotlin.test.Test -import kotlin.test.assertEquals - -data class User(val id: Int, val name: String) - -interface UserRepo { - fun findById(id: Int): User? - fun count(): Int - fun deleteAll() -} - -class AnswersTest { - @Test - fun `returns и returnsMany`() { - val repo = mockk() - every { repo.count() } returnsMany listOf(1, 2, 3) - // последнее значение повторяется для всех дальнейших вызовов - assertEquals(listOf(1, 2, 3, 3), List(4) { repo.count() }) - } - - @Test - fun `answers видит аргументы вызова`() { - val repo = mockk() - every { repo.findById(any()) } answers { - User(firstArg(), "пользователь ${firstArg()}") - } - assertEquals("пользователь 7", repo.findById(7)?.name) - } - - @Test - fun `throws`() { - val repo = mockk() - every { repo.count() } throws IllegalStateException("нет соединения") - val e = runCatching { repo.count() }.exceptionOrNull() - assertEquals("нет соединения", e?.message) - } - - @Test - fun `just Runs для Unit`() { - val repo = mockk() - every { repo.deleteAll() } just Runs - repo.deleteAll() - } -} -``` -| Инфикс | Что делает | -|---|---| -| `returns x` | всегда одно значение | -| `returnsMany listOf(...)` | значения по порядку, последнее повторяется | -| `answers { }` | лямбда с доступом к `firstArg()`, `secondArg()`, `arg(n)`, `call` | -| `answersMany listOf(...)` | список ответов | -| `throws e` | бросает исключение | -| `throwsMany listOf(...)` | исключения по порядку | -| `just Runs` | для `Unit`-функций | -| `propertyType` / `nullablePropertyType` | уточнение типа для дженериков | - -`returnsMany` после исчерпания списка **повторяет последний элемент**, а не бросает - из четырёх вызовов `[1, 2, 3]` даст `1 2 3 3`. - -## Сопоставители аргументов -```kotlin -import io.mockk.every -import io.mockk.mockk -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull - -data class User(val id: Int, val name: String) - -interface UserRepo { - fun findById(id: Int): User? -} - -class MatchersTest { - @Test - fun `сопоставители по условию`() { - val repo = mockk() - every { repo.findById(more(10)) } returns User(0, "большой") - every { repo.findById(range(1, 9)) } returns User(0, "малый") - every { repo.findById(eq(0)) } returns null - - assertEquals("большой", repo.findById(50)?.name) - assertEquals("малый", repo.findById(5)?.name) - assertNull(repo.findById(0)) - } -} -``` -| Сопоставитель | Условие | -|---|---| -| `any()` | любой аргумент | -| `eq(x)` / `neq(x)` | равен / не равен по `equals` | -| `refEq(x)` | та же ссылка | -| `isNull()` / `isNull(inverse = true)` | `null` или нет | -| `ofType()` | конкретный тип | -| `more(x)` / `less(x)` | больше / меньше, с `andEquals` для нестрогого | -| `range(a, b)` | в диапазоне | -| `match { it.name.isNotBlank() }` | произвольный предикат | -| `capture(slot)` | захват значения | -| `withArg { assert(...) }` | проверка внутри `verify` | - -Важно: позже заданный стаб **перекрывает** ранее заданный для тех же аргументов. `every { repo.findById(1) } returns a` после `every { repo.findById(any()) } returns b` даёт `a` для `1`; в обратном порядке `any()` перехватит и `1`. - -## Захват аргументов -```kotlin -import io.mockk.every -import io.mockk.mockk -import io.mockk.slot -import io.mockk.verify -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -data class User(val id: Int, val name: String) - -interface UserRepo { - fun findById(id: Int): User? - fun save(user: User): User -} - -class CaptureTest { - @Test - fun `slot хранит последний аргумент`() { - val repo = mockk() - val saved = slot() - every { repo.save(capture(saved)) } answers { saved.captured } - - repo.save(User(5, "Анна")) - assertEquals("Анна", saved.captured.name) - } - - @Test - fun `список собирает все вызовы`() { - val repo = mockk(relaxed = true) - val ids = mutableListOf() - every { repo.findById(capture(ids)) } returns null - - repo.findById(1) - repo.findById(2) - repo.findById(3) - assertEquals(listOf(1, 2, 3), ids) - } - - @Test - fun `withArg проверяет прямо в verify`() { - val repo = mockk(relaxed = true) - repo.save(User(9, "Олег")) - verify { repo.save(withArg { assertTrue(it.name.startsWith("О")) }) } - } -} -``` -`slot()` хранит одно значение - обращение к `captured` до вызова даёт `IllegalStateException`. Для всех вызовов нужен `MutableList`. `withArg` короче, когда захваченное значение нужно только для одной проверки. - -## Проверка вызовов -```kotlin -import io.mockk.confirmVerified -import io.mockk.mockk -import io.mockk.verify -import io.mockk.verifyOrder -import io.mockk.verifySequence -import kotlin.test.Test - -data class User(val id: Int, val name: String) - -interface UserRepo { - fun findById(id: Int): User? - fun save(user: User): User - fun findAll(): List -} - -class VerifyTest { - @Test - fun `количество вызовов`() { - val repo = mockk(relaxed = true) - repo.findById(1) - repo.findById(1) - - verify(exactly = 2) { repo.findById(1) } - verify(atLeast = 1) { repo.findById(any()) } - verify(exactly = 0) { repo.findAll() } - } - - @Test - fun `порядок и полная последовательность`() { - val repo = mockk(relaxed = true) - repo.findById(1) - repo.save(User(1, "a")) - repo.findAll() - - // verifyOrder допускает вызовы между проверяемыми - verifyOrder { - repo.findById(1) - repo.findAll() - } - // verifySequence требует ровно эти вызовы и только их - verifySequence { - repo.findById(1) - repo.save(any()) - repo.findAll() - } - } - - @Test - fun `confirmVerified ловит непроверенные вызовы`() { - val repo = mockk(relaxed = true) - repo.findById(1) - repo.findAll() - - verify { repo.findById(1) } - // findAll не проверен - confirmVerified бросит AssertionError - val e = runCatching { confirmVerified(repo) }.exceptionOrNull() - kotlin.test.assertTrue(e is AssertionError) - } -} -``` -| Функция | Что требует | -|---|---| -| `verify { }` | хотя бы один подходящий вызов | -| `verify(exactly = n)` | ровно `n` вызовов | -| `verify(atLeast = n)`, `verify(atMost = n)` | границы | -| `verify(exactly = 0)` | вызова не было | -| `verifyAll { }` | все перечисленные вызовы и ничего больше, порядок свободный | -| `verifyOrder { }` | перечисленные в этом порядке, между ними допустимы другие | -| `verifySequence { }` | ровно эти вызовы, ровно в этом порядке | -| `confirmVerified(mock)` | все вызовы мока были проверены | -| `excludeRecords { }` | исключить вызовы из учёта `confirmVerified` | -| `checkUnnecessaryStub(mock)` | ни один стаб не остался неиспользованным | - -`confirmVerified` и `checkUnnecessaryStub` бросают `AssertionError`, то есть выглядят как обычный провал утверждения, а не как ошибка библиотеки. - -## spyk - частичный мок -```kotlin -import io.mockk.every -import io.mockk.mockk -import io.mockk.spyk -import kotlin.test.Test -import kotlin.test.assertEquals - -data class User(val id: Int, val name: String) - -interface UserRepo { - fun findById(id: Int): User? -} - -open class Service(private val repo: UserRepo) { - open fun greet(id: Int): String = "Привет, ${repo.findById(id)?.name ?: "гость"}" - open fun helper(): Int = 42 - fun usesHelper(): Int = helper() * 2 -} - -class SpykTest { - @Test - fun `spyk выполняет настоящий код пока метод не застабован`() { - val repo = mockk() - every { repo.findById(1) } returns User(1, "Иван") - val service = spyk(Service(repo)) - - assertEquals("Привет, Иван", service.greet(1)) - - every { service.helper() } returns 10 - assertEquals(20, service.usesHelper()) - } -} -``` -`spyk` оборачивает **существующий** объект: незастабованные методы работают как раньше, застабованные подменяются. Это нужно, когда тестируется класс, часть методов которого хочется изолировать. - -Приватные методы требуют отдельного флага: -```kotlin -import io.mockk.every -import io.mockk.spyk -import kotlin.test.Test -import kotlin.test.assertEquals - -class WithPrivate { - private fun secret(): Int = 1 - fun public(): Int = secret() * 10 -} - -class PrivateCallsTest { - @Test - fun `приватный метод стабуется по имени`() { - val spy = spyk(WithPrivate(), recordPrivateCalls = true) - every { spy["secret"]() } returns 5 - assertEquals(50, spy.public()) - } -} -``` -Доступ по строковому имени не проверяется компилятором - при переименовании метода тест молча перестанет стабовать нужное. Это крайняя мера; обычно приватный метод стоит вынести в отдельный класс. - -## Kotlin-специфичное мокирование -Здесь MockK даёт то, чего Mockito без расширений не умеет. - -### Финальные классы -```kotlin -import io.mockk.every -import io.mockk.mockk -import kotlin.test.Test -import kotlin.test.assertEquals - -class FinalRepo { - fun load(id: Int): String = "реальный $id" -} - -class FinalClassTest { - @Test - fun `финальный класс мокается без настройки`() { - val repo = mockk() - every { repo.load(1) } returns "мок" - assertEquals("мок", repo.load(1)) - } -} -``` -В Kotlin классы финальны по умолчанию, поэтому это основной случай, а не исключение. Подробнее: [Наследование](../ООП/Наследование.md) - -### object и companion object -```kotlin -import io.mockk.every -import io.mockk.mockkObject -import io.mockk.unmockkObject -import kotlin.test.Test -import kotlin.test.assertEquals - -object Config { - fun url(): String = "prod://db" -} - -class WithCompanion { - companion object { - fun create(): String = "реальный" - } -} - -class ObjectMockTest { - @Test - fun `mockkObject подменяет синглтон`() { - mockkObject(Config) - every { Config.url() } returns "test://mem" - assertEquals("test://mem", Config.url()) - - unmockkObject(Config) - assertEquals("prod://db", Config.url()) - } - - @Test - fun `companion мокается как объект`() { - mockkObject(WithCompanion.Companion) - every { WithCompanion.create() } returns "мок" - assertEquals("мок", WithCompanion.create()) - unmockkObject(WithCompanion.Companion) - } -} -``` -`mockkObject` оборачивает **реальный** экземпляр - это spy, а не чистый мок: незастабованные методы продолжают работать. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md) - -### Функции верхнего уровня и расширения -```kotlin -import io.mockk.every -import io.mockk.mockkStatic -import io.mockk.unmockkStatic -import kotlin.test.Test -import kotlin.test.assertEquals - -fun topLevel(x: Int): Int = x * 2 - -fun String.shout(): String = "$this!" - -class StaticMockTest { - @Test - fun `функция верхнего уровня`() { - mockkStatic(::topLevel) - every { topLevel(any()) } returns 100 - assertEquals(100, topLevel(1)) - - unmockkStatic(::topLevel) - assertEquals(2, topLevel(1)) - } - - @Test - fun `функция-расширение`() { - mockkStatic(String::shout) - every { any().shout() } returns "замок" - assertEquals("замок", "привет".shout()) - - unmockkStatic(String::shout) - assertEquals("привет!", "привет".shout()) - } -} -``` -Обе компилируются в статические методы класса файла, поэтому используется `mockkStatic`. Ссылка на функцию (`::topLevel`) предпочтительнее строки с именем класса - её проверяет компилятор. Подробнее: [Kotlin в байт-коде](../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md) - -### Конструкторы -```kotlin -import io.mockk.every -import io.mockk.mockkConstructor -import io.mockk.unmockkConstructor -import kotlin.test.Test -import kotlin.test.assertEquals - -class FinalRepo { - fun load(id: Int): String = "реальный $id" -} - -class ConstructorMockTest { - @Test - fun `mockkConstructor перехватывает создание объектов`() { - mockkConstructor(FinalRepo::class) - every { anyConstructed().load(any()) } returns "из конструктора" - - assertEquals("из конструктора", FinalRepo().load(5)) - - unmockkConstructor(FinalRepo::class) - assertEquals("реальный 5", FinalRepo().load(5)) - } -} -``` -Это обходной путь для кода, создающего зависимости внутри себя. Нужда в `mockkConstructor` - сигнал, что зависимость стоит передавать через конструктор. - -### suspend-функции -```kotlin -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.just -import io.mockk.Runs -import io.mockk.mockk -import kotlinx.coroutines.delay -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals - -interface AsyncRepo { - suspend fun fetch(id: Int): String - suspend fun saveAll(items: List) -} - -class SuspendMockTest { - @Test - fun `coEvery и coVerify`() = runTest { - val repo = mockk() - coEvery { repo.fetch(1) } returns "данные" - coEvery { repo.saveAll(any()) } just Runs - - assertEquals("данные", repo.fetch(1)) - repo.saveAll(listOf("a")) - - coVerify(exactly = 1) { repo.fetch(1) } - coVerify { repo.saveAll(listOf("a")) } - } - - @Test - fun `coAnswers может приостанавливаться`() = runTest { - val repo = mockk() - coEvery { repo.fetch(any()) } coAnswers { - delay(1000) - "после задержки" - } - assertEquals("после задержки", repo.fetch(1)) - } -} -``` -Для `suspend`-функций нужны `coEvery`, `coVerify`, `coAnswers` - обычные `every` и `verify` не могут вызвать приостанавливаемую лямбду. `delay` внутри `coAnswers` под `runTest` виртуализируется и не замедляет тест. Подробнее: [Тестирование корутин](Тестирование%20корутин.md) - -## Аннотации -```kotlin -import io.mockk.MockKAnnotations -import io.mockk.clearAllMocks -import io.mockk.every -import io.mockk.impl.annotations.InjectMockKs -import io.mockk.impl.annotations.MockK -import io.mockk.impl.annotations.RelaxedMockK -import io.mockk.verify -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import kotlin.test.Test -import kotlin.test.assertTrue - -data class User(val id: Int, val name: String) -interface UserRepo { fun findById(id: Int): User? } -interface Mailer { fun send(to: String, body: String) } - -class Notifier(private val repo: UserRepo, private val mailer: Mailer) { - fun notify(id: Int): Boolean { - val user = repo.findById(id) ?: return false - mailer.send(user.name, "привет") - return true - } -} - -class AnnotationsTest { - @MockK lateinit var repo: UserRepo - @RelaxedMockK lateinit var mailer: Mailer - @InjectMockKs lateinit var notifier: Notifier - - @BeforeEach fun setUp() = MockKAnnotations.init(this) - @AfterEach fun tearDown() = clearAllMocks() - - @Test - fun `зависимости подставляются автоматически`() { - every { repo.findById(1) } returns User(1, "Иван") - - assertTrue(notifier.notify(1)) - verify { mailer.send("Иван", "привет") } - } -} -``` -`@InjectMockKs` ищет моки по типу и передаёт их в конструктор. Это избавляет от ручного `Notifier(repo, mailer)` - полезно, когда конструктор большой и часто меняется. - -JUnit 5 extension делает `init` сам: -```kotlin -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.junit5.MockKExtension -import org.junit.jupiter.api.extension.ExtendWith -import kotlin.test.Test -import kotlin.test.assertEquals - -data class User(val id: Int, val name: String) -interface UserRepo { fun findById(id: Int): User? } - -@ExtendWith(MockKExtension::class) -class ExtensionTest { - @MockK lateinit var repo: UserRepo - - @Test - fun `extension инициализирует моки`() { - every { repo.findById(2) } returns User(2, "Пётр") - assertEquals("Пётр", repo.findById(2)?.name) - } -} -``` -| Аннотация | Что делает | -|---|---| -| `@MockK` | строгий мок | -| `@RelaxedMockK` | `relaxed = true` | -| `@SpyK` | `spyk` существующего значения | -| `@InjectMockKs` | создаёт объект, подставляя моки | -| `@OverrideMockKs` | как `@InjectMockKs`, но перезаписывает уже заданные поля | -| `@ExtendWith(MockKExtension::class)` | автоматический `init` и `unmockk` | - -`MockKExtension` также понимает `@MockKExtension.ConfirmVerification` и `@MockKExtension.CheckUnnecessaryStub` на классе - включает соответствующие проверки для всех тестов. - -## Сброс состояния -```kotlin -import io.mockk.clearMocks -import io.mockk.every -import io.mockk.mockk -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -data class User(val id: Int, val name: String) -interface UserRepo { fun findAll(): List } - -class ClearTest { - @Test - fun `clearMocks снимает стабы и историю вызовов`() { - val repo = mockk() - every { repo.findAll() } returns listOf(User(1, "x")) - assertEquals(1, repo.findAll().size) - - clearMocks(repo) - // стаб снят - вызов снова бросает - val e = runCatching { repo.findAll() }.exceptionOrNull() - assertTrue(e != null) - } -} -``` -| Функция | Что сбрасывает | -|---|---| -| `clearMocks(a, b)` | стабы и записанные вызовы указанных моков | -| `clearMocks(m, answers = false)` | только историю вызовов, стабы остаются | -| `clearAllMocks()` | все моки, включая `mockkObject` и `mockkStatic` | -| `unmockkObject(obj)`, `unmockkStatic(...)`, `unmockkConstructor(...)` | снимают конкретную подмену | -| `unmockkAll()` | снимает все глобальные подмены | - -Статические подмены и `mockkObject` **глобальны на весь JVM-процесс**, поэтому их обязательно снимать: иначе следующий тест в том же прогоне получит подменённый `object`. - -## Когда MockK, когда нет -**Стоит**, если: код на Kotlin с финальными классами, есть `object` и функции верхнего уровня, нужны моки `suspend`-функций, хочется DSL вместо `when(...).thenReturn(...)`. - -**Не стоит**, если: тестируемый код легко собрать вручную (fake-реализация интерфейса часто читается лучше мока), или проект смешанный и в нём уже есть Mockito с настроенным `mockito-inline`. - -Fake вместо мока: -```kotlin -import kotlin.test.Test -import kotlin.test.assertEquals - -data class User(val id: Int, val name: String) - -interface UserRepo { - fun findById(id: Int): User? - fun save(user: User): User -} - -class FakeUserRepo : UserRepo { - private val storage = mutableMapOf() - override fun findById(id: Int) = storage[id] - override fun save(user: User) = user.also { storage[it.id] = it } -} - -class FakeTest { - @Test - fun `fake хранит состояние и читается без DSL`() { - val repo = FakeUserRepo() - repo.save(User(1, "Иван")) - assertEquals("Иван", repo.findById(1)?.name) - } -} -``` -Для интерфейса с состоянием fake обычно короче набора `every`-стабов и не ломается при изменении порядка вызовов. MockK нужнее там, где важно именно проверить взаимодействие. - -> [!info] Отличия от Java -> | Mockito | MockK | -> |---|---| -> | `mock(Repo.class)` | `mockk()` | -> | `when(m.f()).thenReturn(x)` | `every { m.f() } returns x` | -> | `verify(m).f()` | `verify { m.f() }` | -> | `verify(m, times(2)).f()` | `verify(exactly = 2) { m.f() }` | -> | финальные классы требуют `mockito-inline` | работают из коробки | -> | статику мокает только `mockStatic` с try-with-resources | `mockkStatic(::f)` | -> | `suspend` недоступны | `coEvery`, `coVerify` | -> | `ArgumentCaptor` + `getValue()` | `slot()` + `captured` | -> | `@Mock` + `MockitoAnnotations.openMocks` | `@MockK` + `MockKAnnotations.init` | -> -> **Финальность по умолчанию - причина существования MockK.** В Java классы открыты, поэтому Mockito годами обходился генерацией подклассов. В Kotlin класс без `open` финален, и подкласс создать нельзя - старый Mockito на таком коде просто падал. MockK использует инструментацию байт-кода через агент, поэтому финальность ему безразлична. Mockito догнал это `mockito-inline`, но подключать его нужно отдельно. Подробнее: [Наследование](../ООП/Наследование.md) -> -> **`object` и функции верхнего уровня не имеют аналога в Java.** Java-код обычно внедряет зависимости через конструктор, и статический вызов считается плохим стилем, который мокать не нужно. В Kotlin `object` - идиоматичный синглтон, а функция верхнего уровня - обычный способ написать утилиту; оба встречаются в коде, который требуется тестировать. `mockkObject` и `mockkStatic` закрывают этот разрыв, но ценой глобального состояния: подмена живёт до `unmockk`. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md) -> -> **DSL на лямбдах вместо цепочек методов.** `when(m.f()).thenReturn(x)` вызывает `m.f()` по-настоящему, а Mockito перехватывает вызов постфактум - отсюда странности с `void`-методами (`doReturn().when()`). В MockK `every { }` - это лямбда, которую библиотека выполняет в режиме записи, поэтому `Unit`-функции, `suspend`-функции и свойства обрабатываются одинаково. Подробнее: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md) -> -> **`null` в Mockito-моках был постоянной проблемой; в Kotlin он не компилируется.** Mockito по умолчанию возвращает `null` для незастабованных методов, и Java-код падал с NPE внутри теста. MockK вместо этого бросает `MockKException` с понятным сообщением либо, в `relaxed`-режиме, возвращает непустое значение по умолчанию. Строгая типизация `null` в Kotlin делает первый вариант единственно корректным. Подробнее: [Null-безопасность](../Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md) -> -> Сравнить: [Reflection API в Java](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md) - -> [!warning] Подводные камни -> **Порядок стабов определяет приоритет.** Последний заданный `every` выигрывает при совпадении аргументов. `every { repo.findById(1) } returns a` после `every { repo.findById(any()) } returns b` работает как ожидается, но в обратном порядке `any()` перехватит и `1` - проверено: вызов `findById(1)` вернёт значение из `any()`. -> -> **`relaxed = true` маскирует забытые стабы.** Для ссылочных типов возвращается дочерний мок, а не `null`, поэтому `repo.findById(1)?.name` даст не исключение, а бессмысленную строку. Для тестов, где важна полнота стабов, используйте строгий `mockk()` или `relaxUnitFun = true`. -> -> **`mockkObject` и `mockkStatic` глобальны и переживают тест.** Подмена живёт до `unmockkObject` / `unmockkStatic` / `unmockkAll` в пределах JVM, поэтому забытый вызов портит последующие тесты - причём непредсказуемо, так как порядок тестов не гарантирован. Всегда снимайте подмену в `@AfterEach` или через `MockKExtension`. -> -> **`mockkObject` - это spy, а не чистый мок.** Незастабованные методы `object` продолжают выполнять реальный код, в том числе обращаться к сети и базе. Для полной изоляции нужно застабовать каждый используемый метод. -> -> **`every` и `verify` не работают с `suspend`.** Нужны `coEvery` и `coVerify`; обычная форма даст ошибку компиляции или зависнет. Исключение - `coAnswers` вместо `answers`, когда внутри ответа есть `delay`. -> -> **`slot.captured` до первого вызова бросает `IllegalStateException`.** Проверять захваченное значение можно только после того, как мок был вызван; при `verify(exactly = 0)` обращение к `captured` всегда провалится. -> -> **`slot` хранит только последний аргумент.** При нескольких вызовах предыдущие значения теряются молча. Для проверки всех вызовов нужен `capture(mutableListOf())`. -> -> **`verifySequence` ломается от любого лишнего вызова.** Он требует ровно перечисленных вызовов, включая те, которые делает сам тестируемый код внутри. Для проверки только порядка нужных вызовов берите `verifyOrder`. -> -> **`confirmVerified` и `checkUnnecessaryStub` бросают `AssertionError`.** Это удобно (похоже на провал утверждения), но означает, что `runCatching { ... }.onFailure { }` в тесте проглотит ошибку вместе с реальными провалами. -> -> **`spyk` с `recordPrivateCalls` использует строковое имя метода.** `spy["secret"]()` не проверяется компилятором: переименование метода оставит тест зелёным, хотя стаб больше не применяется. -> -> **Сопоставление идёт по `equals`, поэтому `data class` ведёт себя иначе, чем обычный класс.** `verify { repo.save(User(1, "Иван")) }` проходит для `data class`, потому что `equals` сравнивает поля; для обычного класса без `equals` нужен тот же экземпляр или `refEq`. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md) -> -> **`returnsMany` после исчерпания списка повторяет последнее значение.** Четвёртый вызов при `returnsMany listOf(1, 2, 3)` вернёт `3`, а не бросит исключение - тест на «не больше трёх вызовов» так не напишешь, нужен `verify(exactly = 3)`. -> -> **Мокирование `inline`-функций не работает.** Тело `inline`-функции подставляется в место вызова на этапе компиляции, поэтому перехватывать нечего. Это же относится к `inline`-свойствам и функциям с `reified`. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md) -> -> **MockK медленнее Mockito на старте.** Агент инструментирует классы при первом обращении; в больших наборах тестов это десятки секунд. Переиспользование моков через `clearMocks` вместо создания новых в каждом тесте заметно помогает. -> -> **`@InjectMockKs` подбирает зависимости по типу.** Два мока одного типа в тесте приводят к неопределённому выбору, а конструктор с примитивами и строками получит значения по умолчанию вместо осмысленных. Если инъекция неочевидна - собирайте объект руками. - -Далее: [Тестирование корутин](Тестирование%20корутин.md) diff --git a/Kotlin/Тестирование/kotlin.test и JUnit 5.md b/Kotlin/Тестирование/kotlin.test и JUnit 5.md deleted file mode 100644 index 9c1a1d5..0000000 --- a/Kotlin/Тестирование/kotlin.test и JUnit 5.md +++ /dev/null @@ -1,610 +0,0 @@ ---- -title: kotlin.test и JUnit 5 -description: Полный набор утверждений kotlin.test, аннотации JUnit 5, параметризация, вложенные тесты и жизненный цикл -tags: - - kotlin - - testing - - junit ---- - -`kotlin.test` - многоплатформенный API утверждений и аннотаций. На JVM он делегирует работу JUnit 5, поэтому вы получаете стандартный движок и привычную инфраструктуру, но пишете код, который компилируется и для JS, и для Native. - -```kotlin -import kotlin.test.Test -import kotlin.test.assertEquals - -class StringUtilsTest { - @Test - fun `slugify заменяет пробелы`() { - assertEquals("привет-мир", "Привет Мир".lowercase().replace(" ", "-")) - } -} -``` - -## Подключение -```kotlin -// build.gradle.kts -dependencies { - testImplementation(kotlin("test")) -} - -tasks.test { - useJUnitPlatform() -} -``` -`kotlin("test")` на JVM автоматически выбирает вариант под настроенный движок. Если нужны аннотации JUnit 5 напрямую (`@ParameterizedTest`, `@Nested`), добавляется `org.junit.jupiter:junit-jupiter`. - -## Все утверждения -```kotlin -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import kotlin.test.assertTrue -import kotlin.test.assertFalse -import kotlin.test.assertNull -import kotlin.test.assertNotNull -import kotlin.test.assertSame -import kotlin.test.assertNotSame -import kotlin.test.assertContentEquals -import kotlin.test.assertContains -import kotlin.test.assertIs -import kotlin.test.assertIsNot -import kotlin.test.assertFailsWith - -class AllAssertions { - @Test - fun `весь набор kotlin test`() { - assertEquals(5, 2 + 3) - assertNotEquals(5, 4) - assertTrue(1 < 2) - assertFalse(1 > 2) - assertNull(null) - assertNotNull("есть") - assertSame("a", "a") // одна и та же ссылка - assertNotSame(listOf(1), listOf(1)) // разные объекты - assertContentEquals(listOf(1, 2), listOf(1, 2)) // поэлементно - assertContains(listOf(1, 2), 2) - assertContains("строка", "рок") - assertIs("текст") - assertIsNot("текст") - assertEquals(0.1 + 0.2, 0.3, absoluteTolerance = 1e-9) // для Double - - val e = assertFailsWith { error("сбой") } - assertEquals("сбой", e.message) - } -} -``` -| Утверждение | Проверяет | -|---|---| -| `assertEquals(expected, actual)` | равенство через `equals` | -| `assertNotEquals` | неравенство | -| `assertTrue` / `assertFalse` | булево условие | -| `assertNull` / `assertNotNull` | `null` | -| `assertSame` / `assertNotSame` | идентичность ссылок (`===`) | -| `assertContentEquals` | поэлементное равенство коллекций и массивов | -| `assertContains` | вхождение элемента, подстроки, диапазона | -| `assertIs` / `assertIsNot` | тип, со smart cast после проверки | -| `assertFailsWith { }` | исключение; возвращает его | -| `assertEquals(a, b, absoluteTolerance)` | `Double`/`Float` с допуском | -| `fail("сообщение")` | безусловный провал | - -Порядок аргументов - `expected, actual`. При провале сообщение гласит «Expected <...>, actual <...>», и перестановка делает отчёт неверным. - -### assertIs даёт умное приведение -```kotlin -import kotlin.test.Test -import kotlin.test.assertIs -import kotlin.test.assertEquals - -sealed interface Result -data class Ok(val value: String) : Result -data class Err(val code: Int) : Result - -fun compute(flag: Boolean): Result = if (flag) Ok("готово") else Err(500) - -class AssertIsTest { - @Test - fun `assertIs приводит тип`() { - val result = compute(true) - assertIs(result) - // после assertIs компилятор знает, что result это Ok - assertEquals("готово", result.value) - } -} -``` -Это преимущество перед `assertTrue(result is Ok)`: там приведения не будет, и понадобится `as`. Подробнее: [Умные приведения](../Синтаксис%20и%20типы/Null-безопасность/Умные%20приведения.md) - -### assertContentEquals против assertEquals -```kotlin -import kotlin.test.Test -import kotlin.test.assertContentEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class ContentEqualsTest { - @Test - fun `массивы сравниваются только через assertContentEquals`() { - val a = intArrayOf(1, 2, 3) - val b = intArrayOf(1, 2, 3) - - // equals у массивов — это сравнение ссылок - assertFalse(a == b) - assertTrue(a.contentEquals(b)) - assertContentEquals(a.toList(), b.toList()) - } - - @Test - fun `список и множество не равны даже при одном содержимом`() { - assertFalse(listOf(1, 2) == setOf(1, 2)) - assertContentEquals(listOf(1, 2), setOf(1, 2).toList()) - } -} -``` -Для массивов `assertEquals` сравнивает ссылки и всегда провалится. Подробнее: [Массивы](../Синтаксис%20и%20типы/Работа%20с%20данными/Массивы.md) - -### Исключения -```kotlin -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertTrue - -class Validator { - fun parse(input: String): Int { - require(input.isNotBlank()) { "вход пуст" } - return input.toIntOrNull() ?: throw NumberFormatException("не число: $input") - } -} - -class ExceptionTest { - private val v = Validator() - - @Test - fun `пустой вход даёт IllegalArgumentException`() { - val e = assertFailsWith { v.parse(" ") } - assertEquals("вход пуст", e.message) - } - - @Test - fun `нечисловой вход даёт NumberFormatException`() { - val e = assertFailsWith { v.parse("abc") } - assertTrue(e.message!!.contains("abc")) - } - - @Test - fun `корректный вход не бросает`() { - assertEquals(42, v.parse("42")) - } -} -``` -`assertFailsWith` требует **точного** типа или подтипа. `assertFailsWith` поймает что угодно - это обычно нежелательно, потому что тест перестанет различать ожидаемую ошибку и случайный `NullPointerException`. - -## Аннотации -| `kotlin.test` | JUnit 5 | Когда вызывается | -|---|---|---| -| `@Test` | `@Test` | каждый тест | -| `@BeforeTest` | `@BeforeEach` | перед каждым тестом | -| `@AfterTest` | `@AfterEach` | после каждого теста | -| - | `@BeforeAll` | один раз перед классом | -| - | `@AfterAll` | один раз после класса | -| `@Ignore` | `@Disabled` | пропустить | -| - | `@DisplayName` | имя в отчёте | -| - | `@Nested` | вложенный класс | -| - | `@RepeatedTest` | повторить N раз | -| - | `@Timeout` | ограничить время | -| - | `@Tag` | группировка для фильтрации | - -```kotlin -import kotlin.test.Test -import kotlin.test.BeforeTest -import kotlin.test.AfterTest -import kotlin.test.Ignore -import kotlin.test.assertEquals -import kotlin.test.fail - -class LifecycleTest { - private lateinit var state: MutableList - - @BeforeTest - fun setUp() { - state = mutableListOf("начальный") - } - - @AfterTest - fun tearDown() { - state.clear() - } - - @Test - fun `первый тест`() { - state += "добавлено" - assertEquals(2, state.size) - } - - @Test - fun `второй тест видит свежее состояние`() { - assertEquals(1, state.size) - } - - @Test - @Ignore - fun `пока не реализовано`() { - fail("не должен выполняться") - } -} -``` -`@Ignore` без причины - плохая практика: тест тихо не выполняется месяцами. В JUnit 5 причину можно указать: `@Disabled("ждёт исправления KT-12345")`. - -### @BeforeAll требует companion -```kotlin -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.AfterAll -import org.junit.jupiter.api.TestInstance -import kotlin.test.Test -import kotlin.test.assertTrue - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class BeforeAllTest { - private var expensiveResource: String = "" - - @BeforeAll - fun initOnce() { - expensiveResource = "дорогой ресурс" - } - - @AfterAll - fun cleanUpOnce() { - expensiveResource = "" - } - - @Test - fun `ресурс доступен`() { - assertTrue(expensiveResource.isNotEmpty()) - } -} -``` -Без `@TestInstance(PER_CLASS)` метод `@BeforeAll` должен быть статическим, то есть лежать в `companion object` с `@JvmStatic`: -```kotlin -import org.junit.jupiter.api.BeforeAll -import kotlin.test.Test -import kotlin.test.assertTrue - -class CompanionBeforeAllTest { - companion object { - private var resource: String = "" - - @JvmStatic - @BeforeAll - fun initOnce() { - resource = "инициализировано" - } - } - - @Test - fun `ресурс доступен`() { - assertTrue(resource.isNotEmpty()) - } -} -``` -`@TestInstance(PER_CLASS)` короче, но меняет семантику: экземпляр класса создаётся один для всех тестов, и состояние в полях протекает между ними. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md) - -## Параметризованные тесты -```kotlin -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.ValueSource -import org.junit.jupiter.params.provider.CsvSource -import org.junit.jupiter.params.provider.EnumSource -import org.junit.jupiter.params.provider.NullAndEmptySource -import org.junit.jupiter.params.provider.MethodSource -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -enum class Status { ACTIVE, BLOCKED, DELETED } - -class ParameterizedTests { - @ParameterizedTest - @ValueSource(strings = ["", " ", "\t", "\n"]) - fun `все эти строки пустые`(input: String) { - assertTrue(input.isBlank()) - } - - @ParameterizedTest - @ValueSource(ints = [1, 2, 3, 5, 8]) - fun `все эти числа положительны`(n: Int) { - assertTrue(n > 0) - } - - @ParameterizedTest - @CsvSource("2, 3, 5", "0, 0, 0", "-1, 1, 0", "100, -50, 50") - fun `сложение по таблице`(a: Int, b: Int, expected: Int) { - assertEquals(expected, a + b) - } - - @ParameterizedTest - @EnumSource(Status::class) - fun `все статусы имеют непустое имя`(status: Status) { - assertTrue(status.name.isNotEmpty()) - } - - @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = [" "]) - fun `null пустая и пробельная строка одинаково бесполезны`(input: String?) { - assertTrue(input.isNullOrBlank()) - } - - @ParameterizedTest - @MethodSource("слоги") - fun `слоги короткие`(syllable: String) { - assertTrue(syllable.length <= 3) - } - - companion object { - @JvmStatic - fun слоги() = listOf("ма", "па", "ба") - } -} -``` -| Источник | Что даёт | -|---|---| -| `@ValueSource` | массив литералов одного типа | -| `@CsvSource` | строки CSV → несколько параметров | -| `@CsvFileSource` | CSV из ресурса | -| `@EnumSource` | все значения enum | -| `@NullSource` / `@EmptySource` / `@NullAndEmptySource` | граничные значения | -| `@MethodSource` | статический метод, возвращающий коллекцию | -| `@ArgumentsSource` | свой провайдер | - -`@MethodSource` на Kotlin требует `@JvmStatic` в `companion object` - иначе JUnit не найдёт метод. Это частая причина ошибки «Could not find factory method». - -## Вложенные тесты -```kotlin -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.DisplayName -import kotlin.test.Test -import kotlin.test.BeforeTest -import kotlin.test.assertEquals -import kotlin.test.assertNull -import kotlin.test.assertTrue - -class Cache { - private val store = mutableMapOf() - fun put(k: String, v: String) { store[k] = v } - fun get(k: String) = store[k] - fun size() = store.size -} - -@DisplayName("Кэш") -class CacheTest { - - @Nested - @DisplayName("когда пуст") - inner class WhenEmpty { - private val cache = Cache() - - @Test - fun `размер ноль`() { - assertEquals(0, cache.size()) - } - - @Test - fun `любой ключ даёт null`() { - assertNull(cache.get("любой")) - } - } - - @Nested - @DisplayName("когда заполнен") - inner class WhenFilled { - private val cache = Cache() - - @BeforeTest - fun fill() { - cache.put("ключ", "значение") - } - - @Test - fun `размер один`() { - assertEquals(1, cache.size()) - } - - @Test - fun `известный ключ возвращает значение`() { - assertEquals("значение", cache.get("ключ")) - } - - @Test - fun `неизвестный ключ даёт null`() { - assertTrue(cache.get("другой") == null) - } - } -} -``` -`@Nested` требует `inner class` - вложенный класс в Kotlin по умолчанию статический, и JUnit его не примет. Это даёт группировку в отчёте: «Кэш → когда пуст → размер ноль». Подробнее: [Вложенные и внутренние классы](../ООП/Вложенные%20и%20внутренние%20классы.md) - -## Повторы и таймауты -```kotlin -import org.junit.jupiter.api.RepeatedTest -import org.junit.jupiter.api.Timeout -import org.junit.jupiter.api.Tag -import java.util.concurrent.TimeUnit -import kotlin.test.Test -import kotlin.test.assertTrue - -class RepeatAndTimeoutTest { - @RepeatedTest(5) - fun `генератор всегда в диапазоне`() { - val value = (1..100).random() - assertTrue(value in 1..100) - } - - @Test - @Timeout(value = 500, unit = TimeUnit.MILLISECONDS) - fun `быстрая операция укладывается в срок`() { - assertTrue((1..1000).sum() > 0) - } - - @Test - @Tag("slow") - fun `помеченный тест можно отфильтровать`() { - assertTrue(true) - } -} -``` -Фильтрация по тегам: -```kotlin -// build.gradle.kts -tasks.test { - useJUnitPlatform { - excludeTags("slow") - } -} - -tasks.register("slowTest") { - useJUnitPlatform { - includeTags("slow") - } -} -``` - -## Утверждение нескольких условий сразу -```kotlin -import org.junit.jupiter.api.assertAll -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -data class User(val id: Int, val name: String, val age: Int) - -class AssertAllTest { - @Test - fun `все поля проверяются даже при провале одного`() { - val user = User(1, "Иван", 30) - assertAll( - { assertEquals(1, user.id) }, - { assertEquals("Иван", user.name) }, - { assertTrue(user.age >= 18) }, - ) - } -} -``` -Без `assertAll` первый провалившийся `assertEquals` прерывает тест, и об остальных полях вы не узнаете. Это особенно полезно при проверке DTO. - -## Что не стоит тестировать -```kotlin -import kotlin.test.Test -import kotlin.test.assertEquals - -data class Point(val x: Int, val y: Int) { - init { - require(x >= 0 && y >= 0) { "координаты неотрицательны" } - } - - fun distanceTo(other: Point): Double { - val dx = (x - other.x).toDouble() - val dy = (y - other.y).toDouble() - return kotlin.math.sqrt(dx * dx + dy * dy) - } -} - -class PointTest { - // equals, hashCode, toString, copy генерирует компилятор — не тестируем - - @Test - fun `валидация в init работает`() { - val e = runCatching { Point(-1, 0) }.exceptionOrNull() - assertEquals("координаты неотрицательны", e?.message) - } - - @Test - fun `расстояние считается верно`() { - assertEquals(5.0, Point(0, 0).distanceTo(Point(3, 4)), absoluteTolerance = 1e-9) - } -} -``` -Сгенерированные члены `data class` не содержат вашей логики. Тестируйте валидацию в `init`, кастомные методы и всё, что вы написали руками. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md) - -## Запуск -``` -./gradlew test -./gradlew test --tests "com.example.CacheTest" -./gradlew test --tests "*Cache*" -./gradlew test --tests "com.example.CacheTest.WhenEmpty" -./gradlew test --info -./gradlew test --rerun-tasks -``` -Отчёт - `build/reports/tests/test/index.html`. - -> [!info] Отличия от Java -> | Java (JUnit 5) | Kotlin (kotlin.test) | -> |---|---| -> | `Assertions.assertEquals` | `assertEquals` (top-level) | -> | `assertThrows(T.class) { }` | `assertFailsWith { }` | -> | `assertInstanceOf(T.class, x)` | `assertIs(x)` + smart cast | -> | `assertArrayEquals` | `assertContentEquals` | -> | `@BeforeEach` | `@BeforeTest` | -> | `@Disabled` | `@Ignore` | -> | `void shouldDoX()` | `fun \`делает X\`()` | -> | `@BeforeAll static` | `companion object` + `@JvmStatic` | -> | `@Nested static class` | `@Nested inner class` | -> -> **`kotlin.test` - фасад, а не фреймворк.** `Assertions` в JUnit 5 - это сам JUnit. `kotlin.test` - тонкий слой, который на JVM делегирует JUnit, на JS - Jasmine/Mocha, на Native - собственный движок. Поэтому код утверждений переносится между платформами без изменений, а инфраструктура остаётся родной для каждой. Цена - набор утверждений беднее, чем у специализированных библиотек: нет цепочек вроде AssertJ. -> -> **`assertIs` даёт то, чего `assertInstanceOf` в Java дать не может.** Java-версия возвращает приведённое значение, которое нужно присвоить переменной. `assertIs` объявлен с контрактом `returns() implies (value is T)`, поэтому компилятор применяет умное приведение к исходной переменной. Это следствие механизма контрактов Kotlin. Подробнее: [Проверки и контракты](../Исключения/Проверки%20и%20контракты.md) -> -> **Статические методы требуют `companion object` + `@JvmStatic`.** JUnit 5 проектировался под Java, где `static` - модификатор. В Kotlin статики нет, и `@BeforeAll`, `@MethodSource`, `@AfterAll` требуют либо `@JvmStatic` в companion, либо `@TestInstance(PER_CLASS)`. Это самая частая точка трения при работе с JUnit из Kotlin. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md) -> -> **`@Nested` требует `inner`.** В Java вложенный класс нестатический по умолчанию, и `@Nested` работает сразу. В Kotlin всё наоборот: `class Inner` внутри класса статический, и нужен явный `inner`. Без него JUnit сообщает, что `@Nested`-класс должен быть нестатическим. -> -> **Проверяемых исключений нет, поэтому `assertFailsWith` важнее.** В Java компилятор заставляет объявить `throws`, и наличие исключения видно в сигнатуре. В Kotlin исключение - невидимая часть контракта, и тест становится единственной документацией того, что функция может упасть. Подробнее: [Система исключений](../Исключения/Система%20исключений.md) -> -> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md) - -> [!warning] Подводные камни -> **Порядок аргументов `assertEquals(expected, actual)`.** Перестановка не вызывает ошибки, но отчёт о провале становится обратным: «Expected <фактическое>, actual <ожидаемое>». Отлаживать такой тест заметно труднее. -> -> **`assertEquals` на массивах всегда падает:** -> ```kotlin -> import kotlin.test.Test -> import kotlin.test.assertFalse -> import kotlin.test.assertTrue -> -> class ArrayPitfall { -> @Test -> fun `equals у массивов сравнивает ссылки`() { -> val a = intArrayOf(1, 2) -> val b = intArrayOf(1, 2) -> assertFalse(a == b) -> assertTrue(a.contentEquals(b)) -> } -> } -> ``` -> Нужен `assertContentEquals` или `contentEquals`. -> -> **`assertEquals` для `Double` без допуска ненадёжен.** `assertEquals(0.3, 0.1 + 0.2)` провалится: результат `0.30000000000000004`. Всегда указывайте `absoluteTolerance`. Подробнее: [Числа и переполнение](../Синтаксис%20и%20типы/Работа%20с%20данными/Числа%20и%20переполнение.md) -> -> **`assertEquals(1, 1L)` не компилируется.** Типы `Int` и `Long` различаются, и перегрузки для смешанного сравнения нет. Приводите явно: `assertEquals(1L, value)`. -> -> **`@MethodSource` без `@JvmStatic` не находится.** Ошибка - «Could not find factory method». Метод должен лежать в `companion object` и быть помечен `@JvmStatic`, либо класс должен иметь `@TestInstance(PER_CLASS)`. -> -> **`@BeforeAll` без `@JvmStatic` молча не вызывается** или даёт ошибку о нестатическом методе. Та же причина. -> -> **`@TestInstance(PER_CLASS)` протекает состоянием.** Один экземпляр класса на все тесты означает, что мутация поля в первом тесте видна во втором, и порядок начинает иметь значение. Используйте только когда действительно нужна дорогая инициализация. -> -> **`@Nested` без `inner` не работает.** Сообщение JUnit - «@Nested class must not be static». В Kotlin вложенный класс статический по умолчанию. -> -> **`assertFailsWith` слишком широк.** Он поймает и `NullPointerException` от опечатки в тесте, и тест останется зелёным при неправильной реализации. Указывайте конкретный тип. -> -> **Без `useJUnitPlatform()` ничего не запустится.** Gradle по умолчанию использует JUnit 4; тесты JUnit 5 не найдутся, и вывод будет «0 tests found» без ошибки. -> -> **Три разных `@Test` в classpath.** `kotlin.test.Test`, `org.junit.jupiter.api.Test` и `org.junit.Test` (JUnit 4). Импорт не того варианта приводит к тому, что тест не запускается. При наличии `kotlin-test` используйте `kotlin.test.Test`. -> -> **Обратные кавычки запрещают часть символов.** `` `тест: случай` `` или `` `a.b` `` не скомпилируются - двоеточие, точка, слэши и угловые скобки недопустимы в именах методов JVM. -> -> **`@Ignore` без причины превращается в мёртвый код.** Через месяц никто не помнит, почему тест отключён. Используйте `@Disabled("причина")` из JUnit 5. -> -> **`assertSame` на строках даёт неожиданный результат.** `assertSame("a", "a")` проходит из-за интернирования литералов, но `assertSame("a", buildString { append("a") })` провалится. Для содержимого используйте `assertEquals`. -> -> **Тест в `@BeforeTest` с `lateinit` падает непонятно.** Если `@BeforeTest` бросил исключение, `lateinit`-поле останется неинициализированным, и тест упадёт с `UninitializedPropertyAccessException` - маскируя настоящую причину. Проверяйте вывод `@BeforeTest` в отчёте. - -Далее: [Kotest](Kotest.md) diff --git a/Kotlin/Тестирование/Тестирование корутин.md b/Kotlin/Тестирование/Тестирование корутин.md deleted file mode 100644 index 970ce00..0000000 --- a/Kotlin/Тестирование/Тестирование корутин.md +++ /dev/null @@ -1,680 +0,0 @@ ---- -title: Тестирование корутин -description: runTest, виртуальное время, TestDispatcher, backgroundScope, тестирование Flow и Turbine -tags: - - kotlin - - testing - - coroutines ---- - -Корутины ломают привычную модель теста: результат появляется не в момент вызова, а когда-то позже, а `delay` делает тест медленным. Библиотека `kotlinx-coroutines-test` решает обе проблемы - она даёт `runTest` с виртуальным временем, где `delay(10.seconds)` выполняется мгновенно. - -```kotlin -import kotlinx.coroutines.delay -import kotlinx.coroutines.test.currentTime -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.time.Duration.Companion.seconds - -class FirstCoroutineTest { - @Test - fun `delay не замедляет тест`() = runTest { - val start = currentTime - delay(10.seconds) - assertEquals(10_000, currentTime - start) - } -} -``` -Тест завершается за миллисекунды, хотя по виртуальным часам прошло десять секунд. - -## Подключение -```kotlin -// build.gradle.kts -dependencies { - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0") - testImplementation("app.cash.turbine:turbine:1.2.1") -} - -tasks.test { useJUnitPlatform() } -``` -| Артефакт | Зачем | -|---|---| -| `kotlinx-coroutines-test` | `runTest`, `TestDispatcher`, `TestScope` | -| `app.cash.turbine:turbine` | удобная проверка `Flow` по элементам | -| `kotlinx-coroutines-debug` | дампы корутин при зависании | - -Версия `kotlinx-coroutines-test` должна совпадать с версией `kotlinx-coroutines-core` - иначе возможен `NoSuchMethodError` в рантайме. - -## runTest -```kotlin -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.advanceTimeBy -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals - -class RunTestControlTest { - @Test - fun `runCurrent выполняет уже запланированное`() = runTest { - val log = mutableListOf() - launch { log += "сразу" } - // корутина запланирована, но ещё не запущена - assertEquals(emptyList(), log) - runCurrent() - assertEquals(listOf("сразу"), log) - } - - @Test - fun `advanceTimeBy продвигает часы на заданное время`() = runTest { - val log = mutableListOf() - launch { - delay(100); log += "a" - delay(100); log += "b" - } - advanceTimeBy(150) - assertEquals(listOf("a"), log) - advanceUntilIdle() - assertEquals(listOf("a", "b"), log) - } - - @Test - fun `testScheduler доступен напрямую`() = runTest { - assertEquals(0, testScheduler.currentTime) - delay(5) - assertEquals(5, testScheduler.currentTime) - } -} -``` -| Инструмент | Что делает | -|---|---| -| `runTest { }` | запускает тело как корутину с виртуальным временем | -| `currentTime` | виртуальные часы в миллисекундах | -| `runCurrent()` | выполняет задачи, запланированные на текущий момент | -| `advanceTimeBy(ms)` | продвигает часы, выполняя всё по пути | -| `advanceUntilIdle()` | продвигает часы до исчерпания задач | -| `testScheduler` | планировщик, общий для диспетчеров теста | -| `backgroundScope` | scope, отменяемый по завершении теста | -| `runTest(timeout = 5.seconds)` | реальный таймаут на весь тест, по умолчанию 60 с | - -`runTest` возвращает `TestResult`, поэтому тело теста пишется как `= runTest { }`, а не `{ runTest { } }` - на JVM это `Unit`, но форма с `=` работает и в Multiplatform. - -После выхода из тела `runTest` ждёт завершения всех дочерних корутин, запущенных в его scope. Это именно то, что нужно в большинстве тестов: явный `advanceUntilIdle` в конце не требуется. - -## Диспетчеры теста -```kotlin -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals - -class DispatchersTest { - @Test - fun `StandardTestDispatcher откладывает запуск`() = runTest(StandardTestDispatcher()) { - val log = mutableListOf() - launch { log += "дочерняя" } - assertEquals(emptyList(), log) - runCurrent() - assertEquals(listOf("дочерняя"), log) - } - - @Test - fun `UnconfinedTestDispatcher запускает немедленно`() = runTest(UnconfinedTestDispatcher()) { - val log = mutableListOf() - launch { log += "дочерняя" } - assertEquals(listOf("дочерняя"), log) - } - - @Test - fun `общий scheduler связывает диспетчеры`() = runTest { - val other = StandardTestDispatcher(testScheduler) - val log = mutableListOf() - launch(other) { delay(100); log += "другой" } - advanceUntilIdle() - assertEquals(listOf("другой"), log) - } -} -``` -| Диспетчер | Поведение | -|---|---| -| `StandardTestDispatcher` | корутина ставится в очередь, нужен `runCurrent` или `advanceUntilIdle` | -| `UnconfinedTestDispatcher` | корутина выполняется до первой приостановки немедленно | - -`runTest` без аргумента использует `StandardTestDispatcher` - это осознанный выбор: отложенный запуск приближает тест к реальному многопоточному исполнению и находит гонки, которые `Unconfined` скрывает. - -`UnconfinedTestDispatcher` удобен, когда проверяется результат, а не порядок - особенно при сборе `StateFlow`, где конфлюирование съедает промежуточные значения. - -Главное про `scheduler`: виртуальные часы живут в нём, а не в диспетчере. Диспетчер, созданный **без** передачи `testScheduler`, получает свой собственный планировщик, и `advanceUntilIdle` теста на него не действует. - -## backgroundScope -```kotlin -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.advanceTimeBy -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals - -class BackgroundScopeTest { - @Test - fun `бесконечная корутина не подвешивает тест`() = runTest { - var ticks = 0 - backgroundScope.launch { - while (true) { - delay(100) - ticks++ - } - } - advanceTimeBy(350) - assertEquals(3, ticks) - } -} -``` -`runTest` ждёт дочерние корутины своего scope, поэтому бесконечный цикл в `launch { }` приведёт к `UncompletedCoroutinesError` по таймауту. `backgroundScope` отменяется автоматически по завершении теста - это единственно верное место для долгоживущих коллекторов, подписок и имитации фоновой работы. - -## Внедрение диспетчера -Виртуальное время работает только для диспетчеров, связанных с `testScheduler`. Жёстко прописанный `Dispatchers.IO` в продакшн-коде делает тест реально медленным: -```kotlin -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.withContext -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class Hardcoded { - suspend fun work(): String = withContext(Dispatchers.IO) { - delay(1000) - "результат" - } -} - -class Injected(private val io: CoroutineDispatcher) { - suspend fun work(): String = withContext(io) { - delay(1000) - "результат" - } -} - -class DispatcherInjectionTest { - @Test - fun `внедрённый диспетчер виртуализирует delay`() = runTest { - val wall = System.currentTimeMillis() - val service = Injected(StandardTestDispatcher(testScheduler)) - assertEquals("результат", service.work()) - assertTrue(System.currentTimeMillis() - wall < 500) - } - - @Test - fun `жёстко заданный диспетчер ждёт реальную секунду`() = runTest { - val wall = System.currentTimeMillis() - assertEquals("результат", Hardcoded().work()) - assertTrue(System.currentTimeMillis() - wall >= 900) - } -} -``` -Оба теста проходят, но второй занимает реальную секунду. В продакшн-коде диспетчер стоит передавать параметром с значением по умолчанию: -```kotlin -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.withContext - -class Service( - private val io: CoroutineDispatcher = Dispatchers.IO, -) { - suspend fun load(): String = withContext(io) { - delay(1000) - "данные" - } -} -``` -Вызывающий код ничего не меняет, а тест подставляет `StandardTestDispatcher(testScheduler)`. Подробнее: [Диспетчеры и контекст](../Корутины/Диспетчеры%20и%20контекст.md) - -## Dispatchers.Main -`Dispatchers.Main` на JVM без UI-фреймворка не существует, поэтому его подменяют: -```kotlin -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import kotlin.test.Test -import kotlin.test.assertEquals - -class ViewModelLike(private val scope: CoroutineScope) { - private val _state = MutableStateFlow("начало") - val state: StateFlow = _state - - fun load() { - scope.launch { - _state.value = "загрузка" - delay(1000) - _state.value = "готово" - } - } -} - -class MainDispatcherTest { - @Test - fun `setMain подменяет Main на тестовый диспетчер`() = runTest { - Dispatchers.setMain(StandardTestDispatcher(testScheduler)) - try { - val vm = ViewModelLike(CoroutineScope(Dispatchers.Main)) - vm.load() - runCurrent() - assertEquals("загрузка", vm.state.value) - advanceUntilIdle() - assertEquals("готово", vm.state.value) - } finally { - Dispatchers.resetMain() - } - } -} -``` -`setMain` и `resetMain` - глобальное состояние, поэтому `try`/`finally` обязателен. Повторять его в каждом тесте не нужно - проще вынести в расширение JUnit: -```kotlin -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.TestDispatcher -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import org.junit.jupiter.api.extension.AfterEachCallback -import org.junit.jupiter.api.extension.BeforeEachCallback -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.ExtensionContext -import kotlin.test.Test -import kotlin.test.assertEquals - -class MainDispatcherExtension( - private val dispatcher: TestDispatcher = UnconfinedTestDispatcher(), -) : BeforeEachCallback, AfterEachCallback { - override fun beforeEach(context: ExtensionContext) = Dispatchers.setMain(dispatcher) - override fun afterEach(context: ExtensionContext) = Dispatchers.resetMain() -} - -class ViewModel { - private val scope = CoroutineScope(Dispatchers.Main) - private val _state = MutableStateFlow("начало") - val state: StateFlow = _state - - fun load() { - scope.launch { - _state.value = "загрузка" - delay(1000) - _state.value = "готово" - } - } -} - -@ExtendWith(MainDispatcherExtension::class) -class ViewModelTest { - @Test - fun `Main подменён для всех тестов класса`() = runTest { - val vm = ViewModel() - vm.load() - assertEquals("загрузка", vm.state.value) - advanceUntilIdle() - assertEquals("готово", vm.state.value) - } -} -``` -Подробнее про расширения: [kotlin.test и JUnit 5](kotlin.test%20и%20JUnit%205.md) - -## Тестирование Flow -Конечный поток проверяется сборкой в список: -```kotlin -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals - -class FlowCollectTest { - @Test - fun `toList собирает конечный поток`() = runTest { - val source = flow { - emit(1); delay(100) - emit(2); delay(100) - emit(3) - } - assertEquals(listOf(1, 2, 3), source.toList()) - } - - @Test - fun `take ограничивает бесконечный поток`() = runTest { - val infinite = flow { - var i = 0 - while (true) { - emit(i++) - delay(10) - } - } - assertEquals(listOf(0, 1, 2), infinite.take(3).toList()) - } -} -``` -`toList()` на бесконечном потоке подвесит тест до таймаута - ограничение через `take` обязательно. - -### StateFlow и конфлюирование -```kotlin -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals - -class StateFlowTest { - @Test - fun `Unconfined видит каждое значение`() = runTest { - val state = MutableStateFlow(0) - val seen = mutableListOf() - backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { - state.collect { seen += it } - } - state.value = 1 - state.value = 2 - runCurrent() - assertEquals(listOf(0, 1, 2), seen) - } - - @Test - fun `Standard конфлюирует промежуточные значения`() = runTest { - val state = MutableStateFlow(0) - val seen = mutableListOf() - backgroundScope.launch { state.collect { seen += it } } - state.value = 1 - state.value = 2 - runCurrent() - // коллектор не успел запуститься и получил только последнее - assertEquals(listOf(2), seen) - } -} -``` -Разница принципиальная: со `StandardTestDispatcher` коллектор стартует только при `runCurrent`, к тому моменту `value` уже равно `2`, а `StateFlow` не хранит историю. Для проверки последовательности значений нужен `UnconfinedTestDispatcher(testScheduler)` - и обязательно с передачей `testScheduler`. Подробнее: [StateFlow и SharedFlow](../Корутины/StateFlow%20и%20SharedFlow.md) - -Если проверяется только итог, достаточно `state.value` без коллектора вообще. - -## Turbine -Ручной сбор в список быстро становится громоздким: нужно управлять scope, продвигать время и помнить про конфлюирование. Turbine даёт императивный API над потоком: -```kotlin -import app.cash.turbine.test -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals - -class TurbineTest { - @Test - fun `awaitItem забирает элементы по одному`() = runTest { - flow { emit(1); delay(100); emit(2) }.test { - assertEquals(1, awaitItem()) - assertEquals(2, awaitItem()) - awaitComplete() - } - } - - @Test - fun `StateFlow отдаёт начальное значение первым`() = runTest { - val state = MutableStateFlow(0) - state.test { - assertEquals(0, awaitItem()) - state.value = 1 - assertEquals(1, awaitItem()) - cancelAndIgnoreRemainingEvents() - } - } - - @Test - fun `awaitError ловит исключение потока`() = runTest { - flow { emit(1); throw IllegalStateException("сбой") }.test { - assertEquals(1, awaitItem()) - assertEquals("сбой", awaitError().message) - } - } - - @Test - fun `skipItems и expectNoEvents`() = runTest { - flowOf(1, 2, 3, 4).test { - skipItems(2) - assertEquals(3, awaitItem()) - assertEquals(4, awaitItem()) - awaitComplete() - } - MutableSharedFlow().test { - expectNoEvents() - cancelAndIgnoreRemainingEvents() - } - } -} -``` -| Функция | Что делает | -|---|---| -| `awaitItem()` | ждёт следующий элемент | -| `awaitComplete()` | ждёт нормального завершения | -| `awaitError()` | ждёт исключения и возвращает его | -| `skipItems(n)` | пропускает `n` элементов | -| `expectNoEvents()` | проверяет, что событий не было | -| `expectMostRecentItem()` | берёт последний, отбрасывая предыдущие | -| `cancelAndIgnoreRemainingEvents()` | завершает сбор, не требуя разбора остатка | -| `cancelAndConsumeRemainingEvents()` | то же, но возвращает остаток | - -Ключевое свойство: **незабранные события проваливают тест**. Если поток выдал три элемента, а проверены два, Turbine бросит `TurbineAssertionError` при выходе из блока. Это ловит лишние эмиссии, которые ручной `toList()` просто не замечает. - -Несколько потоков сразу - через `turbineScope`: -```kotlin -import app.cash.turbine.turbineScope -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals - -class TurbineScopeTest { - @Test - fun `два потока проверяются вперемешку`() = runTest { - turbineScope { - val numbers = flowOf(1, 2).testIn(backgroundScope) - val letters = flowOf("x").testIn(backgroundScope) - - assertEquals(1, numbers.awaitItem()) - assertEquals("x", letters.awaitItem()) - assertEquals(2, numbers.awaitItem()) - numbers.awaitComplete() - letters.awaitComplete() - } - } -} -``` - -## Исключения и отмена -```kotlin -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue -import kotlin.time.Duration.Companion.seconds - -class ErrorsTest { - @Test - fun `исключение дочерней корутины доходит до теста`() = runTest { - val e = runCatching { - coroutineScope { - launch { throw IllegalStateException("из дочерней") } - } - }.exceptionOrNull() - assertEquals("из дочерней", e?.message) - } - - @Test - fun `незавершённая корутина в основном scope даёт UncompletedCoroutinesError`() { - val e = runCatching { - runTest(timeout = 1.seconds) { - launch { while (true) { delay(100) } } - } - }.exceptionOrNull() - // UncompletedCoroutinesError внутренний, поэтому проверяем по имени - assertEquals("UncompletedCoroutinesError", e!!::class.simpleName) - } - - @Test - fun `TestScope создаётся вручную вне runTest`() { - val scope = TestScope() - var done = false - scope.launch { delay(1000); done = true } - scope.advanceUntilIdle() - assertTrue(done) - } -} -``` -Необработанное исключение дочерней корутины проваливает `runTest` - специально ловить его не нужно. `TestScope()` вручную пригодится, когда тестируемый класс сам принимает `CoroutineScope`: в этом случае scope передаётся в конструктор, а тест управляет временем через `advanceUntilIdle`. Подробнее: [Исключения в корутинах](../Корутины/Исключения%20в%20корутинах.md) - -## Моки suspend-функций -```kotlin -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk -import kotlinx.coroutines.delay -import kotlinx.coroutines.test.currentTime -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals - -interface Api { - suspend fun fetch(id: Int): String -} - -class Repository(private val api: Api) { - suspend fun load(id: Int): String = "данные: ${api.fetch(id)}" -} - -class MockSuspendTest { - @Test - fun `coEvery с виртуальной задержкой`() = runTest { - val api = mockk() - coEvery { api.fetch(1) } coAnswers { - delay(5000) - "ответ" - } - - assertEquals("данные: ответ", Repository(api).load(1)) - assertEquals(5000, currentTime) - coVerify(exactly = 1) { api.fetch(1) } - } -} -``` -`delay` внутри `coAnswers` под `runTest` виртуализируется - имитация медленного сетевого вызова не замедляет тест. Подробнее: [MockK](MockK.md) - -## Корутины в Kotest -```kotlin -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.withContext -import kotlinx.coroutines.test.currentTime -import kotlinx.coroutines.test.runTest - -class KotestCoroutines : StringSpec({ - "тело теста уже suspend" { - withContext(Dispatchers.Default) { 42 } shouldBe 42 - } - "runTest нужен для виртуального времени" { - runTest { - delay(10_000) - currentTime shouldBe 10_000 - } - } -}) -``` -Тело теста в Kotest - уже `suspend`-функция, поэтому `suspend`-вызовы работают без обёртки. Но виртуального времени там нет: `delay(100)` займёт реальные 100 мс, пока тест не завёрнут в `runTest`. Подробнее: [Kotest](Kotest.md) - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `Thread.sleep` в тесте - реальная пауза | `delay` в `runTest` виртуален | -> | `CountDownLatch` для синхронизации | `advanceUntilIdle()` | -> | `Awaitility.await().until { }` | детерминированный планировщик | -> | `future.get(5, SECONDS)` | `runTest { }` ждёт сам | -> | `ExecutorService` + `shutdownNow` | `backgroundScope` отменяется сам | -> | флаки из-за таймингов | порядок задан планировщиком | -> -> **Виртуальное время заменяет ожидание реального.** В Java тест асинхронного кода либо спит (`Thread.sleep(100)` - медленно и ненадёжно), либо синхронизируется через `CountDownLatch` и `Awaitility` - это работает, но требует явных точек синхронизации в продакшн-коде или угадывания таймаутов. `TestCoroutineScheduler` вместо этого контролирует время: `delay` регистрирует задачу на виртуальный момент, а `advanceTimeBy` исполняет её немедленно. Тест становится детерминированным и мгновенным одновременно. Подробнее: [Взаимодейтсвие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md) -> -> **Порядок выполнения задан, а не случаен.** Java-тесты на `ExecutorService` зависят от решений планировщика ОС: один и тот же тест проходит локально и падает в CI. `StandardTestDispatcher` исполняет корутины в строго определённом порядке - по очереди задач. Это убирает флаки, но меняет требования к коду: тест, рассчитывающий на немедленный запуск `launch`, нужно писать с `runCurrent()`. -> -> **Структурированная конкурентность делает «забытую задачу» ошибкой компилятора времени выполнения.** В Java поток, оставшийся после теста, просто живёт дальше и может испортить следующий тест или задержать завершение JVM. `runTest` ждёт все дочерние корутины своего scope и бросает `UncompletedCoroutinesError`, если какая-то не завершилась. Для осознанно долгоживущих задач предусмотрен `backgroundScope` - забыть отменить подписку в тесте просто нельзя. Подробнее: [Структурированная конкурентность](../Корутины/Структурированная%20конкурентность.md) -> -> **`Dispatchers.Main` приходится подменять, потому что это не настоящий диспетчер.** В Java нет понятия «главного потока» на уровне стандартной библиотеки - есть Swing EDT и Android main looper, каждый со своим API. Kotlin вводит `Dispatchers.Main` как общую абстракцию, и на JVM без UI-фреймворка он отсутствует: обращение без `setMain` даёт `IllegalStateException`. Это цена единого API для всех платформ. -> -> Сравнить: [Reflection API в Java](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md) - -> [!warning] Подводные камни -> **Диспетчер без `testScheduler` подвешивает тест.** `UnconfinedTestDispatcher()` вне `runTest` создаёт **свой** планировщик, поэтому `advanceUntilIdle()` на него не действует: корутина остаётся активной, и тест падает с `UncompletedCoroutinesError` через 60 секунд. Всегда передавайте планировщик: `UnconfinedTestDispatcher(testScheduler)`. -> -> **`Dispatchers.Main` без `setMain` роняет не только тест, а весь прогон.** Исключение возникает внутри потока корутины, поэтому может не попасть в отчёт конкретного теста и проявиться как необъяснимое падение JVM. Подменяйте `Main` в `@BeforeEach` или через расширение. -> -> **`Dispatchers.setMain` - глобальное состояние.** Забытый `resetMain` ломает тесты, которым нужен настоящий `Main`, причём порядок тестов не гарантирован. Используйте `try`/`finally` или расширение JUnit. -> -> **`StateFlow` конфлюирует, и промежуточные значения теряются.** Со `StandardTestDispatcher` коллектор запускается позже эмиссий и увидит только последнее значение - проверено: из `0 → 1 → 2` в список попадёт `[2]`. Для последовательности нужен `UnconfinedTestDispatcher(testScheduler)`. Подробнее: [StateFlow и SharedFlow](../Корутины/StateFlow%20и%20SharedFlow.md) -> -> **`MutableSharedFlow` без буфера приостанавливает `emit` до появления подписчика.** Это не потеря данных, как часто пишут, а приостановка: `launch { shared.emit(1) }` до подписки дождётся коллектора и доставит значение. Но если `emit` вызван напрямую в теле теста, тест повиснет. Подписывайтесь до эмиссии и вызывайте `runCurrent()`. -> -> **Жёстко прописанный `Dispatchers.IO` или `Default` делает тест реально медленным.** `withContext(Dispatchers.IO) { delay(1000) }` займёт настоящую секунду даже внутри `runTest` - планировщик теста такой диспетчер не контролирует. Внедряйте диспетчер параметром. -> -> **Бесконечная корутина в основном scope `runTest` даёт `UncompletedCoroutinesError`.** `launch { while (true) { delay(100) } }` ждёт до таймаута (60 с по умолчанию). Для долгоживущих задач используйте `backgroundScope`. -> -> **`toList()` на бесконечном потоке висит до таймаута.** Ограничивайте через `take(n)`, `timeout` или проверяйте через Turbine с `cancelAndIgnoreRemainingEvents()`. -> -> **Незабранные события в Turbine проваливают тест - и это правильно.** `TurbineAssertionError` при выходе из `test { }` означает, что поток выдал больше элементов, чем проверено. Если остаток не важен, завершайте явным `cancelAndIgnoreRemainingEvents()`. -> -> **`advanceUntilIdle()` не исполняет задачи чужих планировщиков.** Если тестируемый класс создаёт корутины на `Dispatchers.Default`, они продолжат работать в реальном времени параллельно тесту - результат непредсказуем. -> -> **`runTest` ждёт дочерние корутины, но не сами задачи `Dispatchers.Default`.** Тест может завершиться раньше реальной фоновой работы, а исключение из неё всплывёт в следующем тесте. -> -> **`currentTime` требует отдельного импорта.** Это свойство-расширение `TestScope`, а не член класса, поэтому `import kotlinx.coroutines.test.currentTime` обязателен - без него IDE покажет `unresolved reference` внутри `runTest`. В обычной `suspend`-функции вне `TestScope` его нет вовсе. -> -> **`runTest(timeout = ...)` - реальный таймаут, а не виртуальный.** Он ограничивает физическое время прогона и не связан с `currentTime`; виртуально тест может «прожить» часы. -> -> **Версии `kotlinx-coroutines-core` и `-test` должны совпадать.** Расхождение даёт `NoSuchMethodError` в рантайме, причём сообщение не указывает на причину. Держите версию в `libs.versions.toml`. Подробнее: [Gradle и Kotlin DSL](../Инструментарий/Gradle%20и%20Kotlin%20DSL.md) -> -> **В Kotest тело теста `suspend`, но времени это не виртуализирует.** `delay(100)` в спецификации занимает реальные 100 мс. Для виртуального времени оборачивайте в `runTest` явно. - -Далее: [Рефлексия](../Рефлексия/Рефлексия.md) diff --git a/Kotlin/Тестирование/Тестирование.md b/Kotlin/Тестирование/Тестирование.md deleted file mode 100644 index 0bc82b1..0000000 --- a/Kotlin/Тестирование/Тестирование.md +++ /dev/null @@ -1,366 +0,0 @@ ---- -title: Тестирование -description: Обзор инструментов тестирования Kotlin — kotlin.test, JUnit 5, Kotest, MockK, coroutines-test — и когда что выбирать -tags: - - kotlin - - testing ---- - -В Kotlin четыре слоя инструментов тестирования, и выбор на каждом уровне независим: фреймворк запуска, библиотека утверждений, библиотека моков и поддержка корутин. - -```kotlin -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - -class Calculator { - fun add(a: Int, b: Int) = a + b - fun divide(a: Int, b: Int): Int { - require(b != 0) { "деление на ноль" } - return a / b - } -} - -class CalculatorTest { - private val calc = Calculator() - - @Test - fun `сложение работает`() { - assertEquals(5, calc.add(2, 3)) - } - - @Test - fun `деление на ноль бросает исключение`() { - val e = assertFailsWith { calc.divide(1, 0) } - assertEquals("деление на ноль", e.message) - } -} -``` -Две особенности, заметные сразу: имена тестов в обратных кавычках читаются как предложения, а `assertFailsWith` возвращает исключение для дальнейших проверок. - -## Четыре слоя -```mermaid -graph TD - A["запуск тестов"] --> B["JUnit 5 Platform"] - B --> C["JUnit Jupiter
@Test"] - B --> D["Kotest engine
StringSpec, FunSpec"] - E["утверждения"] --> F["kotlin.test
assertEquals"] - E --> G["kotest-assertions
shouldBe"] - H["моки"] --> I["MockK"] - J["корутины"] --> K["kotlinx-coroutines-test
runTest"] -``` - -| Слой | Варианты | Рекомендация | -|---|---|---| -| движок запуска | JUnit 5, Kotest | JUnit 5 (универсальный) | -| утверждения | `kotlin.test`, kotest-assertions, AssertJ | `kotlin.test` + kotest при нужде | -| моки | MockK, Mockito-kotlin | MockK | -| корутины | `kotlinx-coroutines-test` | обязательно | -| свойства | kotest-property | при алгоритмах | - -Слои комбинируются: можно писать `@Test` из JUnit с утверждениями `shouldBe` из Kotest и моками MockK. - -## Настройка -```kotlin -// build.gradle.kts -plugins { - kotlin("jvm") version "2.4.0" -} - -repositories { mavenCentral() } - -dependencies { - testImplementation(kotlin("test")) - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0") - testImplementation("io.mockk:mockk:1.14.6") - testImplementation("io.kotest:kotest-assertions-core:6.0.4") - testImplementation("io.kotest:kotest-runner-junit5:6.0.4") - testImplementation("io.kotest:kotest-property:6.0.4") -} - -tasks.test { - useJUnitPlatform() - testLogging { - events("passed", "failed", "skipped") - } -} -``` -`testImplementation(kotlin("test"))` подтягивает `kotlin-test` версии, совпадающей с плагином. Без `useJUnitPlatform()` Gradle использует JUnit 4, и тесты JUnit 5 просто не найдутся. - -## Что выбрать -**`kotlin.test` + JUnit 5** - разумный дефолт. Многоплатформенный API утверждений, стандартный движок, вся инфраструктура (IDE, CI, отчёты) работает из коробки. Подробнее: [kotlin.test и JUnit 5](kotlin.test%20и%20JUnit%205.md) - -**Kotest** - когда нужны вложенные спецификации, data-driven тесты, property-based тестирование или богатые матчеры. Полноценный фреймворк со своим движком и DSL. Подробнее: [Kotest](Kotest.md) - -**MockK** - стандарт для моков в Kotlin. В отличие от Mockito, понимает `final` классы (а в Kotlin все классы `final` по умолчанию), `object`, расширения и `suspend`-функции. Подробнее: [MockK](MockK.md) - -**`kotlinx-coroutines-test`** - не опция, а необходимость, если в коде есть `suspend`. Даёт виртуальное время: `delay(1000)` проходит мгновенно. Подробнее: [Тестирование корутин](Тестирование%20корутин.md) - -## Структура тестов -``` -src/ -├── main/kotlin/com/example/ -│ └── UserService.kt -└── test/kotlin/com/example/ - └── UserServiceTest.kt -``` -Соглашение - зеркальная структура и суффикс `Test`. Тесты в том же пакете видят `internal`-члены: Gradle-плагин связывает test и main source sets как один модуль компиляции. - -```kotlin -import kotlin.test.Test -import kotlin.test.assertEquals - -internal class Repository { - internal fun load() = "данные" -} - -class RepositoryTest { - @Test - fun `internal доступен из теста`() { - assertEquals("данные", Repository().load()) - } -} -``` -Это важное преимущество: не нужно расширять видимость ради тестируемости. Подробнее: [Модификаторы видимости](../ООП/Модификаторы%20видимости.md) - -## Имена тестов -```kotlin -import kotlin.test.Test -import kotlin.test.assertTrue -import kotlin.test.assertEquals - -class NamingTest { - @Test - fun `пустой список не содержит элементов`() { - assertTrue(emptyList().isEmpty()) - } - - @Test - fun `given пустой кэш when запрос then возвращается null`() { - val cache = mutableMapOf() - assertEquals(null, cache["ключ"]) - } -} -``` -Обратные кавычки разрешают пробелы и кириллицу в именах функций. Это не трюк, а поддержанная возможность - именно для тестов. Ограничение: имя не может содержать `.`, `;`, `[`, `]`, `/`, `\`, `<`, `>`, `:` - это запрещено форматом класс-файла JVM. - -## Типичный тест сервиса -```kotlin -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - -data class User(val id: Int, val name: String) - -interface UserRepository { - fun findById(id: Int): User? -} - -class UserService(private val repo: UserRepository) { - fun displayName(id: Int): String = - repo.findById(id)?.name ?: throw NoSuchElementException("нет пользователя $id") -} - -class UserServiceTest { - // ручная заглушка вместо мока — для простых интерфейсов этого достаточно - private val repo = object : UserRepository { - private val users = mapOf(1 to User(1, "Иван")) - override fun findById(id: Int) = users[id] - } - private val service = UserService(repo) - - @Test - fun `существующий пользователь возвращает имя`() { - assertEquals("Иван", service.displayName(1)) - } - - @Test - fun `отсутствующий пользователь даёт исключение`() { - val e = assertFailsWith { service.displayName(99) } - assertEquals("нет пользователя 99", e.message) - } -} -``` -Для интерфейса с двумя методами ручная заглушка через `object :` короче и понятнее мока. Моки нужны, когда важно проверить **факт вызова** или настроить сложное поведение. - -## Что тестировать в Kotlin иначе -| Конструкция | На что обратить внимание | -|---|---| -| `data class` | `equals`/`hashCode` генерируются - тестировать не нужно | -| nullable-типы | тест на `null` обязателен: компилятор не проверит входные данные из Java | -| `sealed` + `when` | исчерпывающий `when` проверяется компилятором | -| значения по умолчанию | тестировать вызов и без аргументов, и со всеми | -| функции-расширения | тестируются как обычные функции | -| `suspend` | только через `runTest` | -| `Flow` | через `toList()` или `turbine` | -| `object` (singleton) | состояние живёт между тестами - сбрасывать | -| `lateinit` | тест на обращение до инициализации | - -```kotlin -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - -class Config { - lateinit var url: String -} - -class DefaultsTest { - fun greet(name: String, greeting: String = "Привет") = "$greeting, $name!" - - @Test - fun `значение по умолчанию применяется`() { - assertEquals("Привет, Мир!", greet("Мир")) - } - - @Test - fun `значение можно переопределить`() { - assertEquals("Hi, Мир!", greet("Мир", "Hi")) - } - - @Test - fun `lateinit до инициализации бросает исключение`() { - assertFailsWith { Config().url } - } -} -``` - -## Параметризованные тесты -```kotlin -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.CsvSource -import org.junit.jupiter.params.provider.ValueSource -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class ParameterizedExamples { - @ParameterizedTest - @ValueSource(strings = ["", " ", "\t"]) - fun `пустые строки распознаются`(input: String) { - assertTrue(input.isBlank()) - } - - @ParameterizedTest - @CsvSource("2, 3, 5", "0, 0, 0", "-1, 1, 0") - fun `сложение по таблице`(a: Int, b: Int, expected: Int) { - assertEquals(expected, a + b) - } -} -``` -В Kotest для той же задачи есть `withData` - читается лучше, но требует перехода на его спецификации. - -## Жизненный цикл -```kotlin -import kotlin.test.BeforeTest -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals - -class LifecycleTest { - private lateinit var items: MutableList - - @BeforeTest - fun setUp() { - items = mutableListOf("начальный") - } - - @AfterTest - fun tearDown() { - items.clear() - } - - @Test - fun `первый тест видит свежее состояние`() { - items += "добавленный" - assertEquals(2, items.size) - } - - @Test - fun `второй тест тоже видит свежее состояние`() { - assertEquals(1, items.size) - } -} -``` -По умолчанию JUnit создаёт новый экземпляр тестового класса на каждый тест, поэтому поля изолированы. Это делает `@BeforeTest` нужным только для дорогой инициализации - обычно достаточно инициализации поля напрямую: -```kotlin -import kotlin.test.Test -import kotlin.test.assertEquals - -class SimplerTest { - // новый экземпляр на каждый тест, поэтому список свежий - private val items = mutableListOf("начальный") - - @Test - fun `первый`() { - items += "ещё" - assertEquals(2, items.size) - } - - @Test - fun `второй`() { - assertEquals(1, items.size) - } -} -``` - -## Карта раздела -| Заметка | О чём | -|---|---| -| [kotlin.test и JUnit 5](kotlin.test%20и%20JUnit%205.md) | утверждения, аннотации, параметризация, жизненный цикл | -| [Kotest](Kotest.md) | спецификации, матчеры, data-driven, property-based | -| [MockK](MockK.md) | моки, стабы, верификация, `object`, расширения, `suspend` | -| [Тестирование корутин](Тестирование%20корутин.md) | `runTest`, виртуальное время, `TestDispatcher`, `Flow` | - -## Запуск -``` -./gradlew test # все тесты -./gradlew test --tests "com.example.UserServiceTest" # один класс -./gradlew test --tests "*UserService*" # по шаблону -./gradlew test --info # с выводом тестов -./gradlew test --rerun-tasks # игнорируя кеш -``` -Отчёт - в `build/reports/tests/test/index.html`. Gradle кеширует результаты: повторный `test` без изменений не запускает ничего, что иногда выглядит как «тесты не работают». - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `@Test void shouldAddNumbers()` | `@Test fun \`сложение работает\`()` | -> | `Assertions.assertEquals` | `kotlin.test.assertEquals` | -> | Mockito | MockK | -> | `@Mock`, `@InjectMocks` | `mockk()`, `every { }` | -> | AssertJ `assertThat(x).isEqualTo(y)` | kotest `x shouldBe y` | -> | нет аналога | `runTest` с виртуальным временем | -> | package-private для тестируемости | `internal` виден тестам | -> -> **Имена тестов в обратных кавычках - маленькая возможность с большим эффектом.** В Java имя теста - идентификатор, поэтому пишут `shouldReturnNullWhenCacheIsEmpty` или добавляют `@DisplayName`. В Kotlin имя функции может быть предложением, и отчёт о тестах читается как спецификация без дополнительных аннотаций. Ограничение - запрещённые символы JVM (`.`, `:`, `/` и другие), и на Android до API 30 обратные кавычки в именах не поддерживались вовсе. -> -> **`internal` решает проблему «package-private для тестов».** В Java тестируемые детали делают package-private, и тест обязан лежать в том же пакете - иначе нужен `public`. В Kotlin `internal` виден всему модулю, а Gradle связывает `test` и `main` в один модуль компиляции: тест видит `internal` независимо от пакета, но внешний код - нет. -> -> **MockK появился потому, что Mockito плохо работает с Kotlin.** Все классы в Kotlin `final` по умолчанию, а Mockito мокает через наследование - поэтому нужен `mockito-inline` или `all-open`-плагин. Плюс Mockito не умеет `object`, функции-расширения и `suspend`. MockK написан для Kotlin и покрывает всё это нативно, включая `coEvery` для корутин. -> -> **Виртуальное время для корутин - того, чего в Java нет.** Тест асинхронного Java-кода либо спит реальное время, либо требует сложной синхронизации через `CountDownLatch`. `runTest` пропускает `delay` мгновенно и гарантирует, что все корутины завершились до конца теста - это следствие структурированной конкурентности. Подробнее: [Структурированная конкурентность](../Корутины/Структурированная%20конкурентность.md) -> -> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md) - -> [!warning] Подводные камни -> **Без `useJUnitPlatform()` тесты JUnit 5 не запускаются.** Gradle по умолчанию использует JUnit 4, и `@Test` из `org.junit.jupiter` просто не находится. Вывод - «0 tests found», без ошибки. Это первое, что нужно проверить, если тесты «не видны». -> -> **Два разных `@Test`.** `kotlin.test.Test` - псевдоним, который на JVM указывает на нужный движок; `org.junit.jupiter.api.Test` - прямая аннотация JUnit 5; `org.junit.Test` - JUnit 4. Смешивание в одном проекте приводит к тому, что часть тестов не запускается. Используйте `kotlin.test.Test`, если нет причин для конкретного движка. -> -> **Обратные кавычки не принимают часть символов.** Имя `` `a.b` `` или `` `тест: случай` `` не скомпилируется: точка и двоеточие запрещены форматом класс-файла. Ошибка компилятора при этом не всегда очевидна. -> -> **`object` сохраняет состояние между тестами.** Singleton не пересоздаётся, поэтому мутация в одном тесте видна в другом, и результат зависит от порядка запуска. Такое состояние нужно сбрасывать в `@BeforeTest` или не использовать `object` для изменяемого состояния. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md) -> -> **Gradle кеширует результат `test`.** Повторный запуск без изменений кода выводит `UP-TO-DATE` и ничего не выполняет. Если нужно принудительно - `--rerun-tasks`. -> -> **`suspend`-функцию нельзя вызвать из обычного `@Test`.** Понадобится `runTest` (или `runBlocking`, что хуже - реальные задержки). Без него код не скомпилируется, но попытка обойти через `runBlocking` даёт медленные тесты. Подробнее: [Тестирование корутин](Тестирование%20корутин.md) -> -> **Тесты другого модуля не видят `internal`.** Связь `test`↔`main` работает в пределах одного Gradle-модуля. Тест в модуле `app`, проверяющий `internal` из модуля `core`, не скомпилируется. -> -> **Параллельный запуск тестов ломается на общем состоянии.** `maxParallelForks` ускоряет, но тесты, использующие `object`, статические поля, временные файлы с фиксированными именами или один порт, начинают случайно падать. Включайте параллельность только для изолированных тестов. -> -> **`assertEquals` различает `Int` и `Long`.** `assertEquals(1, 1L)` не скомпилируется (разные типы) - в отличие от Java, где сработало бы расширение типа. Это скорее помогает, но удивляет при миграции. -> -> **Тесты на `data class` обычно избыточны.** `equals`, `hashCode`, `toString`, `copy` генерируются компилятором и не содержат вашей логики. Тестировать стоит только кастомные члены и валидацию в `init`. - -Далее: [kotlin.test и JUnit 5](kotlin.test%20и%20JUnit%205.md) diff --git a/Kotlin/Функциональный Kotlin/DSL.md b/Kotlin/Функциональный Kotlin/DSL.md deleted file mode 100644 index e9bb5f2..0000000 --- a/Kotlin/Функциональный Kotlin/DSL.md +++ /dev/null @@ -1,444 +0,0 @@ ---- -title: DSL -description: Из чего собирается предметно-ориентированный язык на Kotlin и когда он оправдан -tags: - - kotlin - - functional - - dsl ---- - -DSL (domain-specific language) на Kotlin - это обычный Kotlin-код, который выглядит как декларативное описание. Отдельного механизма для этого нет: DSL собирается из четырёх уже знакомых возможностей. - -| Возможность | Что даёт | -|---|---| -| [лямбда с получателем](Receivers%20и%20context%20parameters.md) `T.() -> Unit` | блок, внутри которого доступны методы объекта | -| [лямбда последним аргументом](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md) | `html { }` вместо `html({ })` | -| [функции-расширения](Функции-расширения.md) | методы только внутри DSL | -| [операторы и `infix`](Инфиксные%20и%20операторные%20функции.md) | `"a" to 1`, `x shouldBe y` | - -## Минимальный пример -```kotlin -class Pizza { - private val toppings = mutableListOf() - var size = "средняя" - - fun topping(name: String) { toppings.add(name) } - - override fun toString() = "$size пицца: ${toppings.joinToString()}" -} - -fun pizza(block: Pizza.() -> Unit): Pizza = Pizza().apply(block) - -fun main() { - val order = pizza { - size = "большая" - topping("сыр") - topping("грибы") - } - println(order) // большая пицца: сыр, грибы -} -``` -Три строки инфраструктуры (`fun pizza(block: ...) = Pizza().apply(block)`) превращают класс в DSL. Это весь базовый приём. - -## Вложенные блоки -```kotlin -@DslMarker -annotation class HtmlDsl - -@HtmlDsl -class Tag(private val name: String) { - private val children = mutableListOf() - private val attributes = mutableMapOf() - private var text: String? = null - - fun attr(key: String, value: String) { attributes[key] = value } - - operator fun String.unaryPlus() { text = this } - - fun tag(name: String, block: Tag.() -> Unit = {}) { - children.add(Tag(name).apply(block)) - } - - fun div(block: Tag.() -> Unit) = tag("div", block) - fun p(block: Tag.() -> Unit) = tag("p", block) - - fun render(): String { - val attrs = attributes.entries.joinToString("") { " ${it.key}=\"${it.value}\"" } - val inner = text ?: children.joinToString("") { it.render() } - return "<$name$attrs>$inner" - } -} - -fun html(block: Tag.() -> Unit): String = Tag("html").apply(block).render() - -fun main() { - val page = html { - div { - attr("class", "container") - p { +"Первый абзац" } - p { +"Второй абзац" } - } - } - println(page) - //

Первый абзац

Второй абзац

-} -``` -Разбор приёмов: -- `Tag.() -> Unit` даёт вложенность: внутри `div { }` доступны методы `Tag` -- `operator fun String.unaryPlus()` даёт синтаксис `+"текст"` для содержимого -- `@DslMarker` запрещает вызвать `div { }` изнутри `div { }` через внешний получатель -- значение по умолчанию `block: Tag.() -> Unit = {}` разрешает теги без тела - -## Типобезопасность -Главное отличие Kotlin-DSL от строковых шаблонов: ошибки видит компилятор. -```kotlin -class Query { - private var table: String? = null - private val conditions = mutableListOf() - private var limit: Int? = null - - fun from(name: String) { table = name } - fun where(condition: String) { conditions.add(condition) } - fun limit(n: Int) { limit = n } - - fun build(): String { - val t = requireNotNull(table) { "не указана таблица" } - return buildString { - append("SELECT * FROM $t") - if (conditions.isNotEmpty()) append(" WHERE ${conditions.joinToString(" AND ")}") - limit?.let { append(" LIMIT $it") } - } - } -} - -fun query(block: Query.() -> Unit): String = Query().apply(block).build() - -fun main() { - println(query { - from("users") - where("age > 18") - where("active = true") - limit(10) - }) - // SELECT * FROM users WHERE age > 18 AND active = true LIMIT 10 - - try { - query { where("x = 1") } - } catch (e: IllegalArgumentException) { - println(e.message) // не указана таблица - } -} -``` -Проверка обязательных полей здесь в рантайме. Вынести её в компиляцию можно через типы-состояния: -```kotlin -class EmptyQuery { - fun from(table: String) = TableQuery(table) -} - -class TableQuery(private val table: String) { - private val conditions = mutableListOf() - - fun where(condition: String): TableQuery = apply { conditions.add(condition) } - - fun build(): String = - "SELECT * FROM $table" + - if (conditions.isEmpty()) "" else " WHERE ${conditions.joinToString(" AND ")}" -} - -fun main() { - println(EmptyQuery().from("orders").where("total > 100").build()) - // SELECT * FROM orders WHERE total > 100 - - // println(EmptyQuery().build()) // ошибка компиляции: метода нет -} -``` -Такой приём (phantom types) делает невозможное состояние непредставимым, но усложняет код. Для большинства DSL достаточно `require` в `build()`. - -## Коллекции внутри DSL -```kotlin -class Routes { - private val routes = mutableListOf>() - - infix fun String.handledBy(handler: String) { routes.add(this to handler) } - - fun group(prefix: String, block: Routes.() -> Unit) { - val nested = Routes().apply(block) - nested.routes.forEach { (path, handler) -> routes.add("$prefix$path" to handler) } - } - - fun render() = routes.joinToString("\n") { "${it.first} -> ${it.second}" } -} - -fun routes(block: Routes.() -> Unit) = Routes().apply(block).render() - -fun main() { - println(routes { - "/" handledBy "HomeController" - group("/api") { - "/users" handledBy "UsersController" - "/orders" handledBy "OrdersController" - } - }) - // / -> HomeController - // /api/users -> UsersController - // /api/orders -> OrdersController -} -``` -`infix` расширение внутри класса (`String.handledBy`) существует только внутри блока `routes { }` - снаружи такого метода у `String` нет. - -## Готовые DSL в stdlib -```kotlin -fun main() { - val text = buildString { - append("Привет") - appendLine(", мир") - append("Вторая строка") - } - println(text) - - val list = buildList { - add(1) - addAll(listOf(2, 3)) - } - println(list) // [1, 2, 3] - - val map = buildMap { - put("a", 1) - putAll(mapOf("b" to 2)) - } - println(map) // {a=1, b=2} -} -``` -Эти функции - ровно тот же приём `T.() -> Unit` + `apply`. - -Числовые и временные DSL: -```kotlin -import kotlin.time.Duration.Companion.seconds -import kotlin.time.Duration.Companion.minutes - -fun main() { - val timeout = 2.minutes + 30.seconds - println(timeout) // 2m 30s - println(timeout.inWholeSeconds) // 150 - - println((1..10 step 2).toList()) // [1, 3, 5, 7, 9] - println((10 downTo 1 step 3).toList()) // [10, 7, 4, 1] -} -``` -`2.minutes` - свойство-расширение на `Int`. Такой «единичный» DSL делается одной строкой и заметно улучшает читаемость конфигураций. - -## Известные DSL в экосистеме -| DSL | Для чего | -|---|---| -| Gradle Kotlin DSL | сборка проекта, `build.gradle.kts` | -| Ktor | маршруты и конфигурация сервера | -| kotlinx.html | генерация HTML | -| Exposed | SQL-запросы с типобезопасностью | -| Kotest | описание тестов и матчеры | -| Compose | описание UI | - -Пример в стиле Gradle - разбор см. [Gradle и Kotlin DSL](../Инструментарий/Gradle%20и%20Kotlin%20DSL.md): -```kotlin -class Dependencies { - private val list = mutableListOf() - fun implementation(dep: String) { list.add("implementation $dep") } - fun testImplementation(dep: String) { list.add("testImplementation $dep") } - fun render() = list.joinToString("\n") -} - -class Project2 { - private var name = "unnamed" - private var deps = Dependencies() - - fun name(value: String) { name = value } - fun dependencies(block: Dependencies.() -> Unit) { deps = Dependencies().apply(block) } - - fun render() = "project $name\n${deps.render()}" -} - -fun project(block: Project2.() -> Unit) = Project2().apply(block).render() - -fun main() { - println(project { - name("demo") - dependencies { - implementation("kotlin-stdlib") - testImplementation("kotest") - } - }) - // project demo - // implementation kotlin-stdlib - // testImplementation kotest -} -``` - -## Когда DSL оправдан -| Признак | Вывод | -|---|---| -| структура описывается многократно, вид стабилен | DSL уместен | -| вложенная иерархия (UI, конфиг, разметка) | DSL уместен | -| писать будут люди, не знающие внутренностей | DSL уместен | -| альтернатива - длинный конструктор или `Map` | DSL уместен | -| одно-два места использования | обычный код или `data class` | -| нужна сериализация/валидация схемы | формат данных (JSON/YAML) + парсер | -| структура меняется каждую неделю | DSL придётся переписывать | - -Главная цена DSL - его нужно изучать. Для внутреннего инструмента, который используют трое, именованные аргументы `data class` часто выигрывают: -```kotlin -data class ServerConfig( - val host: String = "localhost", - val port: Int = 8080, - val tls: Boolean = false, - val timeoutSeconds: Int = 30, -) - -fun main() { - val config = ServerConfig(host = "example.com", port = 443, tls = true) - println(config) - // ServerConfig(host=example.com, port=443, tls=true, timeoutSeconds=30) -} -``` -Это читается не хуже DSL, проверяется компилятором лучше, бесплатно получает `copy`, `equals` и сериализацию. DSL нужен, когда структура **вложенная** и **повторяющаяся** - там, где `data class` требует громоздких вложенных конструкторов. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | fluent builder с `return this` | `T.() -> Unit` | -> | XML/YAML/properties для конфигурации | типобезопасный DSL | -> | анонимный класс с методами | лямбда с получателем | -> | строковая конкатенация SQL | DSL с проверкой типов | -> | нет аналога | `@DslMarker` | -> -> **В Java DSL ограничен цепочками методов.** Максимум, что доступно - fluent interface: -> ```java -> Html.create() -> .div().attr("class", "container") -> .p().text("абзац").end() -> .end() -> .render(); -> ``` -> Вложенность приходится выражать парами `.div()`/`.end()`, и компилятор не проверит их баланс. В Kotlin вложенность выражается вложенностью блоков - забыть «закрывающий тег» физически невозможно. -> -> **Это причина, по которой Gradle перешёл на Kotlin.** `build.gradle` на Groovy был динамическим: опечатка в имени свойства обнаруживалась при запуске сборки, автодополнения не было. `build.gradle.kts` - обычный Kotlin-код, поэтому IDE знает все доступные методы, а ошибка в имени не компилируется. Цена - более медленная первая сборка (скрипт нужно скомпилировать). -> -> **Конфигурация в коде вместо формата данных.** В Java-мире окружение описывают в XML (Spring, Maven) или YAML (Spring Boot). Kotlin-DSL даёт проверку типов и переиспользование (функции, циклы, условия прямо в конфиге), но теряет главное свойство данных: конфигурацию нельзя прочитать и изменить без компилятора. Для конфигурации, которую правят администраторы, формат данных остаётся правильным выбором. -> -> **Лямбда с получателем - ключевая возможность, которой нет в Java.** Без неё все остальные приёмы (операторы, расширения) не складываются в DSL, поэтому в Java аналога попросту не существует. -> -> Сравнить: [Builder](../../Программирование%20на%20Java/Паттерны%20проектирования/Builder.md), [Интерфейсы в Java](../../Программирование%20на%20Java/ООП/Интерфейсы.md) - -> [!warning] Подводные камни -> **Без `@DslMarker` внешний получатель виден изнутри:** -> ```kotlin -> class Node { -> val children = mutableListOf() -> fun child(name: String) { children.add(name) } -> fun nested(block: Node.() -> Unit) { children.add(Node().apply(block).children.toString()) } -> } -> -> fun main() { -> val n = Node().apply { -> child("a") -> nested { child("b") } -> } -> println(n.children) // [a, [b]] -> } -> ``` -> Здесь всё верно, но стоит добавить метод, которого нет у внутреннего уровня, и вызов молча уйдёт на внешний. `@DslMarker` стоит ставить сразу. -> -> **DSL скрывает порядок выполнения:** -> ```kotlin -> class Steps { -> private val log = mutableListOf() -> fun step(name: String) { log.add(name) } -> fun render() = log.joinToString(" -> ") -> } -> -> fun main() { -> val s = Steps().apply { -> step("первый") -> step("второй") -> } -> println(s.render()) // первый -> второй -> } -> ``` -> Блок выглядит декларативно, но это императивный код: вызовы выполняются по порядку, и переставить строки - значит изменить результат. Пользователь DSL об этом не догадывается. -> -> **Изменяемое состояние в билдере переиспользуется:** -> ```kotlin -> class Builder2 { -> private val items = mutableListOf() -> fun item(x: String) { items.add(x) } -> fun build() = items.toList() -> } -> -> fun main() { -> val b = Builder2() -> b.item("a") -> val first = b.build() -> b.item("b") -> val second = b.build() -> println("$first / $second") // [a] / [a, b] -> } -> ``` -> Если билдер доступен снаружи, повторный `build()` даёт другой результат. Делайте билдер одноразовым: создавайте его внутри функции-входа и не возвращайте. -> -> **`var` в DSL без валидации допускает некорректное состояние:** -> ```kotlin -> class Window { -> var width = 0 -> var height = 0 -> fun area() = width * height -> } -> -> fun window(block: Window.() -> Unit) = Window().apply(block) -> -> fun main() { -> println(window { width = -5; height = 10 }.area()) // -50 -> } -> ``` -> Нужна проверка в `build()`/`apply` или сеттер с `require`. -> -> **Вложенные лямбды с получателем трудно отлаживать.** Стек вызовов состоит из `invoke`-методов сгенерированных лямбд, а точка останова внутри блока не показывает, какому уровню она принадлежит. Для глубоких DSL это заметная проблема. -> -> **DSL без документации непригоден.** Пользователь видит `pizza { }` и не знает, какие методы доступны - IDE подскажет, но ревью в браузере и примеры в README необходимы. Это основной скрытый расход на DSL. -> -> **Методы DSL попадают в автодополнение не там, где ожидалось.** Если расширение объявлено на верхнем уровне (а не внутри класса DSL), оно появится везде: -> ```kotlin -> // плохо: operator fun String.unaryPlus() на верхнем уровне сломает код, -> // где +"строка" означает что-то другое -> class Container2 { -> private val parts = mutableListOf() -> operator fun String.unaryPlus() { parts.add(this) } // только внутри Container2 -> fun render() = parts.joinToString() -> } -> -> fun container(block: Container2.() -> Unit) = Container2().apply(block).render() -> -> fun main() { println(container { +"a"; +"b" }) } // a, b -> ``` -> Объявляйте операторы DSL **внутри** класса-контекста. -> -> **`apply` возвращает билдер, а не результат:** -> ```kotlin -> class Doc { -> private val lines = mutableListOf() -> fun line(x: String) { lines.add(x) } -> fun render() = lines.joinToString("\n") -> } -> -> fun docWrong(block: Doc.() -> Unit) = Doc().apply(block) // возвращает Doc -> fun docRight(block: Doc.() -> Unit) = Doc().apply(block).render() // возвращает String -> -> fun main() { -> println(docWrong { line("a") }::class.simpleName) // Doc -> println(docRight { line("a") }) // a -> } -> ``` -> Утечка билдера наружу позволяет менять «готовый» объект после сборки. -> -> **DSL - не замена валидации данных.** Типобезопасность проверяет структуру, а не смысл: `port = 70000` скомпилируется. Проверки диапазонов, обязательных полей и взаимных зависимостей нужно писать вручную. Подробнее: [Проверки и контракты](../Исключения/Проверки%20и%20контракты.md) -> -> **DSL тяжело версионировать.** Переименование метода - несовместимое изменение для всех пользователей, а `@Deprecated` на методах внутри класса-контекста работает, но предупреждения в больших блоках теряются. - -Далее: [Паттерны проектирования](Паттерны%20проектирования.md) diff --git a/Kotlin/Функциональный Kotlin/Inline-функции.md b/Kotlin/Функциональный Kotlin/Inline-функции.md deleted file mode 100644 index dabb0ae..0000000 --- a/Kotlin/Функциональный Kotlin/Inline-функции.md +++ /dev/null @@ -1,322 +0,0 @@ ---- -title: Inline-функции -description: Подстановка тела на этапе компиляции, noinline и crossinline, нелокальный return и цена inline -tags: - - kotlin - - functional - - performance ---- - -`inline` говорит компилятору: не вызывай эту функцию, а подставь её тело в место вызова. Для обычных функций это почти бесполезно (JIT справляется сам), а для функций с лямбдами - убирает объект лямбды. - -```kotlin -inline fun measure(block: () -> Unit): Long { - val start = System.nanoTime() - block() - return System.nanoTime() - start -} - -fun main() { - val time = measure { (1..1000).sum() } - println(time > 0) // true -} -``` -После компиляции в `main` не остаётся ни вызова `measure`, ни объекта `Function0` - там просто `val start = ...; (1..1000).sum(); ...`. - -## Что именно экономится -Без `inline` на каждый вызов создаётся объект, реализующий `Function0`: -```kotlin -fun notInlined(block: () -> Int): Int = block() -inline fun inlined(block: () -> Int): Int = block() - -fun main() { - var captured = 0 - - repeat(3) { notInlined { captured + 1 } } // лямбда захватывает captured -> новый объект каждый раз - repeat(3) { inlined { captured + 1 } } // объектов нет вообще - - println(notInlined { 42 }) // 42 - println(inlined { 42 }) // 42 -} -``` -Лямбда **без захвата** компилируется в синглтон, поэтому объект создаётся один раз - тут `inline` экономит только вызов. Лямбда **с захватом** создаёт объект на каждое вычисление, и вот здесь разница существенна. - -Поэтому все операции над коллекциями в stdlib объявлены `inline`: -```kotlin -fun main() { - val sum = listOf(1, 2, 3).filter { it > 1 }.sumOf { it * 2 } - println(sum) // 10 -} -``` -В байт-коде это два обычных цикла `for` с телами лямбд внутри. Ни одного объекта лямбды и ни одного виртуального вызова - именно поэтому короткие цепочки в Kotlin обычно быстрее Java-стримов. - -## Нелокальный return -Второе следствие подстановки: из inline-лямбды можно выйти из внешней функции. -```kotlin -inline fun List.forEachInline(action: (T) -> Unit) { for (item in this) action(item) } - -fun findFirstNegative(numbers: List): Int? { - numbers.forEachInline { if (it < 0) return it } // выход из findFirstNegative - return null -} - -fun main() { - println(findFirstNegative(listOf(1, 2, -3, 4))) // -3 - println(findFirstNegative(listOf(1, 2))) // null -} -``` -Это работает, потому что `return` оказывается физически внутри `findFirstNegative`. В не-inline функции такой `return` запрещён: -```kotlin -fun List.forEachSlow(action: (T) -> Unit) { for (item in this) action(item) } - -fun firstNegative2(numbers: List): Int? { - numbers.forEachSlow { if (it < 0) return@forEachSlow } // только локальный выход - return null -} - -fun main() { println(firstNegative2(listOf(-1))) } // null -``` -Именно поэтому `forEach` из stdlib ведёт себя как `continue`, а не как `break`: -```kotlin -fun main() { - listOf(1, 2, 3).forEach { - if (it == 2) return@forEach // пропустить элемент - print("$it ") - } - println() // 1 3 -} -``` - -## noinline -Если лямбду нужно сохранить или передать дальше, подставить её нельзя - у подставленного тела нет объекта: -```kotlin -inline fun register( - name: String, - noinline handler: (String) -> Unit, - validator: (String) -> Boolean, -): (String) -> Unit { - println("регистрирую $name, валиден: ${validator(name)}") - return handler // возвращаем объект — нужен noinline -} - -fun main() { - val h = register("событие", handler = { println("обработано: $it") }, validator = { it.isNotEmpty() }) - h("данные") - // регистрирую событие, валиден: true - // обработано: данные -} -``` -Без `noinline` компилятор откажется: «Illegal usage of inline-parameter». Правило: лямбду, которая только вызывается внутри функции, можно инлайнить; лямбду, которую сохраняют в поле, возвращают или передают в не-inline функцию - нельзя. - -## crossinline -Промежуточный случай: лямбда вызывается не напрямую, а внутри другой лямбды или объекта. -```kotlin -inline fun runLater(crossinline block: () -> Unit): Runnable = Runnable { block() } - -fun main() { - val task = runLater { println("выполнено позже") } - task.run() // выполнено позже -} -``` -`crossinline` означает «инлайнить можно, но нелокальный `return` запрещён». Логика такая: тело лямбды подставляется внутрь `Runnable`, а оттуда выйти из вызывающей функции физически невозможно - она уже завершилась. - -```kotlin -inline fun repeatTwice(crossinline block: () -> Unit) { - val wrapped = { block(); block() } - wrapped() -} - -fun demo() { - repeatTwice { println("раз") } - // repeatTwice { return } // ошибка: нелокальный return запрещён -} - -fun main() { demo() } // раз \n раз -``` - -| Модификатор | Лямбда подставляется | Нелокальный `return` | Можно сохранить/передать | -|---|---|---|---| -| (по умолчанию) | да | да | нет | -| `crossinline` | да | нет | только внутрь другой лямбды | -| `noinline` | нет | нет | да | - -## reified -Третье следствие подстановки: в месте вызова известен фактический тип, поэтому параметр типа можно «материализовать». -```kotlin -inline fun filterByType(items: List): List = items.filterIsInstance() - -fun main() { - val mixed = listOf(1, "a", 2.0, 3) - println(filterByType(mixed)) // [1, 3] - println(filterByType(mixed)) // [a] -} -``` -Это основная причина объявлять `inline` функцию без лямбд вообще. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md) - -## Inline-свойства -`inline` применим и к аксессорам: -```kotlin -class Config2(val map: Map) { - inline val isEmpty: Boolean get() = map.isEmpty() - - val host: String - inline get() = map["host"] ?: "localhost" -} - -fun main() { - val c = Config2(mapOf("host" to "example.com")) - println("${c.isEmpty} ${c.host}") // false example.com -} -``` -Обратите внимание: `map` объявлен `val` без `private`. Для `private val` сниппет не скомпилируется - на inline-аксессоры действует то же правило, что на inline-функции: подставленное тело окажется в чужом классе, где приватный член недоступен. - -Выгода минимальна - JIT инлайнит такие геттеры и сам. Реально `inline`-свойства нужны редко: когда геттер должен использовать `reified` или когда свойство - часть публичного API, которое не хочется оставлять виртуальным. - -## Когда inline оправдан -| Случай | Нужен ли `inline` | -|---|---| -| функция принимает лямбду, которая вызывается сразу | да | -| нужен `reified` | да, обязательно | -| нужен нелокальный `return` из лямбды | да, обязательно | -| функция без лямбд и без `reified` | нет, JIT справится | -| тело большое (десятки строк) | нет, байт-код раздуется | -| функция рекурсивная | нельзя | -| публичное API библиотеки | осторожно - см. подводные камни | - -## Что происходит с байт-кодом -```kotlin -inline fun twice(block: () -> Unit) { block(); block() } - -fun main() { - var count = 0 - twice { count++ } - twice { count += 10 } - println(count) // 22 -} -``` -Тело `twice` скопировано в `main` дважды, и каждая копия содержит своё тело лямбды - итого четыре инкремента в коде. Для короткой функции это дешевле вызова; для функции на сто строк, вызываемой из пятидесяти мест, это пять тысяч строк байт-кода. - -Компилятор предупреждает, когда `inline` бесполезен: -```kotlin -inline fun uselessInline(x: Int): Int = x * 2 // warning: expected performance impact is insignificant - -fun main() { println(uselessInline(2)) } // 4 -``` -Такое предупреждение - сигнал убрать `inline`, если не нужен `reified`. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | нет inline на уровне языка | `inline fun` | -> | JIT инлайнит в рантайме | компилятор инлайнит в байт-код | -> | лямбда всегда объект | при `inline` объекта нет | -> | `return` из лямбды невозможен | нелокальный `return` при `inline` | -> | `Class` параметром | `reified T` | -> | `final` подсказывает JIT | `inline` - директива компилятору | -> -> **Java инлайнит в рантайме, Kotlin - при компиляции.** HotSpot инлайнит горячие методы агрессивнее, чем любой компилятор: он знает фактические типы, статистику ветвлений и может деоптимизировать решение. Поэтому `inline` в Kotlin - не про скорость исполнения тела, а про три вещи, недоступные JIT: отсутствие объекта лямбды, нелокальный `return` и `reified`. -> -> **Escape analysis частично закрывает ту же проблему в Java.** Если JIT доказывает, что объект лямбды не покидает метод, он может не выделять его в куче (scalar replacement). Но это оптимизация «на удачу»: она применяется не всегда и ломается при полиморфном вызове. `inline` в Kotlin гарантирован и виден в байт-коде. -> -> **Нелокального `return` в Java нет вообще.** Из тела `forEach` нельзя выйти из внешнего метода - приходится либо возвращаться к обычному `for`, либо бросать исключение, либо использовать `findFirst` на стриме. Kotlin позволяет писать абстракции обхода, которые ведут себя как встроенный цикл: -> ```kotlin -> inline fun Iterable.firstMatching(p: (T) -> Boolean): T? { -> for (item in this) if (p(item)) return item -> return null -> } -> -> fun main() { println(listOf(1, 2, 3).firstMatching { it > 1 }) } // 2 -> ``` -> -> **Цена - совместимость библиотек.** В Java изменение тела метода не требует перекомпиляции клиентов. В Kotlin тело inline-функции физически скопировано в байт-код клиента, поэтому исправление в библиотеке не подействует до пересборки вызывающего кода. Для публичного API это серьёзный аргумент, и stdlib помечает такие функции `@PublishedApi`, когда они обращаются к internal-коду. -> -> **Inline-функцию нельзя вызвать из Java** с сохранением смысла: Java-компилятор не умеет подставлять тело, он вызовет сгенерированный метод как обычный. `reified` при этом не работает. -> -> Сравнить: [JIT в Java](../../Программирование%20на%20Java/JVM/JIT.md), [Байт-код](../../Программирование%20на%20Java/JVM/Байт-код.md) - -> [!warning] Подводные камни -> **Inline раздувает байт-код.** Тело копируется в каждое место вызова. Для больших функций это рост размера класса, давление на кеш инструкций процессора и более медленная загрузка классов. `inline` оправдан для коротких функций - в stdlib почти все inline-функции занимают несколько строк. -> -> **Изменение тела inline-функции не действует без перекомпиляции клиентов:** -> ```kotlin -> inline fun version() = "v1" // если выпустить v2, клиенты со старой сборкой увидят v1 -> -> fun main() { println(version()) } // v1 -> ``` -> Это ломает обычные ожидания от семантического версионирования библиотек. -> -> **Inline-функция не видит `private` членов своего класса:** -> ```kotlin -> class Service3 { -> private val secret = "скрыто" -> // inline fun leak() = secret // ошибка: public-inline не может обращаться к private -> fun ok() = secret -> internal inline fun fine() = ok() // а так можно -> } -> -> fun main() { println(Service3().fine()) } // скрыто -> ``` -> Причина: тело окажется в чужом классе, где `private` недоступен. Обход - `@PublishedApi internal` на члене. -> -> **Рекурсивную функцию нельзя сделать inline:** -> ```kotlin -> // inline fun fact(n: Int): Int = if (n <= 1) 1 else n * fact(n - 1) // ошибка -> fun fact(n: Int): Int = if (n <= 1) 1 else n * fact(n - 1) -> -> fun main() { println(fact(5)) } // 120 -> ``` -> Подстановка была бы бесконечной. -> -> **Лямбду из inline-параметра нельзя сохранить:** -> ```kotlin -> inline fun broken(block: () -> Unit) { -> // val saved = block // ошибка: Illegal usage of inline-parameter -> block() -> } -> -> inline fun fixed(noinline block: () -> Unit): () -> Unit = block -> -> fun main() { -> broken { println("ок") } -> fixed { println("сохранено") }() -> } -> ``` -> -> **`crossinline` запрещает нелокальный `return`, и ошибка возникает у пользователя функции**, а не у автора. Если лямбда оборачивается в `Runnable`, объект или другую лямбду - `crossinline` обязателен, и вызывающий код теряет возможность выйти из своей функции. -> -> **Нелокальный `return` из inline-лямбды пропускает код после вызова:** -> ```kotlin -> inline fun withLogging(block: () -> Unit) { -> println("до") -> block() -> println("после") // не выполнится при return из лямбды -> } -> -> fun run1() { -> withLogging { return } -> } -> -> fun main() { run1(); println("конец") } // до \n конец -> ``` -> Строка «после» потерялась. Если в функции есть освобождение ресурсов, его нужно ставить в `finally` - иначе нелокальный `return` его обойдёт: -> ```kotlin -> inline fun safeLogging(block: () -> Unit) { -> println("до") -> try { block() } finally { println("после") } -> } -> -> fun run2() { safeLogging { return } } -> -> fun main() { run2() } // до \n после -> ``` -> Это важнее, чем кажется: `use`, `withLock` и транзакционные обёртки обязаны использовать `finally` именно по этой причине. -> -> **`inline` на функции без лямбд вызывает предупреждение** и почти не влияет на производительность. Единственные оправдания - `reified` и необходимость подставить тело в клиентский код. -> -> **Внутри inline-лямбды нельзя использовать `suspend`-вызовы, если не помечено специально.** Для функций вроде `measure` это выглядит рабочим, но обёртка над `suspend`-кодом требует либо `suspend inline`, либо `crossinline` и явного учёта контекста. Подробнее: [Suspend-функции](../Корутины/Suspend-функции.md) -> -> **Инлайн не убирает упаковку примитивов в функциональных типах автоматически** - но при полной подстановке упаковки не остаётся, потому что лямбды как объекта нет. А вот с `noinline` параметром `(Int) -> Int` упаковка сохраняется. -> -> **Профилируйте, прежде чем добавлять `inline` вручную.** Накладные расходы лямбды измеримы только в горячем коде с миллионами вызовов. В обычном бизнес-коде преждевременный `inline` даёт лишь раздутый байт-код и проблемы совместимости. - -Далее: [Функции-расширения](Функции-расширения.md) diff --git a/Kotlin/Функциональный Kotlin/Receivers и context parameters.md b/Kotlin/Функциональный Kotlin/Receivers и context parameters.md deleted file mode 100644 index 913a6c2..0000000 --- a/Kotlin/Функциональный Kotlin/Receivers и context parameters.md +++ /dev/null @@ -1,439 +0,0 @@ ---- -title: Receivers и context parameters -description: Лямбды с получателем, несколько получателей в области видимости, DslMarker и context parameters вместо context receivers -tags: - - kotlin - - functional ---- - -Получатель (receiver) - это объект, доступный в блоке кода как `this` без явного указания. На этом механизме построены расширения, scope-функции, билдеры и все DSL в Kotlin. - -```kotlin -fun StringBuilder.addLine(text: String) { - append(text) // this.append — this есть неявно - append("\n") -} - -fun main() { - val sb = StringBuilder() - sb.addLine("первая") - sb.addLine("вторая") - print(sb) - // первая - // вторая -} -``` - -## Функциональный тип с получателем -Тип `T.() -> R` - это функция, внутри которой `this` имеет тип `T`: -```kotlin -fun buildText(block: StringBuilder.() -> Unit): String { - val sb = StringBuilder() - sb.block() // вызываем лямбду с sb как получателем - return sb.toString() -} - -fun main() { - val text = buildText { - append("Привет") // this == StringBuilder - append(", ") - append("мир") - } - println(text) // Привет, мир -} -``` -Разница с обычным `(StringBuilder) -> Unit` только в том, как обращаться к объекту: -```kotlin -fun withReceiver(block: StringBuilder.() -> Unit) = StringBuilder().apply(block).toString() -fun withParameter(block: (StringBuilder) -> Unit) = StringBuilder().also(block).toString() - -fun main() { - println(withReceiver { append("через this") }) // через this - println(withParameter { it.append("через it") }) // через it -} -``` -Типы взаимозаменяемы при передаче: лямбда с получателем подходит там, где ожидается функция с параметром, и наоборот - компилятор конвертирует их сам. - -Так устроены все scope-функции: -```kotlin -// В stdlib: -// inline fun T.run(block: T.() -> R): R = block() -// inline fun T.apply(block: T.() -> Unit): T { block(); return this } -// inline fun with(receiver: T, block: T.() -> R): R = receiver.block() -// inline fun T.let(block: (T) -> R): R = block(this) -// inline fun T.also(block: (T) -> Unit): T { block(this); return this } -``` -`run`/`apply`/`with` принимают `T.() -> R`, `let`/`also` - `(T) -> R`. Вот и всё различие между `this` и `it` в [scope-функциях](Scope-функции.md). - -## Два получателя одновременно -Расширение, объявленное внутри класса, имеет **два** получателя: экземпляр класса (dispatch receiver) и объект расширения (extension receiver). -```kotlin -class HtmlBuilder { - private val sb = StringBuilder() - - fun String.tag(name: String) { // this: String, this@HtmlBuilder: HtmlBuilder - sb.append("<$name>").append(this).append("") - } - - fun build(block: HtmlBuilder.() -> Unit): String { - block() - return sb.toString() - } -} - -fun main() { - val html = HtmlBuilder().build { - "Заголовок".tag("h1") - "Текст".tag("p") - } - println(html) //

Заголовок

Текст

-} -``` -Внутри `tag` доступны оба: `this` (или `this@tag`) - строка, `this@HtmlBuilder` - билдер. Приоритет у ближайшего, то есть у extension receiver. - -## Квалифицированный this -Когда получателей несколько, их различают метками: -```kotlin -class Outer2 { - val name = "внешний" - - inner class Inner3 { - val name = "внутренний" - - fun show(): String { - val local = "локальный" - return "$name / ${this@Inner3.name} / ${this@Outer2.name} / $local" - } - } -} - -fun main() { - println(Outer2().Inner3().show()) - // внутренний / внутренний / внешний / локальный -} -``` -То же в лямбдах с получателем: -```kotlin -fun main() { - val result = StringBuilder().apply { - append("a") - "b".run { - this@apply.append(this) // this — String "b", this@apply — StringBuilder - this@apply.append("c") - } - } - println(result) // abc -} -``` -Метка берётся из имени функции, принимающей лямбду. Поэтому для двух вложенных `run` метка `this@run` указала бы на **внутренний** - ближайший одноимённый. Когда уровней больше одного, выбирайте разные scope-функции или явные имена параметров; если имени нет (анонимная функция), доступ только через параметры. - -## Вложенные билдеры и DslMarker -Проблема вложенных получателей: внутренний блок видит и внешний. -```kotlin -class Table { - private val rows = mutableListOf() - fun row(block: Row.() -> Unit) { rows.add(Row().apply(block).render()) } - fun render() = rows.joinToString("\n") -} - -class Row { - private val cells = mutableListOf() - fun cell(text: String) { cells.add(text) } - fun render() = cells.joinToString(" | ") -} - -fun table(block: Table.() -> Unit) = Table().apply(block).render() - -fun main() { - println(table { - row { - cell("a") - cell("b") - // row { } // компилируется! внешний получатель видим - } - row { cell("c") } - }) - // a | b - // c -} -``` -Вложенный `row { }` внутри `row` - логическая ошибка, но компилятор её не видит. Решение - аннотация `@DslMarker`: -```kotlin -@DslMarker -annotation class TableDsl - -@TableDsl -class Table2 { - private val rows = mutableListOf() - fun row(block: Row2.() -> Unit) { rows.add(Row2().apply(block).render()) } - fun render() = rows.joinToString("\n") -} - -@TableDsl -class Row2 { - private val cells = mutableListOf() - fun cell(text: String) { cells.add(text) } - fun render() = cells.joinToString(" | ") -} - -fun table2(block: Table2.() -> Unit) = Table2().apply(block).render() - -fun main() { - println(table2 { - row { - cell("a") - // row { } // теперь ошибка компиляции - } - }) - // a -} -``` -`@DslMarker` запрещает неявный доступ к внешнему получателю того же «семейства». Явный `this@table2.row { }` при этом остаётся доступен. Подробнее: [DSL](DSL.md) - -## Получатель в расширении для DSL -Полезный приём - расширение, доступное только внутри определённого контекста: -```kotlin -class Validator { - private val errors = mutableListOf() - - fun String.required(field: String) { - if (isBlank()) errors.add("$field обязателен") - } - - fun String.maxLength(field: String, max: Int) { - if (length > max) errors.add("$field длиннее $max") - } - - fun result(): List = errors -} - -fun validate(block: Validator.() -> Unit): List = Validator().apply(block).result() - -fun main() { - val errors = validate { - "".required("имя") - "очень длинное значение".maxLength("описание", 5) - "ок".required("статус") - } - println(errors) // [имя обязателен, описание длиннее 5] -} -``` -`required` существует только внутри `validate { }` - снаружи такого расширения на `String` нет. Это способ не засорять глобальное пространство имён. - -## Context parameters -Начиная с Kotlin 2.2 (стабилизированы в 2.4) есть **контекстные параметры** - неявные зависимости, которые передаются по области видимости, а не через аргументы. Они заменили экспериментальные context receivers. - -```kotlin -interface Logger2 { - fun log(message: String) -} - -context(logger: Logger2) -fun processOrder(id: Int) { - logger.log("обработка заказа $id") - logger.log("заказ $id готов") -} - -class ConsoleLogger : Logger2 { - override fun log(message: String) = println("[LOG] $message") -} - -fun main() { - with(ConsoleLogger()) { - processOrder(42) - } - // [LOG] обработка заказа 42 - // [LOG] заказ 42 готов -} -``` -Отличия от получателя: -- контекстный параметр **именованный** (`logger`), а не безымянный `this` -- он не участвует в разрешении вызовов: `log("x")` без `logger.` не скомпилируется -- таких параметров может быть несколько -- он не делает функцию расширением - вызов остаётся `processOrder(42)` - -```kotlin -interface Transaction { fun commit() } -interface Metrics { fun count(name: String) } - -context(tx: Transaction, metrics: Metrics) -fun saveUser(name: String) { - metrics.count("user.save") - println("сохранён $name") - tx.commit() -} - -fun main() { - val tx = object : Transaction { override fun commit() = println("транзакция закрыта") } - val metrics = object : Metrics { override fun count(name: String) = println("метрика: $name") } - - with(tx) { - with(metrics) { - saveUser("Иван") - } - } - // метрика: user.save - // сохранён Иван - // транзакция закрыта -} -``` -Задача, которую это решает: передать «окружение» (логгер, транзакцию, локаль, конфигурацию) через несколько уровней вызовов, не протаскивая параметр руками и не используя глобальное состояние. - -Контекстный параметр можно объявить и у свойства: -```kotlin -interface Locale2 { val code: String } - -context(locale: Locale2) -val greeting: String - get() = if (locale.code == "ru") "Привет" else "Hello" - -fun main() { - val ru = object : Locale2 { override val code = "ru" } - val en = object : Locale2 { override val code = "en" } - - println(with(ru) { greeting }) // Привет - println(with(en) { greeting }) // Hello -} -``` - -| | Параметр | Получатель (`T.()`) | Контекстный параметр | -|---|---|---|---| -| вызов | `f(x, dep)` | `x.f()` | `f(x)`, зависимость из области | -| доступ внутри | по имени | `this`, неявно | по имени | -| сколько | сколько угодно | один | несколько | -| влияет на разрешение вызовов | нет | да | нет | -| видно в сигнатуре вызова | да | да | нет | - -Практическое правило: получатель - когда объект **главный** в операции (`sb.append`), контекстный параметр - когда это **служебная** зависимость (логгер, транзакция), обычный параметр - когда это данные. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `this` только в методах своего класса | `this` в любой лямбде с получателем | -> | билдер с `return this` | `T.() -> Unit` | -> | передача зависимостей аргументами или DI-контейнером | контекстные параметры | -> | `ThreadLocal` для «окружения» | контекстные параметры | -> | нет аналога | `@DslMarker` | -> -> **Лямбда с получателем аналога в Java не имеет.** Это и есть причина, по которой DSL на Kotlin (Gradle Kotlin DSL, HTML-билдеры, конфигурация Ktor) выглядят как отдельный язык, а в Java ограничиваются цепочками методов: -> ```java -> new StringBuilder().append("a").append("b").toString(); -> ``` -> В Kotlin блок кода сам становится областью, где доступны методы объекта - без повторения имени переменной и без `return this` в каждом методе класса. -> -> **Контекстные параметры решают задачу, для которой в Java используют `ThreadLocal` или DI.** Типичный Java-подход к «окружению» - либо протащить параметр через все слои, либо положить его в `ThreadLocal` (как делают Spring-транзакции и MDC в логировании), либо внедрить через контейнер. Первый вариант шумный, второй - неявное глобальное состояние, ломающееся при переходе между потоками, третий требует фреймворка. Контекстный параметр проверяется компилятором: функция не вызовется без нужного контекста: -> ```kotlin -> interface Tenant { val id: String } -> -> context(tenant: Tenant) -> fun currentTenantId(): String = tenant.id -> -> fun main() { -> val t = object : Tenant { override val id = "acme" } -> println(with(t) { currentTenantId() }) // acme -> // println(currentTenantId()) // ошибка компиляции: нет контекста -> } -> ``` -> Для корутин у той же задачи есть другое решение - `CoroutineContext`. Подробнее: [Диспетчеры и контекст](../Корутины/Диспетчеры%20и%20контекст.md) -> -> **Квалифицированный `this@Label` обобщает Java-овский `Outer.this`.** В Java `Outer.this` работает только для вложенных классов; в Kotlin метки действуют и для лямбд, и для расширений, поэтому вложенные области разрешаются одним механизмом. -> -> Сравнить: [Вложенные классы в Java](../../Программирование%20на%20Java/ООП/Вложенные%20классы.md), [Builder](../../Программирование%20на%20Java/Паттерны%20проектирования/Builder.md) - -> [!warning] Подводные камни -> **Внутренний получатель затеняет внешний:** -> ```kotlin -> fun main() { -> val result = StringBuilder("внешний").run { -> StringBuilder("внутренний").run { -> toString() // внутренний — не тот, что ожидали? -> } -> } -> println(result) // внутренний -> } -> ``` -> При вложенности больше одного уровня используйте `this@label` или обычные переменные. -> -> **Без `@DslMarker` вложенные билдеры принимают неверный код:** -> ```kotlin -> class Menu { -> private val items = mutableListOf() -> fun item(name: String) { items.add(name) } -> fun submenu(block: Menu.() -> Unit) { items.add("[${Menu().apply(block).items.joinToString()}]") } -> fun render() = items.joinToString(", ") -> } -> -> fun main() { -> val m = Menu().apply { -> item("файл") -> submenu { item("открыть") } -> } -> println(m.render()) // файл, [открыть] -> } -> ``` -> Здесь `item` внутри `submenu` попадает во внутреннее меню - как и задумано. Но если имена методов у разных уровней совпадают лишь частично, вызов молча уйдёт не туда, куда ожидал автор. -> -> **Получатель и параметр с одинаковым именем:** -> ```kotlin -> class Box9(val value: Int) -> -> fun Box9.compare(value: Int) = "${this.value} vs $value" -> -> fun main() { -> println(Box9(1).compare(2)) // 1 vs 2 -> } -> ``` -> Без `this.` обращение уходит к параметру - ближайшая область выигрывает. Это корректно, но при чтении легко ошибиться. -> -> **Лямбда с получателем скрывает, откуда взялся метод:** -> ```kotlin -> fun main() { -> val text = buildString { -> append("a") -> appendLine("b") -> insert(0, "!") // метод StringBuilder, не видно из вызова -> } -> println(text.trim()) // !ab -> } -> ``` -> В длинном блоке непонятно, какие функции доступны - помогает только IDE. Для публичных DSL это аргумент за короткие блоки и явные имена. -> -> **Контекстные параметры не участвуют в разрешении вызовов:** -> ```kotlin -> interface Printer2 { fun show(m: String) } -> -> context(printer: Printer2) -> fun demo2() { -> printer.show("через имя") // так -> // show("без имени") // ошибка: не найдено -> } -> -> fun main() { -> with(object : Printer2 { override fun show(m: String) = println(m) }) { demo2() } -> } -> ``` -> Это сознательное отличие от отменённых context receivers: там метод вызывался без префикса, что делало код неоднозначным. -> -> **Контекст передаётся через `with`, и это выглядит странно.** Чтобы вызвать функцию с контекстным параметром, нужно внести значение в область видимости - обычно через `with`. Для нескольких контекстов получается вложенность `with(a) { with(b) { ... } }`, которая читается хуже, чем явные аргументы. Для двух зависимостей обычный параметр часто честнее. -> -> **Контекстные параметры - свежая фича.** Они стабилизированы недавно, поддержка в инструментах и библиотеках неполная, а до них была экспериментальная версия (context receivers) с другим синтаксисом, которую убрали. Код из старых статей (`context(Logger)` без имени параметра) не компилируется. Подробнее: [Компилятор K2](../Инструментарий/Компилятор%20K2.md) -> -> **Из Java функции с контекстными параметрами выглядят как функции с дополнительными аргументами** - контекст становится обычным параметром в начале списка. Никакой «неявности» для Java-клиента нет. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md) -> -> **Расширение внутри класса недоступно снаружи:** -> ```kotlin -> class Scope { -> fun String.shout() = uppercase() + "!" -> fun use() = "привет".shout() -> } -> -> fun main() { -> println(Scope().use()) // ПРИВЕТ! -> // println("привет".shout()) // ошибка: вне Scope расширения нет -> } -> ``` -> Это полезное свойство (ограничение области), но в документации к DSL его нужно объяснять - иначе пользователь не поймёт, почему метод «исчезает». -> -> **`this` в лямбде с получателем нельзя переназначить.** Если нужно и внешний объект, и внутренний одновременно, единственный способ - метки или явный параметр: `fun with2(block: (Outer, Inner) -> Unit)`. - -Далее: [DSL](DSL.md) diff --git a/Kotlin/Функциональный Kotlin/Scope-функции.md b/Kotlin/Функциональный Kotlin/Scope-функции.md deleted file mode 100644 index cbc59c0..0000000 --- a/Kotlin/Функциональный Kotlin/Scope-функции.md +++ /dev/null @@ -1,441 +0,0 @@ ---- -title: Scope-функции -description: let, run, apply, also, with - различия, выбор и типичные ошибки -tags: - - kotlin - - functional ---- - -Пять функций stdlib, которые выполняют блок кода в контексте объекта. Делают они почти одно и то же, различаясь двумя вещами: как доступен объект (`this` или `it`) и что возвращается (объект или результат блока). - -| Функция | Объект внутри | Возвращает | Расширение | -|---|---|---|---| -| `let` | `it` | результат блока | да | -| `run` | `this` | результат блока | да | -| `run` (без получателя) | - | результат блока | нет | -| `apply` | `this` | сам объект | да | -| `also` | `it` | сам объект | да | -| `with` | `this` | результат блока | нет (принимает аргумент) | - -```kotlin -data class Person3(var name: String, var age: Int) - -fun main() { - val p = Person3("Иван", 30) - - val letResult = p.let { "${it.name} ${it.age}" } // it, результат блока - val runResult = p.run { "$name $age" } // this, результат блока - val applyResult = p.apply { age = 31 } // this, сам объект - val alsoResult = p.also { println("создан ${it.name}") } // it, сам объект - val withResult = with(p) { "$name $age" } // this, результат блока - - println(letResult) // Иван 30 - println(runResult) // Иван 30 - println(applyResult) // Person3(name=Иван, age=31) - println(alsoResult === p) // true - println(withResult) // Иван 31 -} -``` - -```mermaid -graph TD - A["Нужна scope-функция"] --> B{"Что возвращать?"} - B -->|"сам объект"| C{"Как обращаться?"} - B -->|"результат блока"| D{"Как обращаться?"} - C -->|"this - настройка полей"| E["apply"] - C -->|"it - побочный эффект"| F["also"] - D -->|"it - преобразование"| G["let"] - D -->|"this - много обращений"| H["run"] - D -->|"объект уже есть, не цепочка"| I["with"] -``` - -## let -Основное применение - работа с nullable и преобразование значения: -```kotlin -fun main() { - val name: String? = "Иван" - - val length = name?.let { it.length } ?: 0 - println(length) // 4 - - name?.let { println("имя: $it") } // имя: Иван - - val empty: String? = null - empty?.let { println("не выполнится") } - println("после") // после -} -``` -`?.let { }` - идиоматичная замена `if (x != null)`, когда внутри нужно значение. Подробнее: [Операторы null-безопасности](../Синтаксис%20и%20типы/Null-безопасность/Операторы%20null-безопасности.md) - -Второе применение - ограничить область видимости промежуточного значения: -```kotlin -fun main() { - val result = listOf(1, 2, 3, 4) - .filter { it % 2 == 0 } - .let { evens -> "чётных: ${evens.size}, сумма: ${evens.sum()}" } - println(result) // чётных: 2, сумма: 6 -} -``` -Переменная `evens` не утекает наружу. Явное имя параметра вместо `it` здесь делает код понятнее. - -## run -То же, что `let`, но объект доступен как `this` - удобно, когда обращений к нему много: -```kotlin -data class Config3(val host: String, val port: Int, val tls: Boolean) - -fun main() { - val url = Config3("example.com", 8443, true).run { - val scheme = if (tls) "https" else "http" - "$scheme://$host:$port" - } - println(url) // https://example.com:8443 -} -``` - -`run` без получателя - просто блок, возвращающий значение: -```kotlin -fun main() { - val value = run { - val a = 2 - val b = 3 - a * b - } - println(value) // 6 -} -``` -Это способ вычислить `val` несколькими строками, не заводя функцию. Полезно в инициализации свойств. - -## apply -Возвращает сам объект - значит, предназначен для настройки: -```kotlin -class Request2 { - var url: String = "" - var method: String = "GET" - val headers = mutableMapOf() - - override fun toString() = "$method $url $headers" -} - -fun main() { - val request = Request2().apply { - url = "https://example.com" - method = "POST" - headers["Content-Type"] = "application/json" - } - println(request) // POST https://example.com {Content-Type=application/json} -} -``` -Это замена builder-паттерна для классов с изменяемыми свойствами. Подробнее: [Паттерны проектирования](Паттерны%20проектирования.md) - -Частый случай - настройка при возврате: -```kotlin -fun buildList2(): List = mutableListOf().apply { - add(1) - add(2) - addAll(listOf(3, 4)) -} - -fun main() { - println(buildList2()) // [1, 2, 3, 4] -} -``` -Хотя для коллекций в stdlib уже есть `buildList`, который чище. - -## also -Возвращает объект, доступ через `it` - для побочных эффектов, не меняющих объект: -```kotlin -fun main() { - val numbers = listOf(3, 1, 2) - .also { println("исходный: $it") } - .sorted() - .also { println("отсортированный: $it") } - .filter { it > 1 } - .also { println("отфильтрованный: $it") } - - println(numbers) - // исходный: [3, 1, 2] - // отсортированный: [1, 2, 3] - // отфильтрованный: [2, 3] - // [2, 3] -} -``` -`also` вставляется в цепочку, ничего в ней не меняя - идеально для логирования и отладки. - -Второе применение - валидация и регистрация при создании: -```kotlin -class Resource(val name: String) { - fun close() = println("закрыт $name") -} - -fun main() { - val registry = mutableListOf() - - val r = Resource("файл").also { registry.add(it) } - println("${r.name}, в реестре: ${registry.size}") // файл, в реестре: 1 -} -``` - -## with -Единственная не-расширение: объект передаётся аргументом. -```kotlin -fun main() { - val sb = StringBuilder() - with(sb) { - append("Привет") - append(", ") - append("мир") - append("!") - } - println(sb) // Привет, мир! -} -``` -`with(x) { }` читается как «с объектом x сделай» и применяется, когда объект уже есть в переменной и цепочка не нужна. Результат - значение блока: -```kotlin -data class Point2(val x: Int, val y: Int) - -fun main() { - val distance = with(Point2(3, 4)) { - kotlin.math.sqrt((x * x + y * y).toDouble()) - } - println(distance) // 5.0 -} -``` - -## Как выбирать -Практические правила, покрывающие почти все случаи: - -| Задача | Функция | -|---|---| -| выполнить код, если значение не `null` | `?.let { }` | -| преобразовать значение в другое | `let` | -| настроить свойства нового объекта | `apply` | -| залогировать значение в середине цепочки | `also` | -| вычислить значение из нескольких полей объекта | `run` | -| сгруппировать вызовы на существующем объекте | `with` | -| вычислить `val` из нескольких строк | `run { }` без получателя | - -Выбор между `it` и `this` - вопрос читаемости: `this` короче при многих обращениях к полям, `it` яснее, когда объект используется один-два раза или когда внутри есть другой `this`. - -## Вложенность и затенение -```kotlin -data class Inner(val value: Int) -data class Outer(val inner: Inner, val name: String) - -fun main() { - val o = Outer(Inner(42), "внешний") - - // плохо: два this, непонятно какой - val bad = o.run { inner.run { "$value" } } - println(bad) // 42 - - // хорошо: явные имена - val good = o.let { outer -> outer.inner.let { inner -> "${outer.name}: ${inner.value}" } } - println(good) // внешний: 42 -} -``` -Вложенные scope-функции - главный источник нечитаемого кода на Kotlin. Правило: вложенность больше одного уровня - заводите явные имена параметров или обычные переменные. - -## Scope-функции и null -```kotlin -fun main() { - val maybe: String? = "текст" - - println(maybe?.let { it.length }) // 5 — блок выполнится, если не null - println(maybe?.run { length }) // 5 — то же - println(maybe.let { it?.length }) // 5 — блок выполнится всегда, it nullable - - val nothing: String? = null - println(nothing?.let { it.length }) // null - println(nothing.let { it?.length }) // null — но блок выполнился! -} -``` -Разница в месте `?.`: `x?.let { }` не вызывает блок при `null`, `x.let { }` вызывает всегда с nullable `it`. Это часто путают. - -## takeIf и takeUnless -Родственные функции - фильтр для одного значения: -```kotlin -fun main() { - val number = 42 - - println(number.takeIf { it > 0 }) // 42 - println(number.takeIf { it > 100 }) // null - println(number.takeUnless { it > 0 }) // null - - val name = " " - println(name.takeIf { it.isNotBlank() } ?: "(по умолчанию)") // (по умолчанию) - - val config = mapOf("port" to "8080") - val port = config["port"]?.takeIf { it.isNotBlank() }?.toIntOrNull() ?: 80 - println(port) // 8080 -} -``` -`takeIf` превращает условие в nullable-значение, которое дальше обрабатывается через `?:` и `?.`. Это заменяет `if`-цепочки в конвейерах преобразования. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `if (x != null) { ... }` | `x?.let { ... }` | -> | `Optional.map` | `let` | -> | `Optional.ifPresent` | `?.let` или `?.also` | -> | builder с `return this` | `apply` | -> | промежуточная переменная | `let`/`run` | -> | `Objects.requireNonNullElse(x, y)` | `x ?: y` | -> | нет аналога | `also` в середине цепочки | -> | нет аналога | `with` | -> -> **Scope-функции заменяют `Optional` и промежуточные переменные.** В Java цепочка преобразований с проверками на null выглядит так: -> ```java -> String result = Optional.ofNullable(user) -> .map(User::getAddress) -> .map(Address::getCity) -> .orElse("неизвестно"); -> ``` -> В Kotlin `?.` и `?:` делают то же без обёртки, а `let` подключается, когда нужен не просто вызов метода: -> ```kotlin -> data class Address(val city: String) -> data class User11(val address: Address?) -> -> fun main() { -> val user: User11? = User11(Address("Москва")) -> val result = user?.address?.city?.let { it.uppercase() } ?: "неизвестно" -> println(result) // МОСКВА -> } -> ``` -> Принципиальная разница: `Optional` - объект-обёртка со своей стоимостью и своим API, а `?.` - синтаксис над обычной ссылкой. Поэтому `Optional` в Kotlin не используют вообще. -> -> **`apply` заменяет builder-паттерн для изменяемых объектов.** В Java чтобы настраивать объект цепочкой, класс должен возвращать `this` из каждого сеттера - это решение автора класса. `apply` работает с любым классом, включая чужой: -> ```kotlin -> fun main() { -> val props = java.util.Properties().apply { -> setProperty("host", "localhost") -> setProperty("port", "8080") -> } -> println(props.getProperty("host")) // localhost -> } -> ``` -> `Properties` из JDK не имеет fluent-API, но с `apply` ведёт себя как будто имеет. -> -> **`also` в цепочке аналога в Java не имеет.** `Stream.peek` похож, но работает только внутри стрима и его использование не рекомендуется. `also` применим к любому значению. -> -> **Это не магия, а обычные inline-расширения.** `let` объявлена как `inline fun T.let(block: (T) -> R): R = block(this)` - одна строка. Поэтому накладных расходов нет: после компиляции остаётся только тело блока. Подробнее: [Inline-функции](Inline-функции.md) -> -> Сравнить: [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md), [Builder](../../Программирование%20на%20Java/Паттерны%20проектирования/Builder.md) - -> [!warning] Подводные камни -> **`apply` возвращает объект, даже если блок что-то вычислил:** -> ```kotlin -> fun main() { -> val wrong = StringBuilder().apply { append("текст"); length } -> println(wrong) // текст — не 5! -> val right = StringBuilder().run { append("текст"); length } -> println(right) // 5 -> } -> ``` -> Перепутать `apply` и `run` легко, а ошибка типов возникает далеко от места. -> -> **`let` на nullable без `?.` выполняется всегда:** -> ```kotlin -> fun main() { -> val x: String? = null -> x.let { println("выполнилось, it = $it") } // выполнилось, it = null -> x?.let { println("не выполнится") } -> } -> ``` -> -> **Вложенные `this` и `it` путаются:** -> ```kotlin -> data class Address2(var city: String) -> data class User12(var name: String, val address: Address2) -> -> fun main() { -> val u = User12("Иван", Address2("Москва")) -> u.apply { -> name = "Пётр" -> address.apply { -> city = "Казань" -> // name = "x" // name внешнего this — компилируется, но запутанно -> } -> } -> println(u) // User12(name=Пётр, address=Address2(city=Казань)) -> } -> ``` -> Внутренний блок видит и свой `this`, и внешний. Компилятор разрешает обращаться к членам обоих, и ошибку заметить трудно. -> -> **`also` и `apply` не защищают от изменения объекта.** Оба возвращают **тот же** объект, а не копию: -> ```kotlin -> data class Counter3(var value: Int) -> -> fun main() { -> val c = Counter3(1) -> val same = c.also { it.value = 99 } -> println("${c.value} ${same === c}") // 99 true -> } -> ``` -> -> **`?.let` на цепочке проверяет только последнее звено:** -> ```kotlin -> data class Inner2(val value: Int?) -> -> fun main() { -> val i: Inner2? = Inner2(null) -> println(i?.let { it.value?.plus(1) }) // null -> println(i?.value?.plus(1)) // null — короче и яснее -> } -> ``` -> Когда достаточно `?.`, `let` только добавляет шум. Используйте `let`, если внутри нужно больше одного действия. -> -> **Цепочка из scope-функций читается хуже обычного кода:** -> ```kotlin -> fun main() { -> val bad = "42".let { it.toInt() }.let { it * 2 }.also { println(it) }.let { "$it" } -> println(bad) // 84 \n 84 -> -> val good = ("42".toInt() * 2).toString() -> println(good) // 84 -> } -> ``` -> Scope-функции - инструмент, а не стиль. Если без них короче - пишите без них. -> -> **`run` без получателя путается с `run` на объекте.** `x.run { }` и `run { }` - разные функции с одним именем; при чтении диффа разница в одну точку. -> -> **Нелокальный `return` внутри scope-функции выходит из внешней функции:** -> ```kotlin -> fun process5(value: String?): String { -> value?.let { -> if (it.isBlank()) return "пусто" // выход из process5! -> } -> return "обработано: $value" -> } -> -> fun main() { -> println(process5(" ")) // пусто -> println(process5("текст")) // обработано: текст -> } -> ``` -> Это работает благодаря `inline`, но для читателя неочевидно: `return` внутри лямбды выглядит как возврат из лямбды. -> -> **`takeIf` возвращает nullable - и это заразно:** -> ```kotlin -> fun main() { -> val port: Int? = 8080.takeIf { it > 1024 } -> // println(port + 1) // ошибка: Int? не складывается -> println(port?.plus(1)) // 8081 -> } -> ``` -> Каждый `takeIf` добавляет в тип `?`, который нужно где-то разрешить. -> -> **`apply` на `val`-свойствах бесполезен:** -> ```kotlin -> data class Immutable(val a: Int, val b: Int) -> -> fun main() { -> val x = Immutable(1, 2).apply { -> // a = 10 // ошибка: val -> } -> println(x) // Immutable(a=1, b=2) -> println(Immutable(1, 2).copy(a = 10)) // Immutable(a=10, b=2) -> } -> ``` -> Для неизменяемых классов нужен `copy`, а не `apply`. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md) - -Далее: [Инфиксные и операторные функции](Инфиксные%20и%20операторные%20функции.md) diff --git a/Kotlin/Функциональный Kotlin/Инфиксные и операторные функции.md b/Kotlin/Функциональный Kotlin/Инфиксные и операторные функции.md deleted file mode 100644 index 3ebf03c..0000000 --- a/Kotlin/Функциональный Kotlin/Инфиксные и операторные функции.md +++ /dev/null @@ -1,504 +0,0 @@ ---- -title: Инфиксные и операторные функции -description: infix-нотация, перегрузка операторов, invoke, деструктуризация и делегирование свойств -tags: - - kotlin - - functional ---- - -Kotlin позволяет задать для своих типов тот же синтаксис, что у встроенных: `a + b`, `a[i]`, `a()`, `a in b`. Делается это обычными функциями с модификатором `operator` или `infix`. - -## infix -```kotlin -infix fun Int.упаковатьВ(unit: String): String = "$this $unit" - -data class Money(val amount: Long, val currency: String) - -infix fun Long.of(currency: String) = Money(this, currency) - -fun main() { - println(5 упаковатьВ "кг") // 5 кг - println(100L of "RUB") // Money(amount=100, currency=RUB) - println(100L.of("RUB")) // Money(amount=100, currency=RUB) — обычный вызов тоже работает -} -``` -Требования к `infix`: ровно один параметр, не `vararg`, без значения по умолчанию, и функция должна быть методом или расширением. - -В stdlib `infix` используется для операций, читающихся как фраза: -```kotlin -fun main() { - println(setOf(1, 2) union setOf(2, 3)) // [1, 2, 3] - println(setOf(1, 2) intersect setOf(2, 3)) // [2] - println("a" to 1) // (a, 1) - println(5 shl 2) // 20 - println(5 and 3) // 1 - println(1 until 5) // 1..4 - println(5 downTo 1) // 5 downTo 1 step 1 - println(1..10 step 2) // 1..9 step 2 -} -``` - -Приоритет инфиксных функций ниже арифметики, но выше сравнений - это источник ошибок: -```kotlin -infix fun Int.plusTen(x: Int) = this + x + 10 - -fun main() { - println(1 plusTen 2 * 3) // 17 — это 1 plusTen (2*3) - println((1 plusTen 2) * 3) // 39 -} -``` -При сомнении ставьте скобки. - -## Перегрузка операторов -Каждый оператор отображается в функцию с фиксированным именем: - -| Оператор | Функция | -|---|---| -| `a + b` | `a.plus(b)` | -| `a - b` | `a.minus(b)` | -| `a * b` | `a.times(b)` | -| `a / b` | `a.div(b)` | -| `a % b` | `a.rem(b)` | -| `+a` / `-a` | `a.unaryPlus()` / `a.unaryMinus()` | -| `!a` | `a.not()` | -| `a++` / `a--` | `a.inc()` / `a.dec()` | -| `a += b` | `a.plusAssign(b)` или `a = a.plus(b)` | -| `a == b` | `a?.equals(b) ?: (b === null)` | -| `a > b` | `a.compareTo(b) > 0` | -| `a[i]` | `a.get(i)` | -| `a[i] = v` | `a.set(i, v)` | -| `a()` | `a.invoke()` | -| `a in b` | `b.contains(a)` | -| `a..b` | `a.rangeTo(b)` | -| `a.. x - 1 -> y - else -> throw IndexOutOfBoundsException("индекс $index") - } - - val length: Double get() = kotlin.math.sqrt(x * x + y * y) -} - -fun main() { - val a = Vec(1.0, 2.0) - val b = Vec(3.0, 4.0) - - println(a + b) // Vec(x=4.0, y=6.0) - println(b - a) // Vec(x=2.0, y=2.0) - println(a * 2.0) // Vec(x=2.0, y=4.0) - println(-a) // Vec(x=-1.0, y=-2.0) - println(b[0]) // 3.0 - println(b.length) // 5.0 -} -``` -Умножение на скаляр справа - метод класса; чтобы работало и слева, нужно расширение: -```kotlin -data class Vec2(val x: Double, val y: Double) { - operator fun times(scalar: Double) = Vec2(x * scalar, y * scalar) -} - -operator fun Double.times(v: Vec2) = v * this - -fun main() { - val v = Vec2(1.0, 2.0) - println(v * 3.0) // Vec2(x=3.0, y=6.0) - println(3.0 * v) // Vec2(x=3.0, y=6.0) -} -``` - -## Операторы как расширения -Оператор можно добавить к чужому типу: -```kotlin -operator fun String.times(n: Int) = repeat(n) - -operator fun List.get(range: IntRange): List = subList(range.first, range.last + 1) - -fun main() { - println("ab" * 3) // ababab - println(listOf(1, 2, 3, 4, 5)[1..3]) // [2, 3, 4] -} -``` -Так в stdlib добавлены `Map.get` через `[]`, `List.plus` и операции над множествами. - -## invoke -`operator fun invoke` позволяет вызывать объект как функцию: -```kotlin -class Multiplier(private val factor: Int) { - operator fun invoke(x: Int) = x * factor -} - -fun main() { - val triple = Multiplier(3) - println(triple(5)) // 15 - println(triple.invoke(5)) // 15 — то же самое -} -``` -Практическое применение - объект с состоянием, который ведёт себя как функция: -```kotlin -class RateLimiter(private val maxCalls: Int) { - private var calls = 0 - operator fun invoke(): Boolean { - calls++ - return calls <= maxCalls - } -} - -fun main() { - val limiter = RateLimiter(2) - println("${limiter()} ${limiter()} ${limiter()}") // true true false -} -``` -`invoke` на `companion object` даёт фабрику с синтаксисом конструктора: -```kotlin -class Email private constructor(val value: String) { - companion object { - operator fun invoke(raw: String): Email? = - if (raw.contains("@")) Email(raw.trim().lowercase()) else null - } - override fun toString() = value -} - -fun main() { - println(Email(" Ivan@Example.COM ")) // ivan@example.com - println(Email("не почта")) // null -} -``` -Это способ сделать «конструктор, возвращающий null» - настоящий конструктор так не может. - -## Сравнение: compareTo -```kotlin -class Version(val major: Int, val minor: Int) : Comparable { - override fun compareTo(other: Version): Int = - compareValuesBy(this, other, { it.major }, { it.minor }) - - override fun toString() = "$major.$minor" -} - -fun main() { - val a = Version(1, 5) - val b = Version(2, 0) - - println(a < b) // true - println(a >= Version(1, 5)) // true - println(listOf(b, a, Version(1, 9)).sorted()) // [1.5, 1.9, 2.0] - println(maxOf(a, b)) // 2.0 - println(a in Version(1, 0)..Version(1, 9)) // true -} -``` -Одна функция `compareTo` даёт `<`, `>`, `<=`, `>=`, сортировку, `maxOf`/`minOf` и диапазоны. `compareValuesBy` сравнивает по нескольким ключам по порядку. - -## in: contains -```kotlin -data class DateRange(val from: Int, val to: Int) { - operator fun contains(day: Int) = day in from..to -} - -fun main() { - val week = DateRange(1, 7) - println(3 in week) // true - println(10 in week) // false - println(10 !in week) // true — !in получается автоматически -} -``` - -## Итератор -```kotlin -class Countdown(private val from: Int) { - operator fun iterator(): Iterator = object : Iterator { - private var current = from - override fun hasNext() = current > 0 - override fun next() = current-- - } -} - -fun main() { - for (i in Countdown(3)) print("$i ") - println() // 3 2 1 -} -``` -`iterator` можно добавить и расширением - тогда `for` заработает на чужом типе: -```kotlin -operator fun ClosedRange.iterator(): Iterator = - object : Iterator { - private var current = start - override fun hasNext() = current <= endInclusive - override fun next() = current.also { current = current.plusDays(1) } - } - -fun main() { - val from = java.time.LocalDate.of(2026, 1, 1) - val to = java.time.LocalDate.of(2026, 1, 3) - for (d in from..to) print("${d.dayOfMonth} ") - println() // 1 2 3 -} -``` - -## get и set -```kotlin -class Matrix(private val rows: Int, private val cols: Int) { - private val data = DoubleArray(rows * cols) - - operator fun get(i: Int, j: Int): Double = data[i * cols + j] - operator fun set(i: Int, j: Int, value: Double) { data[i * cols + j] = value } - - override fun toString() = (0 until rows).joinToString("\n") { i -> - (0 until cols).joinToString(" ") { j -> this[i, j].toString() } - } -} - -fun main() { - val m = Matrix(2, 2) - m[0, 0] = 1.0 - m[1, 1] = 2.0 - println(m[0, 0]) // 1.0 - println(m) - // 1.0 0.0 - // 0.0 2.0 -} -``` -`get` и `set` принимают любое число аргументов - это даёт многомерную индексацию, которой нет в Java. - -## plusAssign против plus -```kotlin -class Basket { - private val items = mutableListOf() - operator fun plusAssign(item: String) { items.add(item) } - override fun toString() = items.toString() -} - -fun main() { - val basket = Basket() - basket += "хлеб" - basket += "молоко" - println(basket) // [хлеб, молоко] -} -``` -Если объявлены оба (`plus` и `plusAssign`) и переменная `var` - компилятор сообщит об неоднозначности. Правило: `plusAssign` для изменяемых коллекций-подобных типов, `plus` для значений. - -## Деструктуризация -```kotlin -class Pair3(val first: Int, val second: String) { - operator fun component1() = first - operator fun component2() = second -} - -fun main() { - val (num, text) = Pair3(1, "один") - println("$num = $text") // 1 = один -} -``` -`data class` объявляет `componentN` автоматически. Подробнее: [Деструктуризация](../Синтаксис%20и%20типы/Работа%20с%20данными/Деструктуризация.md) - -## Делегирование свойств -Операторы `getValue`/`setValue` позволяют подменить реализацию свойства: -```kotlin -import kotlin.reflect.KProperty - -class Trimmed { - private var value: String = "" - operator fun getValue(thisRef: Any?, property: KProperty<*>): String = value - operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) { - value = newValue.trim() - } -} - -class Form { - var login: String by Trimmed() -} - -fun main() { - val f = Form() - f.login = " ivan " - println("[${f.login}]") // [ivan] -} -``` -Подробнее о делегатах и готовых реализациях (`lazy`, `observable`, `map`): [Делегирование](../ООП/Делегирование.md) - -## Когда перегружать оператор -| Ситуация | Стоит ли | -|---|---| -| математический тип (вектор, матрица, деньги, дата) | да | -| коллекция-подобный тип (`[]`, `in`, `for`) | да | -| сравнение по естественному порядку | да, через `Comparable` | -| «красивее выглядит» | нет | -| смысл оператора неочевиден (`user + role`) | нет | -| операция может бросить исключение неожиданно | нет | - -Критерий простой: оператор должен означать то же, что для встроенных типов. `+` обязан быть коммутативным по смыслу и не иметь побочных эффектов; `==` - соответствовать `hashCode`. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | перегрузки операторов нет | `operator fun` | -> | `a.add(b)` | `a + b` | -> | `list.get(i)` | `list[i]` | -> | `map.put(k, v)` | `map[k] = v` | -> | `a.compareTo(b) < 0` | `a < b` | -> | `a.equals(b)` | `a == b` | -> | `BigDecimal.add().multiply()` | `a + b * c` | -> | инфиксной нотации нет | `infix fun` | -> -> **Java не поддерживает перегрузку операторов принципиально** - кроме `+` для строк, который встроен в компилятор. Это сознательное решение дизайнеров Java: операторы с произвольным смыслом усложняют чтение. Kotlin выбрал компромисс: перегрузка есть, но набор операторов фиксирован и имена функций заданы жёстко, так что `plus` всегда означает `+` и нельзя придумать свой оператор. -> -> **Разница заметнее всего на числовых типах.** `BigDecimal` в Java: -> ```java -> BigDecimal result = a.multiply(b).add(c).divide(d, RoundingMode.HALF_UP); -> ``` -> В Kotlin те же операторы уже объявлены в stdlib как расширения: -> ```kotlin -> import java.math.BigDecimal -> -> fun main() { -> val a = BigDecimal("10.5") -> val b = BigDecimal("2") -> println(a + b) // 12.5 -> println(a * b) // 21.0 -> println(a > b) // true -> } -> ``` -> Для денежных расчётов это главный практический выигрыш: формула в коде выглядит как формула. -> -> **`==` в Kotlin - это `equals`, а не сравнение ссылок.** Самое важное отличие на практике: в Java `a == b` для объектов сравнивает ссылки, и забытый `.equals()` - классическая ошибка со строками. В Kotlin `==` вызывает `equals` (с корректной обработкой null), а для ссылок есть отдельный `===`: -> ```kotlin -> fun main() { -> val a = StringBuilder("x").toString() -> val b = StringBuilder("x").toString() -> println(a == b) // true — по значению -> println(a === b) // false — разные объекты -> } -> ``` -> -> **`infix` даёт читаемость без DSL-библиотек.** В Java тестовые фреймворки имитируют инфиксную запись цепочкой методов: `assertThat(x).isEqualTo(y)`. В Kotlin это записывается как `x shouldBe y` - подробнее: [Kotest](../Тестирование/Kotest.md) -> -> **`invoke` аналога не имеет.** В Java объект нельзя вызвать как функцию; ближайшее - интерфейс с методом `apply`, вызываемый явно. -> -> Сравнить: [Операторы и выражения в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Операторы%20и%20выражения.md), [Класс Object](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Класс%20Object.md) - -> [!warning] Подводные камни -> **Приоритет `infix` ниже арифметики:** -> ```kotlin -> infix fun Int.append(x: Int) = "$this$x" -> -> fun main() { -> println(1 append 2 + 3) // 15 — это 1 append (2+3) -> println((1 append 2) + 3) // 123 -> } -> ``` -> Выражение выглядит как последовательность слева направо, но арифметика вычисляется первой. -> -> **`infix` нельзя использовать без получателя и с `this` неявно:** -> ```kotlin -> class Calc { -> infix fun add(x: Int) = x + 1 -> fun demo(): Int { -> // return add 5 // ошибка -> return this add 5 // так можно -> } -> } -> -> fun main() { println(Calc().demo()) } // 6 -> ``` -> -> **`plus` не меняет объект, `plusAssign` меняет - и перепутать легко:** -> ```kotlin -> fun main() { -> var list1 = listOf(1, 2) -> list1 += 3 // новый список: plus + присваивание -> println(list1) // [1, 2, 3] -> -> val list2 = mutableListOf(1, 2) -> list2 += 3 // изменение на месте: plusAssign -> println(list2) // [1, 2, 3] -> } -> ``` -> Результат одинаковый, стоимость разная: первый вариант копирует весь список. -> -> **Объявление и `plus`, и `plusAssign` даёт ошибку компиляции при `var`:** -> ```kotlin -> class Both(val v: Int) { -> operator fun plus(x: Int) = Both(v + x) -> operator fun plusAssign(x: Int) { } -> } -> -> fun main() { -> val b = Both(1) -> b += 1 // выбран plusAssign — для val это единственный вариант -> println(b.v) // 1 -> } -> ``` -> Для `var`-переменной компилятор не сможет выбрать между `b = b + 1` и `b.plusAssign(1)`. -> -> **`inc`/`dec` должны возвращать новый объект:** -> ```kotlin -> class Counter4(val value: Int) { -> operator fun inc() = Counter4(value + 1) -> } -> -> fun main() { -> var c = Counter4(0) -> c++ -> println(c.value) // 1 -> } -> ``` -> Если `inc` изменит объект и вернёт `this`, постфиксный `c++` сломается: он должен вернуть **старое** значение, а вернёт изменённое. -> -> **`equals` нельзя объявить расширением:** -> ```kotlin -> class NoEquals(val v: Int) -> -> // operator fun NoEquals.equals(other: Any?) = ... // не заработает -> -> fun main() { -> println(NoEquals(1) == NoEquals(1)) // false — сравнение ссылок из Any -> } -> ``` -> `equals` должен быть `override` в классе: он объявлен в `Any`, а расширение не переопределяет член. Подробнее: [equals, hashCode и toString](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md) -> -> **`compareTo` обязан быть согласован с `equals`.** Если `a.compareTo(b) == 0`, но `a != b`, то `TreeSet` и `sortedSetOf` начнут терять элементы - они используют сравнение, а не равенство. -> -> **`get` без `set` создаёт асимметричный API:** -> ```kotlin -> class ReadOnlyBox(private val items: List) { -> operator fun get(i: Int) = items[i] -> } -> -> fun main() { -> val b = ReadOnlyBox(listOf(1, 2)) -> println(b[0]) // 1 -> // b[0] = 5 // ошибка: нет set -> } -> ``` -> Это нормально (так работает `List`), но пользователь увидит `[]` и может ожидать присваивания. -> -> **Оператор на расширении не виден, если есть член класса** - действует то же правило затенения, что у обычных расширений. -> -> **Операторы с неочевидным смыслом ухудшают код:** -> ```kotlin -> data class User13(val name: String, val roles: List) -> -> // operator fun User13.plus(role: String) = copy(roles = roles + role) -> fun User13.withRole(role: String) = copy(roles = roles + role) -> -> fun main() { -> println(User13("Иван", emptyList()).withRole("админ")) -> // User13(name=Иван, roles=[админ]) -> } -> ``` -> `user + "админ"` короче, но непонятно: что значит «сложить пользователя со строкой»? Именованный метод честнее. -> -> **`invoke` на обычном классе запутывает:** `service()` не говорит, что делает объект. Оправдано для функциональных обёрток (`UseCase`, `Validator`), где у объекта ровно одна операция. -> -> **Нельзя придумать новый оператор.** Набор фиксирован: `+`, `-`, `*`, `/`, `%`, `[]`, `()`, `in`, `..`, сравнения, `++`/`--`, унарные. Произвольные символы вроде `<*>` или `|>` недоступны - только `infix`-функция со словесным именем. - -Далее: [Receivers и context parameters](Receivers%20и%20context%20parameters.md) diff --git a/Kotlin/Функциональный Kotlin/Паттерны проектирования.md b/Kotlin/Функциональный Kotlin/Паттерны проектирования.md deleted file mode 100644 index ccb2d88..0000000 --- a/Kotlin/Функциональный Kotlin/Паттерны проектирования.md +++ /dev/null @@ -1,577 +0,0 @@ ---- -title: Паттерны проектирования -description: Какие паттерны GoF встроены в язык, какие упростились и какие остались нужны -tags: - - kotlin - - functional - - patterns ---- - -Многие паттерны проектирования - это обходные приёмы для того, чего не хватает языку. Kotlin часть этих возможностей встроил, поэтому соответствующие паттерны сводятся к одной конструкции. - -| Паттерн | В Kotlin | -|---|---| -| Singleton | `object` | -| Builder | именованные аргументы, `apply`, DSL | -| Factory Method | функция верхнего уровня или `invoke` в companion | -| Strategy | функциональный тип | -| Decorator | делегирование `by` | -| Observer | `Flow`, `StateFlow` или список лямбд | -| Template Method | функция высшего порядка | -| Command | лямбда | -| Adapter | функции-расширения | -| Visitor | `sealed` + `when` | -| Prototype | `copy()` у `data class` | -| Null Object | nullable-тип и `?:` | -| Facade, Repository, Proxy | остаются как есть | - -## Singleton -```kotlin -object Settings { - private val values = mutableMapOf() - - var theme: String = "light" - - fun put(key: String, value: String) { values[key] = value } - fun get(key: String): String? = values[key] -} - -fun main() { - Settings.theme = "dark" - Settings.put("lang", "ru") - println("${Settings.theme} ${Settings.get("lang")}") // dark ru -} -``` -`object` - потокобезопасный ленивый синглтон: экземпляр создаётся при первом обращении, инициализация защищена загрузчиком классов. Никакого `getInstance()`, `volatile` и двойной проверки блокировки. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md) - -Цена та же, что у любого синглтона: глобальное состояние, которое трудно подменить в тестах. Если объект нужно мокировать - он должен реализовать интерфейс и внедряться параметром. - -## Builder -Три варианта по возрастанию сложности. Первый - именованные аргументы с значениями по умолчанию: -```kotlin -data class HttpRequest( - val url: String, - val method: String = "GET", - val timeoutMs: Int = 5_000, - val headers: Map = emptyMap(), - val retries: Int = 0, -) - -fun main() { - val r = HttpRequest(url = "https://example.com", method = "POST", retries = 3) - println(r) - // HttpRequest(url=https://example.com, method=POST, timeoutMs=5000, headers={}, retries=3) -} -``` -Это закрывает подавляющее большинство случаев, ради которых пишут builder. Плюс к этому `copy` даёт модификацию: -```kotlin -data class HttpRequest( - val url: String, - val method: String = "GET", - val headers: Map = emptyMap(), -) - -fun main() { - val base = HttpRequest("https://example.com") - val withAuth = base.copy(headers = mapOf("Authorization" to "Bearer x")) - println(withAuth.headers) // {Authorization=Bearer x} - println(base.headers) // {} — оригинал цел -} -``` - -Второй вариант - `apply` для классов с изменяемыми свойствами (включая чужие): -```kotlin -fun main() { - val sb = StringBuilder().apply { - append("строка") - append(" ") - append(42) - } - println(sb) // строка 42 -} -``` - -Третий - DSL, когда структура вложенная: -```kotlin -class Form2 { - private val fields = mutableListOf() - fun field(name: String, required: Boolean = false) { - fields.add(if (required) "$name*" else name) - } - fun render() = fields.joinToString(", ") -} - -fun form(block: Form2.() -> Unit) = Form2().apply(block).render() - -fun main() { - println(form { - field("имя", required = true) - field("телефон") - }) - // имя*, телефон -} -``` -Подробнее: [DSL](DSL.md) - -## Factory Method -```kotlin -sealed interface Shape2 { - data class Circle2(val radius: Double) : Shape2 - data class Square(val side: Double) : Shape2 -} - -fun shapeOf(type: String, size: Double): Shape2? = when (type) { - "circle" -> Shape2.Circle2(size) - "square" -> Shape2.Square(size) - else -> null -} - -fun main() { - println(shapeOf("circle", 2.0)) // Circle2(radius=2.0) - println(shapeOf("unknown", 1.0)) // null -} -``` -Отдельный класс-фабрика не нужен: функция верхнего уровня делает то же. Когда фабрика должна выглядеть как конструктор, помогает `invoke` в companion: -```kotlin -class PositiveNumber private constructor(val value: Int) { - companion object { - operator fun invoke(raw: Int): PositiveNumber? = - if (raw > 0) PositiveNumber(raw) else null - } - override fun toString() = "+$value" -} - -fun main() { - println(PositiveNumber(5)) // +5 - println(PositiveNumber(-1)) // null -} -``` -Настоящий конструктор не может вернуть `null` или кешированный экземпляр - здесь это возможно, а вызов выглядит как `PositiveNumber(5)`. Подробнее: [Инфиксные и операторные функции](Инфиксные%20и%20операторные%20функции.md) - -## Strategy -Классический паттерн - интерфейс с одним методом и набор реализаций. В Kotlin это функциональный тип: -```kotlin -class Checkout(private val discount: (Long) -> Long) { - fun total(amount: Long) = discount(amount) -} - -fun main() { - val none = Checkout { it } - val tenPercent = Checkout { it * 90 / 100 } - val fixed = Checkout { (it - 100).coerceAtLeast(0) } - - println("${none.total(1000)} ${tenPercent.total(1000)} ${fixed.total(1000)}") - // 1000 900 900 -} -``` -Стратегии становятся значениями: их можно хранить в `Map`, выбирать по ключу, комбинировать: -```kotlin -fun main() { - val strategies: Map Long> = mapOf( - "none" to { it }, - "ten" to { it * 90 / 100 }, - "half" to { it / 2 }, - ) - - val chosen = strategies["ten"] ?: strategies.getValue("none") - println(chosen(1000)) // 900 - - val combined: (Long) -> Long = { strategies.getValue("ten")(strategies.getValue("half")(it)) } - println(combined(1000)) // 450 -} -``` -Когда у стратегии больше одной операции - интерфейс остаётся правильным выбором. - -## Decorator -```kotlin -interface Repository8 { - fun find(id: Int): String? -} - -class DatabaseRepository : Repository8 { - override fun find(id: Int): String? = if (id == 1) "Иван" else null -} - -class CachingRepository(private val delegate: Repository8) : Repository8 by delegate { - private val cache = mutableMapOf() - - override fun find(id: Int): String? = cache.getOrPut(id) { - println("запрос в БД: $id") - delegate.find(id) - } -} - -class LoggingRepository(private val delegate: Repository8) : Repository8 by delegate - -fun main() { - val repo = LoggingRepository(CachingRepository(DatabaseRepository())) - println(repo.find(1)) // запрос в БД: 1 \n Иван - println(repo.find(1)) // Иван — из кеша -} -``` -`by delegate` генерирует делегирующие реализации для всех методов интерфейса, а переопределить нужно только изменённые. В Java каждый декоратор обязан вручную прописать все методы интерфейса. Подробнее: [Делегирование](../ООП/Делегирование.md) - -## Observer -Простая версия - список лямбд: -```kotlin -class Publisher { - private val subscribers = mutableListOf<(T) -> Unit>() - - fun subscribe(handler: (T) -> Unit): () -> Unit { - subscribers.add(handler) - return { subscribers.remove(handler) } // функция отписки - } - - fun publish(event: T) = subscribers.forEach { it(event) } -} - -fun main() { - val publisher = Publisher() - val unsubscribe = publisher.subscribe { println("получено: $it") } - publisher.subscribe { println("второй: $it") } - - publisher.publish("событие 1") - // получено: событие 1 - // второй: событие 1 - - unsubscribe() - publisher.publish("событие 2") - // второй: событие 2 -} -``` -Возврат функции отписки вместо метода `unsubscribe(handler)` избавляет от хранения ссылки на обработчик - частая причина утечек в Java. - -Для реальных задач вместо своей реализации берут `StateFlow` или `SharedFlow`: там уже решены обратное давление, конкурентность и жизненный цикл подписки. Подробнее: [StateFlow и SharedFlow](../Корутины/StateFlow%20и%20SharedFlow.md) - -Для одного наблюдаемого свойства есть готовый делегат: -```kotlin -import kotlin.properties.Delegates - -class Model { - var status: String by Delegates.observable("новый") { _, old, new -> - println("статус: $old -> $new") - } -} - -fun main() { - val m = Model() - m.status = "в работе" // статус: новый -> в работе - m.status = "готов" // статус: в работе -> готов -} -``` - -## Template Method -Паттерн, где базовый класс задаёт скелет алгоритма, а подклассы заполняют шаги. В Kotlin это функция с лямбдами: -```kotlin -fun processFile( - lines: List, - parse: (String) -> T?, - onError: (String) -> Unit = { println("пропущено: $it") }, -): List = lines.mapNotNull { line -> - parse(line) ?: null.also { onError(line) } -} - -fun main() { - val result = processFile( - lines = listOf("1", "два", "3"), - parse = { it.toIntOrNull() }, - ) - println(result) - // пропущено: два - // [1, 3] -} -``` -Наследование не нужно: шаги передаются параметрами. Это и проще (нет иерархии), и гибче (шаги можно комбинировать в рантайме). - -## Command -```kotlin -class History { - private val undoStack = mutableListOf<() -> Unit>() - - fun execute(action: () -> Unit, undo: () -> Unit) { - action() - undoStack.add(undo) - } - - fun undo() { undoStack.removeLastOrNull()?.invoke() } -} - -fun main() { - val text = StringBuilder() - val history = History() - - history.execute({ text.append("Привет") }, { text.setLength(0) }) - println(text) // Привет - history.undo() - println("[$text]") // [] -} -``` -Интерфейс `Command` с методом `execute` заменяется лямбдой. Если у команды есть состояние или несколько операций - `data class` с методами честнее. - -## Visitor -Паттерн нужен там, где над закрытой иерархией выполняют разные операции. В Kotlin это `sealed` + `when`: -```kotlin -sealed interface Expr { - data class Num(val value: Int) : Expr - data class Add(val left: Expr, val right: Expr) : Expr - data class Mul(val left: Expr, val right: Expr) : Expr -} - -fun eval(e: Expr): Int = when (e) { - is Expr.Num -> e.value - is Expr.Add -> eval(e.left) + eval(e.right) - is Expr.Mul -> eval(e.left) * eval(e.right) -} - -fun render(e: Expr): String = when (e) { - is Expr.Num -> e.value.toString() - is Expr.Add -> "(${render(e.left)} + ${render(e.right)})" - is Expr.Mul -> "${render(e.left)} * ${render(e.right)}" -} - -fun main() { - val expr = Expr.Add(Expr.Num(2), Expr.Mul(Expr.Num(3), Expr.Num(4))) - println(render(expr)) // (2 + 3 * 4) - println(eval(expr)) // 14 -} -``` -Новая операция - новая функция, менять классы не нужно. Компилятор проверяет полноту `when`, поэтому добавление варианта в `sealed` сразу покажет все места, требующие правки - то, чего Visitor добивается ценой двойной диспетчеризации. Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md) - -## Prototype и Null Object -```kotlin -data class Config4(val host: String, val port: Int, val debug: Boolean = false) - -fun main() { - val prod = Config4("example.com", 443) - val staging = prod.copy(host = "staging.example.com", debug = true) - println(staging) - // Config4(host=staging.example.com, port=443, debug=true) -} -``` -`copy` - это Prototype, встроенный в `data class`. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md) - -Null Object создавался, чтобы избежать проверок на `null`. Nullable-типы решают ту же задачу явнее: -```kotlin -interface Logger3 { fun log(m: String) } - -fun process6(data: String, logger: Logger3? = null) { - logger?.log("начало") - println("обработано: $data") - logger?.log("конец") -} - -fun main() { - process6("x") // обработано: x - process6("y", object : Logger3 { override fun log(m: String) = println("[$m]") }) - // [начало] - // обработано: y - // [конец] -} -``` -Пустая реализация-заглушка не нужна: `?.` не вызывает метод при `null`. Подробнее: [Операторы null-безопасности](../Синтаксис%20и%20типы/Null-безопасность/Операторы%20null-безопасности.md) - -## Что остаётся нужным -Паттерны, описывающие **архитектуру**, а не обход ограничений языка, остаются актуальными: Repository, Facade, Adapter между слоями, Strategy с несколькими методами, State-машина, Chain of Responsibility. - -```kotlin -sealed interface OrderState { - data object New : OrderState - data object Paid : OrderState - data class Shipped(val track: String) : OrderState - data object Cancelled : OrderState -} - -fun next(state: OrderState, event: String): OrderState = when (state) { - OrderState.New -> when (event) { - "pay" -> OrderState.Paid - "cancel" -> OrderState.Cancelled - else -> state - } - OrderState.Paid -> if (event == "ship") OrderState.Shipped("TRACK-1") else state - is OrderState.Shipped -> state - OrderState.Cancelled -> state -} - -fun main() { - var state: OrderState = OrderState.New - for (event in listOf("pay", "ship", "cancel")) { - state = next(state, event) - println("$event -> $state") - } - // pay -> Paid - // ship -> Shipped(track=TRACK-1) - // cancel -> Shipped(track=TRACK-1) -} -``` -Здесь `sealed` + `when` не отменяют паттерн State, а дают ему надёжную реализацию: невозможное состояние не создать, а забытый переход не скомпилируется. - -> [!info] Отличия от Java -> | Паттерн в Java | В Kotlin | -> |---|---| -> | `private static final INSTANCE` + `getInstance()` | `object` | -> | класс `Builder` с 10 методами и `build()` | именованные аргументы | -> | `interface Strategy` + 3 класса | `(T) -> R` | -> | декоратор с ручным проксированием всех методов | `by delegate` | -> | `List` + `addListener`/`removeListener` | лямбды или `Flow` | -> | `abstract class` с `protected abstract` шагами | функция с лямбдами | -> | `interface Visitor` с `visitX` на каждый тип | `sealed` + `when` | -> | `clone()` или конструктор копирования | `copy()` | -> | `NullLogger implements Logger {}` | `Logger?` + `?.` | -> -> **Главная разница - в количестве кода на тот же смысл.** Builder для класса с пятью полями в Java - это ~60 строк (вложенный класс, поле на каждый параметр, метод на каждый параметр, `build()`). В Kotlin это объявление `data class` с значениями по умолчанию, то есть ноль дополнительных строк. То же с Singleton (было: приватный конструктор, статическое поле, `getInstance`, возможно двойная проверка блокировки; стало: `object`) и Strategy (было: интерфейс плюс класс на каждую реализацию; стало: лямбда). -> -> **Декоратор - самый показательный случай.** В Java, чтобы задекорировать интерфейс с 15 методами, нужно написать 15 делегирующих методов, из которых интересен один. Отсюда классы вроде `HttpServletRequestWrapper` в стандартной библиотеке - их пишут специально, чтобы наследоваться. В Kotlin `by delegate` делает это автоматически: -> ```kotlin -> interface Big { -> fun a(): Int -> fun b(): Int -> fun c(): Int -> } -> -> class Impl : Big { -> override fun a() = 1 -> override fun b() = 2 -> override fun c() = 3 -> } -> -> class OnlyBChanged(d: Big) : Big by d { -> override fun b() = 99 -> } -> -> fun main() { -> val x = OnlyBChanged(Impl()) -> println("${x.a()} ${x.b()} ${x.c()}") // 1 99 3 -> } -> ``` -> -> **Паттерны не исчезли - исчез их boilerplate.** Смысл Strategy (вынести изменяемое поведение) остался и в Kotlin, просто выражается функциональным типом. Полезно знать паттерны именно как словарь: «здесь Strategy» понятнее, чем «здесь лямбда в конструкторе». -> -> **Некоторые паттерны в Kotlin не нужны вообще.** Null Object существовал, чтобы не писать `if (x != null)`; при наличии `?.` он только добавляет класс. Double-Checked Locking для синглтона не нужен, потому что `object` и `lazy` уже потокобезопасны. -> -> Сравнить: [Singleton в Java](../../Программирование%20на%20Java/Паттерны%20проектирования/Singleton.md), [Builder](../../Программирование%20на%20Java/Паттерны%20проектирования/Builder.md) - -> [!warning] Подводные камни -> **`object` - глобальное состояние, которое не подменить в тестах:** -> ```kotlin -> object Clock2 { -> fun now(): Long = System.currentTimeMillis() -> } -> -> interface Clock3 { fun now(): Long } -> -> class Service4(private val clock: Clock3) { -> fun timestamp() = clock.now() -> } -> -> fun main() { -> val fake = object : Clock3 { override fun now() = 1_000L } -> println(Service4(fake).timestamp()) // 1000 — тест детерминирован -> println(Clock2.now() > 0) // true — а это не подменить -> } -> ``` -> Между `object` и интерфейсом с внедрением выбирайте второе, если поведение нужно будет подменить. -> -> **`object` с изменяемым состоянием не потокобезопасен.** Создание экземпляра защищено, а доступ к полям - нет: -> ```kotlin -> object Counter5 { -> var count = 0 // гонка при обращении из нескольких потоков -> } -> -> fun main() { -> Counter5.count++ -> println(Counter5.count) // 1 -> } -> ``` -> -> **Стратегия-лямбда теряет имя в стеке и логах:** -> ```kotlin -> fun main() { -> val strategies: Map Int> = mapOf("double" to { it * 2 }) -> val f = strategies.getValue("double") -> println(f::class.simpleName?.contains("Lambda")) // true — имя сгенерировано -> } -> ``` -> Вместо осмысленного имени получается что-то вроде `MainKt$$Lambda/0x00001e0001040620` - с адресом, меняющимся от запуска к запуску. Отладка «какая стратегия сработала» требует хранить имя рядом. Если это важно - интерфейс или `data class` с полем `name`. -> -> **`by delegate` делегирует только методы интерфейса.** `equals`, `hashCode` и `toString` берутся из `Any`, а не из делегата: -> ```kotlin -> interface Named5 { val name: String } -> data class Impl2(override val name: String) : Named5 -> -> class Wrapper(d: Named5) : Named5 by d -> -> fun main() { -> val w = Wrapper(Impl2("x")) -> println(w.name) // x -> println(w.toString().startsWith("Wrapper")) // true — не делегировано -> println(w == Wrapper(Impl2("x"))) // false -> } -> ``` -> -> **Делегат фиксируется при создании:** -> ```kotlin -> interface Source { fun value(): Int } -> -> class Changing(private var inner: Source) : Source by inner { -> fun replace(newInner: Source) { inner = newInner } -> } -> -> fun main() { -> val first = object : Source { override fun value() = 1 } -> val second = object : Source { override fun value() = 2 } -> val c = Changing(first) -> c.replace(second) -> println(c.value()) // 1 — делегирует первому! -> } -> ``` -> `by` запоминает ссылку в момент конструирования. Для подменяемого делегата нужно явное проксирование метода. -> -> **Свой Observer течёт по памяти:** -> ```kotlin -> class Leaky3 { -> private val handlers = mutableListOf<(Int) -> Unit>() -> fun subscribe(h: (Int) -> Unit) { handlers.add(h) } -> fun count() = handlers.size -> } -> -> fun main() { -> val p = Leaky3() -> repeat(3) { p.subscribe { } } -> println(p.count()) // 3 — и никогда не уменьшится -> } -> ``` -> Без механизма отписки подписчики живут до конца жизни издателя и держат свои замыкания. Поэтому для событий лучше `Flow` с привязкой к области корутины. Подробнее: [Структурированная конкурентность](../Корутины/Структурированная%20конкурентность.md) -> -> **Лямбда в коллекции не сравнивается:** -> ```kotlin -> fun main() { -> val handlers = mutableListOf<() -> Unit>() -> val h = { println("x") } -> handlers.add(h) -> println(handlers.remove(h)) // true — та же ссылка -> handlers.add { println("x") } -> println(handlers.remove { println("x") }) // false — другой объект -> } -> ``` -> Отписка по «такой же» лямбде не работает - нужна именно та ссылка или токен отписки. -> -> **Builder через `apply` на `val`-полях невозможен** - нужны `var`, а значит объект изменяем после сборки. Для неизменяемого результата используйте `data class` с конструктором или отдельный билдер, возвращающий immutable-объект. -> -> **Именованные аргументы не защищают от неверного сочетания.** `HttpRequest(url = "x", retries = -5)` скомпилируется. Валидацию нужно писать в `init`: -> ```kotlin -> data class Retry(val attempts: Int) { -> init { require(attempts >= 0) { "attempts не может быть отрицательным" } } -> } -> -> fun main() { -> println(Retry(3)) -> try { Retry(-1) } catch (e: IllegalArgumentException) { println(e.message) } -> // Retry(attempts=3) -> // attempts не может быть отрицательным -> } -> ``` -> Подробнее: [Проверки и контракты](../Исключения/Проверки%20и%20контракты.md) -> -> **Применение паттерна там, где он не нужен, остаётся главной проблемой** - и язык от этого не спасает. Фабрика для класса с одним конструктором, Strategy с единственной реализацией и Repository поверх одного SQL-запроса добавляют уровни без выгоды. Критерий прежний: паттерн оправдан, когда вариативность уже существует, а не когда она предполагается. - -Далее: [Корутины](../Корутины/Корутины.md) diff --git a/Kotlin/Функциональный Kotlin/Функции высшего порядка.md b/Kotlin/Функциональный Kotlin/Функции высшего порядка.md deleted file mode 100644 index a4bcc69..0000000 --- a/Kotlin/Функциональный Kotlin/Функции высшего порядка.md +++ /dev/null @@ -1,392 +0,0 @@ ---- -title: Функции высшего порядка -description: Функции, принимающие и возвращающие функции, композиция, каррирование и мемоизация -tags: - - kotlin - - functional ---- - -Функция высшего порядка - это функция, которая принимает функцию в параметре или возвращает функцию. В Kotlin функциональный тип (`(Int) -> String`) полноправный, поэтому ничего особенного для этого не нужно. - -```kotlin -fun applyTwice(x: Int, f: (Int) -> Int): Int = f(f(x)) - -fun main() { - println(applyTwice(3) { it * 2 }) // 12 - println(applyTwice(3) { it + 10 }) // 23 -} -``` - -Возврат функции: -```kotlin -fun multiplier(factor: Int): (Int) -> Int = { it * factor } - -fun main() { - val triple = multiplier(3) - println(triple(5)) // 15 - println(multiplier(10)(7)) // 70 -} -``` -`multiplier(3)` возвращает лямбду, которая **захватила** `factor`. Это замыкание, и оно работает так же, как в любом функциональном языке. Подробнее о синтаксисе: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md) - -## Зачем это нужно -Функция высшего порядка позволяет вынести **изменяемую часть** алгоритма в параметр. Классический пример - повторяющаяся обвязка: - -```kotlin -fun retry(times: Int, block: () -> T): T { - var lastError: Throwable? = null - repeat(times) { attempt -> - try { - return block() - } catch (e: Exception) { - lastError = e - println("попытка ${attempt + 1} не удалась") - } - } - throw IllegalStateException("все $times попыток провалились", lastError) -} - -fun main() { - var counter = 0 - val result = retry(3) { - counter++ - if (counter < 3) error("ещё не готово") - "успех на попытке $counter" - } - println(result) // успех на попытке 3 -} -``` -Логика повторов написана один раз; что именно повторять - решает вызывающий. Без функциональных типов пришлось бы заводить интерфейс с одним методом и анонимный класс. - -Та же идея для измерения, транзакций и логирования: -```kotlin -inline fun measured(name: String, block: () -> T): T { - val start = System.nanoTime() - try { - return block() - } finally { - println("$name: ${(System.nanoTime() - start) / 1_000} мкс") - } -} - -fun main() { - val sum = measured("сумма") { (1..100_000).sumOf { it.toLong() } } - println(sum) // 5000050000 -} -``` -`finally` здесь важен: время выводится и при исключении. `inline` убирает объект лямбды - подробнее: [Inline-функции](Inline-функции.md) - -## Функциональные типы как параметры -```kotlin -fun processNumbers( - numbers: List, - filter: (Int) -> Boolean = { true }, - transform: (Int) -> String = { it.toString() }, -): List = numbers.filter(filter).map(transform) - -fun main() { - println(processNumbers(listOf(1, 2, 3))) // [1, 2, 3] - println(processNumbers(listOf(1, 2, 3), filter = { it > 1 })) // [2, 3] - println(processNumbers(listOf(1, 2), transform = { "№$it" })) // [№1, №2] -} -``` -Функциональный параметр может иметь значение по умолчанию - это заменяет перегрузки. - -Nullable функциональный тип требует скобок: -```kotlin -fun handle(value: Int, onSuccess: ((Int) -> Unit)? = null): Int { - onSuccess?.invoke(value) - return value -} - -fun main() { - println(handle(1)) // 1 - println(handle(2) { println("получено $it") }) // получено 2 \n 2 -} -``` -`(Int) -> Unit?` - это функция, возвращающая `Unit?`. Скобки меняют смысл принципиально. - -## Композиция -```kotlin -infix fun ((A) -> B).then(next: (B) -> C): (A) -> C = { next(this(it)) } - -fun main() { - val parse: (String) -> Int = { it.trim().toInt() } - val double: (Int) -> Int = { it * 2 } - val format: (Int) -> String = { "результат: $it" } - - val pipeline = parse then double then format - println(pipeline(" 21 ")) // результат: 42 -} -``` -В stdlib готовой композиции нет - её пишут сами в три строки. Направление `then` (слева направо) читается лучше математического `compose`, но встречаются оба: -```kotlin -infix fun ((B) -> C).compose(before: (A) -> B): (A) -> C = { this(before(it)) } - -fun main() { - val inc: (Int) -> Int = { it + 1 } - val double: (Int) -> Int = { it * 2 } - - println((inc compose double)(5)) // 11 — сначала double, потом inc - println((double compose inc)(5)) // 12 — наоборот -} -``` - -## Каррирование и частичное применение -```kotlin -fun curry(f: (A, B) -> C): (A) -> (B) -> C = { a -> { b -> f(a, b) } } - -fun main() { - val sum: (Int, Int) -> Int = { a, b -> a + b } - val curried = curry(sum) - - println(curried(2)(3)) // 5 - val add10 = curried(10) - println(add10(5)) // 15 - println(listOf(1, 2, 3).map(add10)) // [11, 12, 13] -} -``` -На практике частичное применение чаще делают прямо лямбдой - это короче и понятнее: -```kotlin -fun log(level: String, module: String, message: String) = "[$level][$module] $message" - -fun main() { - val errorInAuth: (String) -> String = { log("ERROR", "auth", it) } - println(errorInAuth("неверный пароль")) // [ERROR][auth] неверный пароль -} -``` -Ещё проще - значения по умолчанию и именованные аргументы, которые в Kotlin закрывают большинство задач, решаемых каррированием в других языках. Подробнее: [Функции](../Синтаксис%20и%20типы/Работа%20с%20данными/Функции.md) - -## Мемоизация -```kotlin -fun memoize(f: (A) -> R): (A) -> R { - val cache = mutableMapOf() - return { arg -> cache.getOrPut(arg) { f(arg) } } -} - -fun main() { - var calls = 0 - val slowSquare = memoize { calls++; it * it } - - println(slowSquare(4)) // 16 - println(slowSquare(4)) // 16 - println(slowSquare(5)) // 25 - println("вызовов: $calls") // вызовов: 2 -} -``` -`getOrPut` делает это в одну строку. Ограничения такого кеша: он не ограничен по размеру и не потокобезопасен - для продакшена нужен `ConcurrentHashMap` или готовый кеш. - -Рекурсия с мемоизацией требует ссылки на саму функцию: -```kotlin -fun main() { - val cache = mutableMapOf(0 to 0L, 1 to 1L) - fun fib(n: Int): Long = cache.getOrPut(n) { fib(n - 1) + fib(n - 2) } - - println(fib(50)) // 12586269025 - println(cache.size) // 51 -} -``` -Локальная функция может вызывать себя, а лямбда - нет: внутри `val f = { ... f(...) }` имя `f` ещё не инициализировано. - -## Функции, возвращающие функции -Типичный случай - настраиваемый валидатор: -```kotlin -fun lengthBetween(min: Int, max: Int): (String) -> String? = { value -> - when { - value.length < min -> "короче $min символов" - value.length > max -> "длиннее $max символов" - else -> null - } -} - -fun validateAll(value: String, vararg rules: (String) -> String?): List = - rules.mapNotNull { it(value) } - -fun main() { - val notBlank: (String) -> String? = { if (it.isBlank()) "пустое значение" else null } - - println(validateAll("ab", notBlank, lengthBetween(3, 10))) // [короче 3 символов] - println(validateAll("привет", notBlank, lengthBetween(3, 10))) // [] - println(validateAll(" ", notBlank)) // [пустое значение] -} -``` -Правила - обычные значения: их можно хранить в списке, передавать и комбинировать. Это основная практическая выгода функций высшего порядка. - -## Ссылки на функции вместо лямбд -```kotlin -fun isEven(x: Int) = x % 2 == 0 - -fun main() { - val numbers = listOf(1, 2, 3, 4) - - println(numbers.filter { isEven(it) }) // [2, 4] — лямбда-обёртка - println(numbers.filter(::isEven)) // [2, 4] — ссылка напрямую - println(numbers.map(Int::toString)) // [1, 2, 3, 4] — ссылка на метод - println(listOf("1", "2").map(String::toInt)) // [1, 2] - - val strings = listOf("bb", "a", "ccc") - println(strings.sortedBy(String::length)) // [a, bb, ccc] -} -``` -Ссылка `::isEven` короче и не создаёт лишнего уровня вложенности. Подробнее: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md) - -## Лямбда с получателем -Особый вид функционального типа - `T.() -> R`. Внутри такой лямбды `this` - это `T`: -```kotlin -fun buildMessage(build: StringBuilder.() -> Unit): String { - val sb = StringBuilder() - sb.build() - return sb.toString() -} - -fun main() { - val text = buildMessage { - append("Привет") // this == StringBuilder - append(", ") - append("мир") - } - println(text) // Привет, мир -} -``` -На этом построены все билдеры Kotlin, включая `buildList` и `apply`. Подробнее: [Receivers и context parameters](Receivers%20и%20context%20parameters.md), [DSL](DSL.md) - -## Карта раздела -| Заметка | О чём | -|---|---| -| [Inline-функции](Inline-функции.md) | как убрать накладные расходы лямбд | -| [Функции-расширения](Функции-расширения.md) | добавление методов к готовым типам | -| [Scope-функции](Scope-функции.md) | `let`, `run`, `apply`, `also`, `with` | -| [Инфиксные и операторные функции](Инфиксные%20и%20операторные%20функции.md) | свой синтаксис для своих типов | -| [Receivers и context parameters](Receivers%20и%20context%20parameters.md) | неявный `this` и контекстные зависимости | -| [DSL](DSL.md) | сборка всего перечисленного в язык под задачу | -| [Паттерны проектирования](Паттерны%20проектирования.md) | что от паттернов осталось в Kotlin | - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `Function`, `BiFunction`, `Supplier`, `Consumer` | один синтаксис `(T) -> R` | -> | `Predicate` | `(T) -> Boolean` | -> | `Runnable` | `() -> Unit` | -> | `IntFunction`, `ToIntFunction`, ... (40+ интерфейсов) | нет, но есть упаковка | -> | `f.apply(x)` | `f(x)` | -> | `f.andThen(g)` | своя `then` | -> | лямбда - объект | лямбда инлайнится при `inline` | -> | нет аналога | лямбда с получателем `T.() -> R` | -> -> **Функциональный тип вместо интерфейса.** В Java функция передаётся как реализация интерфейса, и интерфейс нужно выбрать: `Function` для одного аргумента, `BiFunction` для двух, `Supplier` без аргументов, `Consumer` без результата, `Predicate` для `boolean`, плюс примитивные специализации - в `java.util.function` их больше сорока. В Kotlin тип записывается напрямую: -> ```kotlin -> fun demo( -> supplier: () -> Int, -> consumer: (Int) -> Unit, -> predicate: (Int) -> Boolean, -> biFunction: (Int, Int) -> Int, -> ) { -> val v = supplier() -> consumer(v) -> println("${predicate(v)} ${biFunction(v, v)}") -> } -> -> fun main() { demo({ 2 }, { }, { it > 1 }, { a, b -> a + b }) } // true 4 -> ``` -> Под капотом это всё равно интерфейсы `Function0`, `Function1`, ... `Function22`, но в коде их не видно. Три с лишним аргумента в Java требуют собственного интерфейса - в Kotlin предел 22. -> -> **Вызов через `invoke`, а не `apply`.** В Java функциональный объект вызывают методом интерфейса: `f.apply(x)`, `p.test(x)`, `c.accept(x)`, `s.get()` - имя зависит от интерфейса. В Kotlin оператор `()` работает для любого функционального типа, потому что все они объявляют `operator fun invoke`. -> -> **Композиции в stdlib нет.** Java даёт `Function.andThen`, `Function.compose`, `Predicate.and/or/negate`. Kotlin их не предоставляет сознательно: обычно короче написать одну лямбду, чем собирать её из частей. Когда композиция действительно нужна, она пишется в одну строку - как `then` выше. -> -> **Лямбда с получателем аналога в Java не имеет.** Это основа билдеров и DSL в Kotlin; в Java эквивалента нет даже близко, поэтому билдеры там пишут методами, возвращающими `this`. -> -> Сравнить: [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md), [Интерфейсы в Java](../../Программирование%20на%20Java/ООП/Интерфейсы.md) - -> [!warning] Подводные камни -> **Лямбда без `inline` создаёт объект на каждый вызов:** -> ```kotlin -> fun notInlined(block: () -> Unit) { block() } -> inline fun inlined(block: () -> Unit) { block() } -> -> fun main() { -> repeat(3) { notInlined { } } // объект Function0 на каждый вызов -> repeat(3) { inlined { } } // объектов нет -> println("готово") -> } -> ``` -> Лямбда без захвата переменных кешируется компилятором в синглтон, а лямбда с захватом - нет. В горячем цикле это заметно. Подробнее: [Inline-функции](Inline-функции.md) -> -> **`return` внутри лямбды возвращает из внешней функции - но только в inline:** -> ```kotlin -> inline fun forEachInline(items: List, action: (Int) -> Unit) { for (i in items) action(i) } -> -> fun findFirstEven(items: List): Int? { -> forEachInline(items) { if (it % 2 == 0) return it } // выход из findFirstEven -> return null -> } -> -> fun main() { println(findFirstEven(listOf(1, 3, 4, 6))) } // 4 -> ``` -> В не-inline функции такой `return` не скомпилируется - нужен `return@label`. Это разное поведение для внешне одинакового кода. -> -> **Nullable функциональный тип требует скобок:** -> ```kotlin -> fun main() { -> val a: ((Int) -> Unit)? = null // функция может быть null -> val b: (Int) -> Unit? = { } // функция возвращает Unit? -> println("${a == null} ${b(1)}") // true kotlin.Unit -> } -> ``` -> Без скобок получается совсем другой тип, и ошибка проявится далеко от объявления. -> -> **Вызов nullable-функции только через `invoke`:** -> ```kotlin -> fun main() { -> val f: ((Int) -> Int)? = { it * 2 } -> // println(f?(1)) // синтаксическая ошибка -> println(f?.invoke(1)) // 2 -> } -> ``` -> -> **Захваченная `var` меняется после создания лямбды:** -> ```kotlin -> fun main() { -> var counter = 0 -> val show = { println("counter = $counter") } -> counter = 10 -> show() // counter = 10, а не 0 -> } -> ``` -> В отличие от Java, Kotlin разрешает захват изменяемых переменных - они оборачиваются в объект-ссылку. Удобно, но лямбда перестаёт быть чистой функцией и становится зависимой от момента вызова. -> -> **Лямбды в цикле захватывают переменную, а не значение:** -> ```kotlin -> fun main() { -> val actions = mutableListOf<() -> Int>() -> var i = 0 -> while (i < 3) { actions.add { i }; i++ } -> println(actions.map { it() }) // [3, 3, 3] — все видят финальное i -> -> val correct = (0 until 3).map { v -> { v } } -> println(correct.map { it() }) // [0, 1, 2] — параметр цикла свой у каждой -> -> val fixed = mutableListOf<() -> Int>() -> for (j in 0 until 3) fixed.add { j } -> println(fixed.map { it() }) // [0, 1, 2] — переменная for неизменяема -> } -> ``` -> -> **Мемоизация течёт по памяти.** `mutableMapOf` в замыкании растёт без ограничений и держит все аргументы и результаты. Если функция живёт всё время работы приложения, это утечка; нужен кеш с ограничением размера. -> -> **Мемоизация не потокобезопасна.** `getOrPut` на обычном `MutableMap` при одновременном доступе может повредить структуру. Минимальная защита - `ConcurrentHashMap`, но и у него `getOrPut` не атомарен: используйте `computeIfAbsent`. -> -> **Лямбда не может ссылаться на себя:** -> ```kotlin -> fun main() { -> // val fact: (Int) -> Int = { if (it <= 1) 1 else it * fact(it - 1) } // ошибка -> fun fact(n: Int): Int = if (n <= 1) 1 else n * fact(n - 1) // локальная функция может -> println(fact(5)) // 120 -> } -> ``` -> -> **Больше двух-трёх функциональных параметров делают вызов нечитаемым.** `process(list, { }, { }, { })` ничего не говорит о смысле аргументов. Варианты: именованные аргументы, объект-конфигурация или интерфейс с несколькими методами. -> -> **Функциональные типы упаковывают примитивы.** `(Int) -> Int` в байт-коде - это `Function1`. Специализаций вроде `IntUnaryOperator` в Kotlin нет, поэтому в численно-интенсивном коде `inline` важен не только из-за объекта лямбды, но и из-за упаковки. - -Далее: [Inline-функции](Inline-функции.md) diff --git a/Kotlin/Функциональный Kotlin/Функции-расширения.md b/Kotlin/Функциональный Kotlin/Функции-расширения.md deleted file mode 100644 index c8b790f..0000000 --- a/Kotlin/Функциональный Kotlin/Функции-расширения.md +++ /dev/null @@ -1,361 +0,0 @@ ---- -title: Функции-расширения -description: Добавление методов к готовым типам, статическое разрешение и свойства-расширения -tags: - - kotlin - - functional ---- - -Функция-расширение добавляет метод к типу, который вы не контролируете - без наследования и без обёрток. - -```kotlin -fun String.isPalindrome(): Boolean { - val clean = lowercase().filter { it.isLetter() } - return clean == clean.reversed() -} - -fun main() { - println("Довод".isPalindrome()) // true - println("А роза упала на лапу Азора".isPalindrome()) // true - println("Привет".isPalindrome()) // false -} -``` -Внутри расширения `this` - это объект, на котором вызвали функцию (его называют **получателем**, receiver). `this` можно опускать: `lowercase()` - это `this.lowercase()`. - -## Это обычная статическая функция -Расширение не меняет класс и не попадает в него. Компилятор превращает его в статический метод с получателем первым параметром: -```kotlin -fun Int.double(): Int = this * 2 - -fun main() { - println(5.double()) // 10 — выглядит как метод - // В байт-коде: Файл.double(5) -} -``` -Из этого следуют все особенности расширений - и удобные, и опасные. - -**Нет доступа к `private`:** -```kotlin -class Account(private val balance: Long, val owner: String) - -// fun Account.canWithdraw(x: Long) = balance >= x // ошибка: balance недоступен -fun Account.describe() = "счёт $owner" // public — можно - -fun main() { - println(Account(100, "Иван").describe()) // счёт Иван -} -``` -Расширение - внешний код, инкапсуляция работает. Это принципиальное ограничение: расширение не заменяет метод класса, когда нужен доступ к состоянию. - -**Разрешение статическое, не виртуальное:** -```kotlin -open class Shape -class Circle : Shape() - -fun Shape.name() = "фигура" -fun Circle.name() = "круг" - -fun main() { - val shape: Shape = Circle() - println(shape.name()) // фигура — тип переменной, не объекта! - println(Circle().name()) // круг -} -``` -Это главная ловушка расширений. Выбор функции делается компилятором по **объявленному** типу, поэтому полиморфизма здесь нет. Подробнее: [Полиморфизм](../ООП/Полиморфизм.md) - -**Метод класса всегда выигрывает:** -```kotlin -class Printer { - fun show() = "метод класса" -} - -fun Printer.show() = "расширение" // warning: extension is shadowed by a member - -fun main() { - println(Printer().show()) // метод класса -} -``` -Это гарантия совместимости: добавление метода в класс не ломает код, но молча меняет поведение вызова. Компилятор предупреждает. - -## Расширения на nullable-типах -```kotlin -fun String?.orPlaceholder(): String = if (isNullOrBlank()) "(пусто)" else this - -fun main() { - println("текст".orPlaceholder()) // текст - println(null.orPlaceholder()) // (пусто) - println(" ".orPlaceholder()) // (пусто) -} -``` -Расширение на `T?` можно вызвать на `null` без `?.` - внутри `this` просто может быть `null`. Так устроены `isNullOrEmpty`, `orEmpty` и `toString` в stdlib: -```kotlin -fun main() { - val s: String? = null - println(s.isNullOrEmpty()) // true — без ?. ! - println(s.orEmpty().length) // 0 - println(s.toString()) // null — строка "null" -} -``` - -## Свойства-расширения -```kotlin -val String.firstWord: String get() = substringBefore(" ") -val List.secondOrNull: T? get() = getOrNull(1) -val Int.isEven: Boolean get() = this % 2 == 0 - -fun main() { - println("привет мир".firstWord) // привет - println(listOf(1, 2, 3).secondOrNull) // 2 - println(4.isEven) // true -} -``` -У свойства-расширения **не может быть поля** - только геттер (и сеттер, если получатель изменяемый): -```kotlin -var StringBuilder.lastChar: Char - get() = this[length - 1] - set(value) { setCharAt(length - 1, value) } - -fun main() { - val sb = StringBuilder("Котлин") - println(sb.lastChar) // н - sb.lastChar = '!' - println(sb) // Котли! -} -``` -Хранить состояние в расширении нельзя: `val Int.counter = 0` не скомпилируется, потому что хранить его негде. - -## Расширения на дженериках -```kotlin -fun List.penultimate(): T? = getOrNull(size - 2) - -fun > List.isSorted(): Boolean = zipWithNext().all { (a, b) -> a <= b } - -fun T.alsoPrint(label: String): T = also { println("$label: $it") } - -fun main() { - println(listOf(1, 2, 3).penultimate()) // 2 - println(listOf(1, 2, 3).isSorted()) // true - val x = 42.alsoPrint("значение") // значение: 42 - println(x + 1) // 43 -} -``` -Расширение на `T` без ограничений применимо к любому типу - именно так объявлены все [scope-функции](Scope-функции.md). - -Расширение можно объявить на конкретной параметризации: -```kotlin -fun List.sumOfSquares(): Int = sumOf { it * it } -fun List.longest(): String? = maxByOrNull { it.length } - -fun main() { - println(listOf(1, 2, 3).sumOfSquares()) // 14 - println(listOf("a", "ccc", "bb").longest()) // ccc -} -``` -Так же работает `Iterable.sum()` в stdlib - одна функция на каждый числовой тип. - -## Расширения на функциональных типах -```kotlin -fun ((T) -> R).withLogging(name: String): (T) -> R = { arg -> - println("$name($arg)") - this(arg) -} - -fun main() { - val double: (Int) -> Int = { it * 2 } - val logged = double.withLogging("double") - println(logged(21)) - // double(21) - // 42 -} -``` -Функциональный тип - обычный тип, расширения на нём работают так же. - -## Расширения на объектах-компаньонах -```kotlin -class Celsius(val value: Double) { - companion object {} - override fun toString() = "$value°C" -} - -fun Celsius.Companion.fromFahrenheit(f: Double) = Celsius((f - 32) * 5 / 9) - -fun main() { - println(Celsius.fromFahrenheit(68.0)) // 20.0°C -} -``` -Так к типу добавляют фабричный метод, вызываемый через имя класса. Два условия: `companion object` должен быть объявлен в классе (пустого объявления достаточно) и конструктор должен быть доступен - расширение на `Companion` остаётся внешним кодом и приватный конструктор не увидит. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md) - -## Где объявлять -```kotlin -class Order6(val items: List, val total: Long) { - // расширение внутри класса — видно только внутри - private fun String.shorten() = take(3) - - fun summary() = items.joinToString { it.shorten() } -} - -fun main() { - println(Order6(listOf("молоко", "хлеб"), 100).summary()) // мол, хле -} -``` -Расширение, объявленное внутри класса, имеет два получателя: `this` расширения и `this` класса. Это основа DSL - подробнее: [Receivers и context parameters](Receivers%20и%20context%20parameters.md) - -Для расширений верхнего уровня принято заводить файл по теме: -```kotlin -// StringExtensions.kt -fun String.toSlug() = lowercase().replace(Regex("[^a-zа-я0-9]+"), "-").trim('-') - -fun main() { - println("Привет, Мир!".toSlug()) // привет-мир -} -``` - -## Когда расширение, когда метод -| Ситуация | Выбор | -|---|---| -| нужен доступ к `private`-состоянию | метод класса | -| поведение - часть сути типа | метод класса | -| нужен полиморфизм (`override`) | метод класса | -| тип чужой (stdlib, библиотека) | расширение | -| утилита, нужная не всем пользователям типа | расширение | -| адаптация типа к своему слою (DTO -> модель) | расширение | -| логика зависит от внешних сервисов | расширение или отдельный класс | - -Практическое правило: расширение не должно притворяться частью типа, если оно меняет его инварианты. Для чистых преобразований и утилит это идеальный инструмент. - -> [!info] Отличия от Java -> | Java | Kotlin | -> |---|---| -> | `StringUtils.isPalindrome(s)` | `s.isPalindrome()` | -> | класс `*Utils` со статикой | расширения в файле | -> | `default`-метод в интерфейсе | расширение на интерфейсе | -> | наследование или декоратор | расширение | -> | нет аналога | свойства-расширения | -> -> **Расширение - это и есть статический утилитный метод, но с синтаксисом метода.** В Java накопление утилит приводит к классам вроде `StringUtils`, `CollectionUtils`, `DateUtils`, и вызов читается наизнанку: `StringUtils.capitalize(StringUtils.trim(s))`. В Kotlin то же самое читается слева направо: -> ```kotlin -> fun String.capitalizeFirst() = replaceFirstChar { it.uppercase() } -> -> fun main() { -> println(" привет ".trim().capitalizeFirst()) // Привет -> } -> ``` -> Разницы в байт-коде нет: и там и там статический метод. Отличается только порядок чтения и то, что IDE подсказывает расширения в автодополнении по типу. -> -> **`default`-методы требуют владения интерфейсом.** В Java добавить поведение к `List` можно только если вы автор `List`. Расширение объявляется где угодно - поэтому `List` в Kotlin имеет ~150 операций, не входящих в интерфейс. -> -> **Вызов из Java выглядит как статика.** Расширение `fun String.toSlug()` в файле `StringExtensions.kt` компилируется в `StringExtensionsKt.toSlug(String)`: -> ```kotlin -> @file:JvmName("StringUtils") -> package demo -> -> fun String.toSlug() = lowercase() -> ``` -> Java-код вызовет `StringUtils.toSlug(s)`. То есть для Java-клиентов это ровно привычный утилитный класс. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md) -> -> **Lombok-подобных расширений в Java нет вообще.** Попытки добиться похожего (манифольд, плагины компилятора) остаются экзотикой, потому что требуют нестандартного компилятора. -> -> Сравнить: [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md), [Интерфейсы в Java](../../Программирование%20на%20Java/ООП/Интерфейсы.md) - -> [!warning] Подводные камни -> **Разрешение статическое - полиморфизма нет:** -> ```kotlin -> open class Animal -> class Dog : Animal() -> -> fun Animal.sound() = "..." -> fun Dog.sound() = "гав" -> -> fun makeSound(a: Animal) = a.sound() -> -> fun main() { -> println(Dog().sound()) // гав -> println(makeSound(Dog())) // ... — выбрано по типу параметра! -> } -> ``` -> Это самая частая ошибка с расширениями. Если поведение должно зависеть от фактического типа - нужен метод класса или `when` по типу. -> -> **Метод класса перекрывает расширение молча для вызывающего:** -> ```kotlin -> class Box8 { -> fun size() = 1 -> } -> -> fun Box8.size() = 999 -> -> fun main() { println(Box8().size()) } // 1 -> ``` -> Опаснее другое: если библиотека добавит метод `size()` в новой версии, ваше расширение перестанет вызываться после обновления - без ошибки компиляции. -> -> **Нет доступа к `private` и `protected`:** -> ```kotlin -> class Counter2 { -> private var count = 0 -> fun increment() { count++ } -> fun value() = count -> } -> -> // fun Counter2.reset() { count = 0 } // ошибка -> fun Counter2.incrementTwice() { increment(); increment() } -> -> fun main() { -> val c = Counter2() -> c.incrementTwice() -> println(c.value()) // 2 -> } -> ``` -> -> **Расширение на `T?` скрывает проверку на null:** -> ```kotlin -> fun String?.safeLength(): Int = this?.length ?: 0 -> -> fun main() { -> val s: String? = null -> println(s.safeLength()) // 0 — без ?. и без предупреждений -> } -> ``` -> Читатель видит `s.safeLength()` и может решить, что `s` не может быть `null`. Для такой функции нужно очень явное имя. -> -> **Свойство-расширение не хранит состояние:** -> ```kotlin -> // val Int.cached: Int = compute() // ошибка: initializer not allowed -> val Int.computed: Int get() = this * 2 // только геттер -> -> fun main() { println(2.computed) } // 4 -> ``` -> Геттер вызывается на каждое обращение. Для дорогих вычислений это неожиданная стоимость - `val` выглядит как поле, но является функцией. -> -> **Расширения не участвуют в `when` по типу и в рефлексии:** -> ```kotlin -> fun Any.describe5() = "объект" -> -> fun main() { -> println("x".describe5()) // объект -> println(String::class.members.any { it.name == "describe5" }) // false -> } -> ``` -> Для фреймворков, работающих через рефлексию (сериализация, ORM), расширения невидимы. -> -> **Расширение на `Any` засоряет автодополнение:** -> ```kotlin -> fun Any?.debug(): String = "значение: $this" -> -> fun main() { println(1.debug()) } // значение: 1 -> ``` -> Такая функция появится в подсказках для **каждого** типа в проекте. Расширения на `Any` оправданы только в stdlib-подобных случаях. -> -> **Одинаковые расширения из разных пакетов конфликтуют.** Если два импорта дают `String.toSlug()`, компилятор потребует явного импорта или алиаса (`import a.toSlug as aSlug`). Для библиотек это аргумент за специфичные имена. -> -> **Расширение не наследуется и не переопределяется.** `override fun String.foo()` невозможно: у расширений нет виртуальной таблицы. -> -> **Расширение на платформенном типе может получить `null` вопреки сигнатуре:** -> ```kotlin -> fun String.shout() = uppercase() + "!" -> -> fun main() { -> println("да".shout()) // ДА! -> } -> ``` -> Если такое расширение вызвать на значении, пришедшем из Java-метода без аннотаций, проверка на `null` сработает на входе и даст NPE внутри расширения - со стеком, указывающим не на источник проблемы. Подробнее: [Платформенные типы](../Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md) - -Далее: [Scope-функции](Scope-функции.md)