Added kotlin guide. (WIP) (15.10.26)
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
---
|
||||
title: Null-безопасность
|
||||
description: Разделение типов на nullable и non-null, как компилятор предотвращает NPE
|
||||
tags:
|
||||
- kotlin
|
||||
- null-safety
|
||||
---
|
||||
|
||||
Главная особенность системы типов Kotlin: **возможность быть null - часть типа**.
|
||||
|
||||
```kotlin
|
||||
var a: String = "text"
|
||||
// a = null // ошибка компиляции
|
||||
|
||||
var b: String? = "text"
|
||||
b = null // ok
|
||||
```
|
||||
|
||||
`String` и `String?` - разные типы. `String` является подтипом `String?`, но не наоборот.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
SN["String? — допускает null"]
|
||||
S["String — не допускает null"]
|
||||
N["Nothing? — только null"]
|
||||
SN --> S
|
||||
SN --> N
|
||||
```
|
||||
|
||||
## Зачем это нужно
|
||||
`NullPointerException` в Java возникает в рантайме, иногда далеко от места, где null попал в переменную. В Kotlin компилятор отказывается собирать такой код:
|
||||
|
||||
```kotlin
|
||||
fun length(s: String?): Int {
|
||||
// return s.length // ошибка: only safe (?.) or non-null (!!) calls are allowed
|
||||
return s?.length ?: 0
|
||||
}
|
||||
```
|
||||
Проблема переносится с этапа выполнения на этап компиляции. Разыменовать nullable-значение без проверки невозможно.
|
||||
|
||||
## Работа с nullable
|
||||
Пять подходов, от предпочтительного к крайнему:
|
||||
|
||||
### 1. Проверка на null
|
||||
После проверки компилятор сам приводит тип:
|
||||
```kotlin
|
||||
fun describe(s: String?): String {
|
||||
if (s == null) return "нет значения"
|
||||
return "длина ${s.length}" // здесь s уже String, не String?
|
||||
}
|
||||
```
|
||||
Это [умное приведение](Умные%20приведения.md).
|
||||
|
||||
### 2. Безопасный вызов - ?.
|
||||
```kotlin
|
||||
val len: Int? = s?.length // null, если s == null
|
||||
```
|
||||
|
||||
### 3. Elvis - ?:
|
||||
```kotlin
|
||||
val len: Int = s?.length ?: 0 // значение по умолчанию
|
||||
val name = user?.name ?: error("нет пользователя") // или выход
|
||||
```
|
||||
|
||||
### 4. Безопасное приведение - as?
|
||||
```kotlin
|
||||
val n: Int? = value as? Int // null вместо ClassCastException
|
||||
```
|
||||
|
||||
### 5. Утверждение - !!
|
||||
```kotlin
|
||||
val len: Int = s!!.length // NPE, если s == null
|
||||
```
|
||||
`!!` означает «я гарантирую, что здесь не null». Каждое его использование - это место, где вы отключили проверку компилятора.
|
||||
|
||||
Подробнее про операторы: [Операторы null-безопасности](Операторы%20null-безопасности.md)
|
||||
|
||||
## Nullable в коллекциях
|
||||
`?` ставится раздельно для элементов и для самой коллекции:
|
||||
```kotlin
|
||||
val a: List<String> = listOf("x") // список есть, элементы не null
|
||||
val b: List<String?> = listOf(null) // список есть, элементы могут быть null
|
||||
val c: List<String>? = null // списка может не быть
|
||||
val d: List<String?>? = null // и то, и другое
|
||||
```
|
||||
Для отсеивания null есть готовые функции:
|
||||
```kotlin
|
||||
val items: List<String?> = listOf("a", null, "b")
|
||||
println(items.filterNotNull()) // [a, b]
|
||||
```
|
||||
|
||||
## Nullable и дженерики
|
||||
Параметр типа по умолчанию допускает null, его верхняя граница - `Any?`:
|
||||
```kotlin
|
||||
fun <T> process(item: T) { } // T может быть nullable
|
||||
fun <T : Any> strict(item: T) { } // T гарантированно non-null
|
||||
|
||||
strict("text")
|
||||
// strict(null) // ошибка компиляции
|
||||
```
|
||||
Подробнее: [Параметрический полиморфизм](../../Дженерики/Параметрический%20полиморфизм.md)
|
||||
|
||||
## Где NPE всё-таки возможен
|
||||
Kotlin не даёт абсолютной гарантии. Источники NPE:
|
||||
|
||||
1. **Явный `!!`** - вы сами попросили
|
||||
2. **Явный `throw NullPointerException()`**
|
||||
3. **Код на Java** - типы оттуда компилятор проверить не может ([Платформенные типы](Платформенные%20типы.md))
|
||||
4. **`lateinit` до инициализации** - технически `UninitializedPropertyAccessException`
|
||||
5. **Утечка `this` из конструктора** - обращение к ещё не инициализированному свойству
|
||||
|
||||
```kotlin
|
||||
class Broken {
|
||||
val a: String = compute() // вызывается до инициализации b
|
||||
val b: String = "value"
|
||||
|
||||
fun compute(): String = b.uppercase() // NPE: b ещё null
|
||||
}
|
||||
```
|
||||
Последний случай компилятор обычно замечает и выдаёт предупреждение, но через несколько уровней вызовов - может и пропустить.
|
||||
|
||||
## Правило
|
||||
Если у вас много `!!` - типы спроектированы неправильно. Nullable-тип должен появляться там, где отсутствие значения - реальная часть предметной области («у пользователя может не быть отчества»), а не там, где лень разбираться с инициализацией.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | любая ссылка может быть null | null допускают только типы с `?` |
|
||||
> | NPE в рантайме | ошибка компиляции |
|
||||
> | `Optional<T>` как обёртка | `T?` на уровне типов, без аллокации |
|
||||
> | `@Nullable`/`@NotNull` - подсказки для IDE | часть системы типов, проверяется компилятором |
|
||||
> | `Objects.requireNonNull(x)` | `requireNotNull(x)` или просто non-null тип |
|
||||
>
|
||||
> `Optional<T>` в Java - это объект-обёртка: аллокация, нет поддержки в полях, не композируется с дженериками. `T?` в Kotlin не создаёт обёртки: в байт-коде это обычная ссылка, а проверки расставляет компилятор.
|
||||
>
|
||||
> Публичные функции Kotlin получают проверки параметров автоматически - вызов из Java с null упадёт сразу на входе, с понятным сообщением, а не глубже по стеку:
|
||||
> ```kotlin
|
||||
> fun greet(name: String) = "Hi, $name"
|
||||
> ```
|
||||
> ```java
|
||||
> GreetKt.greet(null);
|
||||
> // NullPointerException: Parameter specified as non-null is null:
|
||||
> // method GreetKt.greet, parameter name
|
||||
> ```
|
||||
> В сообщении указаны и метод, и имя параметра.
|
||||
>
|
||||
> Сравнить: [Приведение ссылочных типов в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Приведение%20ссылочных%20типов.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`!!` - не решение, а отложенная проблема.** Строка `user!!.profile!!.name!!` падает без указания, какое из трёх звеньев было null.
|
||||
>
|
||||
> **Умное приведение не работает с `var` из другого потока и с открытыми свойствами:**
|
||||
> ```kotlin
|
||||
> class Holder {
|
||||
> var value: String? = null
|
||||
> fun use() {
|
||||
> if (value != null) {
|
||||
> // println(value.length) // ошибка: smart cast невозможен
|
||||
> println(value!!.length) // или скопировать в локальную val
|
||||
> }
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
> Подробнее: [Умные приведения](Умные%20приведения.md)
|
||||
>
|
||||
> **Платформенные типы из Java проверок не имеют:**
|
||||
> ```kotlin
|
||||
> val s: String = javaMethodThatReturnsNull() // скомпилируется, упадёт в рантайме
|
||||
> ```
|
||||
>
|
||||
> **`Boolean?` нельзя использовать в `if` напрямую:**
|
||||
> ```kotlin
|
||||
> val flag: Boolean? = null
|
||||
> // if (flag) { } // ошибка компиляции
|
||||
> if (flag == true) { } // так
|
||||
> if (flag ?: false) { } // или так
|
||||
> ```
|
||||
>
|
||||
> **`null` в строковом шаблоне превращается в текст:**
|
||||
> ```kotlin
|
||||
> val x: String? = null
|
||||
> println("значение: $x") // значение: null
|
||||
> ```
|
||||
|
||||
Далее: [Операторы null-безопасности](Операторы%20null-безопасности.md)
|
||||
@@ -0,0 +1,192 @@
|
||||
---
|
||||
title: Операторы null-безопасности
|
||||
description: Безопасный вызов ?., elvis ?:, as?, !! и функции let, requireNotNull
|
||||
tags:
|
||||
- kotlin
|
||||
- null-safety
|
||||
---
|
||||
|
||||
Четыре оператора и несколько функций стандартной библиотеки для работы с nullable-типами.
|
||||
|
||||
## Безопасный вызов - ?.
|
||||
Вызывает член, если значение не null, иначе возвращает null:
|
||||
```kotlin
|
||||
val s: String? = null
|
||||
println(s?.length) // null
|
||||
println(s?.uppercase()) // null
|
||||
|
||||
val t: String? = "text"
|
||||
println(t?.length) // 4
|
||||
```
|
||||
Результат `?.` всегда nullable - даже если сам член возвращает non-null тип.
|
||||
|
||||
### Цепочки
|
||||
```kotlin
|
||||
class Address(val city: String?)
|
||||
class User(val address: Address?)
|
||||
|
||||
val user: User? = null
|
||||
val city: String? = user?.address?.city // null, если оборвалось где угодно
|
||||
```
|
||||
Цепочка прерывается на первом null, остальные вызовы не выполняются. Это «безопасная навигация» - в Java пришлось бы писать три вложенных `if`.
|
||||
|
||||
### С присваиванием
|
||||
```kotlin
|
||||
class Box { var value: Int = 0 }
|
||||
val box: Box? = null
|
||||
box?.value = 42 // ничего не произойдёт, если box == null
|
||||
```
|
||||
|
||||
## Elvis - ?:
|
||||
Возвращает левую часть, если она не null, иначе правую:
|
||||
```kotlin
|
||||
val name: String? = null
|
||||
println(name ?: "анонимный") // анонимный
|
||||
|
||||
val len = name?.length ?: 0 // 0
|
||||
```
|
||||
Назван так из-за схожести `?:` с причёской Элвиса Пресли.
|
||||
|
||||
Правая часть вычисляется **только** при null (ленивость):
|
||||
```kotlin
|
||||
val value = cached ?: expensiveCompute() // вызов только при необходимости
|
||||
```
|
||||
|
||||
### Для раннего выхода
|
||||
Справа может стоять `return` или `throw` - они имеют тип [Nothing](../Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md), поэтому подходят под любой тип:
|
||||
```kotlin
|
||||
fun process(input: String?): Int {
|
||||
val text = input ?: return -1 // выход из функции
|
||||
val n = text.toIntOrNull() ?: error("не число") // исключение
|
||||
return n * 2
|
||||
}
|
||||
```
|
||||
Это идиома: вместо вложенных проверок - плоская последовательность «достали или вышли».
|
||||
|
||||
## Безопасное приведение - as?
|
||||
Возвращает null вместо `ClassCastException`:
|
||||
```kotlin
|
||||
val value: Any = "text"
|
||||
val n: Int? = value as? Int // null
|
||||
val s: String? = value as? String // "text"
|
||||
|
||||
val safe = value as? Int ?: 0 // 0 — частая связка с elvis
|
||||
```
|
||||
Обычный `as` при несовпадении типов падает:
|
||||
```kotlin
|
||||
// val bad = value as Int // ClassCastException
|
||||
```
|
||||
|
||||
## Утверждение non-null - !!
|
||||
Превращает `T?` в `T`, бросая NPE при null:
|
||||
```kotlin
|
||||
val s: String? = "text"
|
||||
println(s!!.length) // 4
|
||||
|
||||
val n: String? = null
|
||||
// println(n!!.length) // NullPointerException
|
||||
```
|
||||
Применять только когда null означает ошибку программиста, и падение предпочтительнее тихого продолжения. В остальных случаях есть `?:`, `?.` или проверка.
|
||||
|
||||
## let для nullable
|
||||
Выполняет блок, только если значение не null:
|
||||
```kotlin
|
||||
val name: String? = "Kotlin"
|
||||
name?.let {
|
||||
println(it.length) // внутри it — уже String, не String?
|
||||
}
|
||||
|
||||
val upper = name?.let { it.uppercase() } ?: "ПУСТО"
|
||||
```
|
||||
Полезно, когда нужно несколько операций и нежелательно повторять `?.`:
|
||||
```kotlin
|
||||
user?.let {
|
||||
log(it.name)
|
||||
send(it.email)
|
||||
it.lastSeen = now()
|
||||
}
|
||||
```
|
||||
Подробнее про `let`, `run`, `also`, `apply`: [Scope-функции](../../Функциональный%20Kotlin/Scope-функции.md)
|
||||
|
||||
## Функции проверки
|
||||
```kotlin
|
||||
val value: String? = getValue()
|
||||
|
||||
val a = requireNotNull(value) // IllegalArgumentException
|
||||
val b = requireNotNull(value) { "нет значения" }
|
||||
val c = checkNotNull(value) // IllegalStateException
|
||||
```
|
||||
Обе возвращают non-null значение, то есть заменяют `!!` с внятным сообщением. Разница - в типе исключения: `require*` для проверки аргументов, `check*` для проверки состояния.
|
||||
Подробнее: [Проверки и контракты](../../Исключения/Проверки%20и%20контракты.md)
|
||||
|
||||
## Полезные nullable-функции stdlib
|
||||
```kotlin
|
||||
val s: String? = null
|
||||
println(s.isNullOrEmpty()) // true — работает на nullable-получателе
|
||||
println(s.isNullOrBlank()) // true
|
||||
println(s.orEmpty()) // "" — строка вместо null
|
||||
|
||||
val list: List<Int>? = null
|
||||
println(list.orEmpty()) // []
|
||||
println(list?.size ?: 0) // 0
|
||||
|
||||
println("42".toIntOrNull()) // 42
|
||||
println("abc".toIntOrNull()) // null — вместо исключения
|
||||
```
|
||||
Функции с суффиксом `OrNull` есть у многих операций: `firstOrNull()`, `singleOrNull()`, `toIntOrNull()`, `getOrNull(i)`.
|
||||
|
||||
## Сводка
|
||||
| Оператор | Результат при null | Когда применять |
|
||||
|---|---|---|
|
||||
| `?.` | `null` | навигация по цепочке |
|
||||
| `?:` | правая часть | значение по умолчанию, ранний выход |
|
||||
| `as?` | `null` | неуверенное приведение типа |
|
||||
| `!!` | NPE | null невозможен по инварианту |
|
||||
| `?.let {}` | блок не выполняется | несколько действий со значением |
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `if (x != null) x.foo()` | `x?.foo()` |
|
||||
> | `x != null ? x : def` | `x ?: def` |
|
||||
> | `x instanceof T ? (T) x : null` | `x as? T` |
|
||||
> | `Objects.requireNonNull(x)` | `requireNotNull(x)` или `x!!` |
|
||||
> | `Optional.ofNullable(x).map(f).orElse(d)` | `x?.let(f) ?: d` |
|
||||
> | три вложенных `if` для цепочки | `a?.b?.c` |
|
||||
>
|
||||
> Java-аналог `Optional` требует оборачивания и разворачивания, работает только как возвращаемое значение и создаёт объект. Операторы Kotlin компилируются в обычные проверки `ifnull` в байт-коде - накладных расходов нет.
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`?.` возвращает nullable, цепочка «заражает» весь результат:**
|
||||
> ```kotlin
|
||||
> val list: List<String>? = listOf("a")
|
||||
> val size: Int? = list?.size // Int?, не Int
|
||||
> val sure: Int = list?.size ?: 0 // нужен elvis
|
||||
> ```
|
||||
>
|
||||
> **Приоритет `?:` ниже арифметики** - без скобок получится не то:
|
||||
> ```kotlin
|
||||
> val a: Int? = null
|
||||
> println((a ?: 0) + 1) // 1
|
||||
> // println(a ?: 0 + 1) // это a ?: (0+1), то есть 1 — здесь совпало,
|
||||
> // // но в сложных выражениях расхождение будет
|
||||
> ```
|
||||
>
|
||||
> **`as?` не спасает от null, только от неверного типа:**
|
||||
> ```kotlin
|
||||
> val x: Any? = null
|
||||
> val s: String? = x as? String // null — потому что x был null
|
||||
> ```
|
||||
>
|
||||
> **`!!` в цепочке скрывает, что именно упало.** `a!!.b!!.c!!` даёт NPE без указания звена. Либо проверяйте по шагам, либо используйте `requireNotNull` с сообщением.
|
||||
>
|
||||
> **`?.let` возвращает null, если получатель null** - а не то, что вернул блок:
|
||||
> ```kotlin
|
||||
> val s: String? = null
|
||||
> val r: Int? = s?.let { 42 } // null, блок не выполнялся
|
||||
> ```
|
||||
>
|
||||
> **Безопасный вызов на функции-расширении с nullable-получателем работает иначе.** `s.isNullOrEmpty()` вызывается без `?.`, потому что расширение объявлено для `String?` и само обрабатывает null внутри.
|
||||
|
||||
Дополнительно: [Scope-функции](../../Функциональный%20Kotlin/Scope-функции.md)
|
||||
Далее: [Умные приведения](Умные%20приведения.md)
|
||||
@@ -0,0 +1,160 @@
|
||||
---
|
||||
title: Платформенные типы
|
||||
description: Тип T! из Java-кода, где null-безопасность Kotlin перестаёт работать и как это чинить
|
||||
tags:
|
||||
- kotlin
|
||||
- null-safety
|
||||
- interop
|
||||
---
|
||||
|
||||
Платформенный тип (platform type) - тип, пришедший из Java, про nullability которого компилятор Kotlin ничего не знает. Обозначается `T!`, но написать его в коде нельзя - он существует только в сообщениях компилятора и в IDE.
|
||||
|
||||
```java
|
||||
// Java
|
||||
public class Repo {
|
||||
public String findName() { return null; }
|
||||
}
|
||||
```
|
||||
```kotlin
|
||||
// Kotlin
|
||||
val repo = Repo()
|
||||
val a: String = repo.findName() // компилируется! падает в рантайме
|
||||
val b: String? = repo.findName() // тоже компилируется
|
||||
```
|
||||
`findName()` имеет тип `String!` - его можно присвоить и в `String`, и в `String?`. Проверку компилятор не делает.
|
||||
|
||||
## Почему так
|
||||
Альтернативы были хуже:
|
||||
- считать все Java-типы nullable → весь Java-код пришлось бы обвешивать `!!` и `?.`, включая `System.out`
|
||||
- считать все non-null → ложное чувство безопасности без возможности это заметить
|
||||
|
||||
Платформенный тип - компромисс: ответственность за nullability возвращается программисту, как в Java.
|
||||
|
||||
## Где падает
|
||||
NPE возникает в точке присваивания, а не использования - компилятор вставляет проверку на границе:
|
||||
```kotlin
|
||||
val a: String = repo.findName() // NPE здесь же
|
||||
println(a.length) // до этой строки не дойдёт
|
||||
```
|
||||
Это лучше, чем в Java: падение происходит рядом с источником, а не там, куда значение утекло.
|
||||
|
||||
## Как чинить
|
||||
### 1. Указывать nullable-тип явно
|
||||
Любое значение из Java трактуйте как `T?`, пока не убедились в обратном:
|
||||
```kotlin
|
||||
val name: String? = repo.findName()
|
||||
println(name?.length ?: 0)
|
||||
```
|
||||
|
||||
### 2. Аннотации в Java-коде
|
||||
Компилятор Kotlin читает аннотации nullability и начинает проверять типы:
|
||||
```java
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class Repo {
|
||||
@Nullable public String findName() { return null; }
|
||||
@NotNull public String getId() { return "1"; }
|
||||
}
|
||||
```
|
||||
```kotlin
|
||||
val a: String = repo.findName() // теперь ошибка компиляции
|
||||
val b: String? = repo.findName() // ok
|
||||
val c: String = repo.getId() // ok, проверка не нужна
|
||||
```
|
||||
|
||||
Поддерживаются аннотации из нескольких наборов:
|
||||
|
||||
| Набор | Пакет |
|
||||
|---|---|
|
||||
| JetBrains | `org.jetbrains.annotations` |
|
||||
| JSpecify | `org.jspecify.annotations` |
|
||||
| Android | `androidx.annotation` |
|
||||
| JSR-305 | `javax.annotation` |
|
||||
| Eclipse | `org.eclipse.jdt.annotation` |
|
||||
| Lombok | `lombok.NonNull` |
|
||||
|
||||
### 3. Обёртка на границе
|
||||
Для внешней библиотеки без аннотаций - свой слой, который один раз разбирается с nullability:
|
||||
```kotlin
|
||||
// вся работа с «грязными» типами собрана здесь
|
||||
fun Repo.nameOrNull(): String? = findName()
|
||||
fun Repo.requireName(): String = findName() ?: error("name отсутствует")
|
||||
```
|
||||
Дальше код работает с честными типами Kotlin.
|
||||
|
||||
### 4. Строгий режим компилятора
|
||||
Флаг `-Xjsr305=strict` заставляет компилятор относиться к JSR-305-аннотациям как к обязательным:
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-Xjsr305=strict")
|
||||
}
|
||||
}
|
||||
```
|
||||
Значения: `ignore`, `warn` (по умолчанию), `strict`.
|
||||
|
||||
## Коллекции из Java
|
||||
Платформенность распространяется на параметры типа:
|
||||
```java
|
||||
public List<String> getItems() { return null; }
|
||||
```
|
||||
Тип результата - `(Mutable)List<String!>!`. Неизвестно ни то, может ли быть null сам список, ни его элементы:
|
||||
```kotlin
|
||||
val items: List<String?>? = repo.items // самое честное предположение
|
||||
items?.filterNotNull()?.forEach(::println)
|
||||
```
|
||||
|
||||
Отдельная проблема - изменяемость. `java.util.List` отображается в `(Mutable)List`, то есть Kotlin позволит передать его туда, где ожидается `MutableList`, и Java-код может изменить «неизменяемый» список:
|
||||
```kotlin
|
||||
val list: List<String> = repo.items // на самом деле ArrayList
|
||||
// list.add("x") // так нельзя, тип List
|
||||
(list as MutableList).add("x") // а так — да, и это сработает
|
||||
```
|
||||
Подробнее: [Java из Kotlin](../../Взаимодействие%20с%20Java/Java%20из%20Kotlin.md)
|
||||
|
||||
## Обратное направление
|
||||
Код на Kotlin для Java-вызывающего выглядит аннотированным: компилятор добавляет `@NotNull`/`@Nullable` в байт-код автоматически. Плюс в публичные функции вставляются проверки параметров:
|
||||
```kotlin
|
||||
fun greet(name: String) = "Hi, $name"
|
||||
```
|
||||
```java
|
||||
GreetKt.greet(null);
|
||||
// NullPointerException: Parameter specified as non-null is null:
|
||||
// method GreetKt.greet, parameter name
|
||||
```
|
||||
Падение происходит на входе в функцию, с указанием метода и имени параметра.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> В Java платформенных типов нет - там все ссылки одинаково могут быть null, и никакой проверки не существует.
|
||||
>
|
||||
> Платформенные типы - это механизм совместимости, а не фича языка. Их задача - позволить Kotlin вызывать существующие библиотеки без переписывания. Чем больше кода в проекте на Kotlin, тем реже они встречаются.
|
||||
>
|
||||
> Сравнить: [Система исключений в Java](../../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Самый опасный случай - значение «просачивается» в non-null тип:**
|
||||
> ```kotlin
|
||||
> data class User(val name: String)
|
||||
> val user = User(repo.findName()) // NPE в конструкторе, а причина — в Java-методе
|
||||
> ```
|
||||
>
|
||||
> **IDE подсказывает тип `String!`, но в коде так написать нельзя.** Если увидели `!` в подсказке - значит, компилятор вас не защищает.
|
||||
>
|
||||
> **Аннотации на уровне пакета легко пропустить.** `@NonNullApi` или `package-info.java` с `@ParametersAreNonnullByDefault` меняют поведение для всего пакета. Spring, например, аннотирует свои пакеты - поэтому его API в Kotlin выглядит типизированным.
|
||||
>
|
||||
> **`-Xjsr305=strict` может сломать сборку при обновлении библиотеки** - новые аннотации превратятся в ошибки компиляции. Включать осознанно.
|
||||
>
|
||||
> **Дженерик-методы Java особенно коварны:**
|
||||
> ```java
|
||||
> public <T> T getOrNull(String key) { return null; }
|
||||
> ```
|
||||
> ```kotlin
|
||||
> val x: String = repo.getOrNull("k") // T выведется в String, NPE в рантайме
|
||||
> ```
|
||||
>
|
||||
> **Массивы из Java тоже платформенные:** `String[]` → `Array<(out) String!>!`. Элементы могут быть null даже если сам массив не null.
|
||||
|
||||
Дополнительно: [Java из Kotlin](../../Взаимодействие%20с%20Java/Java%20из%20Kotlin.md)
|
||||
Далее: [Строки](../Работа%20с%20данными/Строки.md)
|
||||
@@ -0,0 +1,241 @@
|
||||
---
|
||||
title: Умные приведения
|
||||
description: Smart cast после is и проверки на null, условия работы и случаи, когда компилятор отказывается
|
||||
tags:
|
||||
- kotlin
|
||||
- null-safety
|
||||
- types
|
||||
---
|
||||
|
||||
Умное приведение (smart cast) - автоматическое приведение типа после проверки. Компилятор отслеживает поток управления и знает, что в этой ветке тип уже уточнён.
|
||||
|
||||
```kotlin
|
||||
fun describe(value: Any): String {
|
||||
if (value is String) {
|
||||
return "строка длины ${value.length}" // value уже String
|
||||
}
|
||||
return "не строка"
|
||||
}
|
||||
```
|
||||
Явного `(String) value` писать не нужно - в отличие от Java (до версии 16).
|
||||
|
||||
## После проверки типа - is
|
||||
```kotlin
|
||||
fun size(x: Any): Int = when (x) {
|
||||
is String -> x.length // String
|
||||
is List<*> -> x.size // List<*>
|
||||
is Map<*, *> -> x.size // Map<*, *>
|
||||
else -> 0
|
||||
}
|
||||
```
|
||||
`!is` работает так же, с инверсией:
|
||||
```kotlin
|
||||
fun check(x: Any) {
|
||||
if (x !is String) return
|
||||
println(x.length) // после return здесь только String
|
||||
}
|
||||
```
|
||||
|
||||
## После проверки на null
|
||||
```kotlin
|
||||
fun length(s: String?): Int {
|
||||
if (s == null) return 0
|
||||
return s.length // s: String
|
||||
}
|
||||
|
||||
fun length2(s: String?): Int {
|
||||
if (s != null) {
|
||||
return s.length // s: String
|
||||
}
|
||||
return 0
|
||||
}
|
||||
```
|
||||
|
||||
## В логических выражениях
|
||||
Приведение работает внутри `&&` и `||`, так как они ленивые:
|
||||
```kotlin
|
||||
fun check(x: Any?): Boolean {
|
||||
return x is String && x.length > 3 // x приведён во второй части
|
||||
}
|
||||
|
||||
fun check2(x: Any?): Boolean {
|
||||
return x !is String || x.length > 3 // приведён в правой части ||
|
||||
}
|
||||
```
|
||||
|
||||
## В elvis и безопасных вызовах
|
||||
```kotlin
|
||||
fun process(s: String?) {
|
||||
val text = s ?: return // после этой строки s... нет, но text: String
|
||||
println(text.length)
|
||||
}
|
||||
```
|
||||
|
||||
## Где приведение НЕ работает
|
||||
Главное условие: компилятор должен гарантировать, что **значение не изменится между проверкой и использованием**.
|
||||
|
||||
### Изменяемое свойство класса - var
|
||||
```kotlin
|
||||
class Holder {
|
||||
var value: String? = null
|
||||
|
||||
fun broken() {
|
||||
if (value != null) {
|
||||
// println(value.length) // ошибка: smart cast to 'String' is impossible
|
||||
}
|
||||
}
|
||||
|
||||
fun fixed() {
|
||||
val local = value // копия в локальную val
|
||||
if (local != null) {
|
||||
println(local.length) // работает
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Почему: между проверкой и обращением другой поток (или вызванная функция) может присвоить `value = null`.
|
||||
|
||||
### Свойство с кастомным геттером
|
||||
```kotlin
|
||||
class Random2 {
|
||||
val value: String?
|
||||
get() = if (Math.random() > 0.5) "x" else null
|
||||
|
||||
fun broken() {
|
||||
if (value != null) {
|
||||
// println(value.length) // ошибка: геттер вернёт разное
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### open-свойство
|
||||
```kotlin
|
||||
open class Base {
|
||||
open val value: String? = "x"
|
||||
}
|
||||
|
||||
fun check(b: Base) {
|
||||
if (b.value != null) {
|
||||
// println(b.value.length) // ошибка: наследник мог переопределить геттер
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Свойство из другого модуля
|
||||
Даже `val` из другой единицы компиляции не приводится: компилятор не может доказать, что у него нет кастомного геттера.
|
||||
|
||||
### Локальная var, захваченная лямбдой
|
||||
```kotlin
|
||||
fun broken() {
|
||||
var s: String? = "text"
|
||||
val modify = { s = null }
|
||||
|
||||
if (s != null) {
|
||||
// println(s.length) // ошибка: лямбда может изменить s
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Сводка условий
|
||||
| Что проверяем | Умное приведение |
|
||||
|---|---|
|
||||
| локальная `val` | ✅ работает |
|
||||
| локальная `var` без захвата лямбдой | ✅ работает |
|
||||
| локальная `var`, захваченная лямбдой | ❌ |
|
||||
| `val`-свойство в том же модуле, без геттера | ✅ работает |
|
||||
| `var`-свойство | ❌ |
|
||||
| `open val` | ❌ |
|
||||
| `val` с кастомным геттером | ❌ |
|
||||
| `val` из другого модуля | ❌ |
|
||||
| параметр функции (всегда `val`) | ✅ работает |
|
||||
|
||||
Универсальное решение во всех ❌-случаях: скопировать значение в локальную `val`.
|
||||
|
||||
## Явное приведение - as
|
||||
Когда умное не применимо:
|
||||
```kotlin
|
||||
val x: Any = "text"
|
||||
val s = x as String // ClassCastException при несовпадении
|
||||
val n = x as? Int // null при несовпадении
|
||||
```
|
||||
Подробнее: [Приведение типов](../Работа%20с%20данными/Приведение%20типов.md)
|
||||
|
||||
## Умное приведение в Kotlin 2.x
|
||||
Компилятор K2 (по умолчанию с Kotlin 2.0) расширил анализ. Теперь работает то, что раньше требовало ручного приведения:
|
||||
```kotlin
|
||||
fun test(x: Any?) {
|
||||
val isString = x is String
|
||||
if (isString) {
|
||||
println(x.length) // K2 понимает связь через переменную-флаг
|
||||
}
|
||||
}
|
||||
|
||||
fun orElse(x: Any?) {
|
||||
if (x !is String) return
|
||||
println(x.length) // работало и раньше
|
||||
}
|
||||
```
|
||||
Подробнее: [Компилятор K2](../../Инструментарий/Компилятор%20K2.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `if (x instanceof String) { String s = (String) x; ... }` | `if (x is String) { x.length }` |
|
||||
> | pattern matching `x instanceof String s` (Java 16+) | есть с первой версии, без объявления переменной |
|
||||
> | приведение только по типу | ещё и по null |
|
||||
> | компилятор не отслеживает изменяемость | отказывается приводить, если значение может измениться |
|
||||
>
|
||||
> Java 16 добавила pattern matching: `if (x instanceof String s)`. Это ближе к Kotlin, но Java всё равно требует явно назвать переменную, и на nullability это не распространяется.
|
||||
>
|
||||
> Сравнить: [Приведение ссылочных типов в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Приведение%20ссылочных%20типов.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Сообщение компилятора объясняет причину** - его стоит читать целиком:
|
||||
> ```
|
||||
> smart cast to 'String' is impossible, because 'value' is a mutable property
|
||||
> that could be mutated concurrently
|
||||
>
|
||||
> smart cast to 'String' is impossible, because 'value' is a property
|
||||
> that has an open or custom getter
|
||||
>
|
||||
> smart cast to 'String' is impossible, because 's' is a local variable
|
||||
> that is mutated in a capturing closure
|
||||
> ```
|
||||
> Там прямо сказано и что мешает, и почему.
|
||||
>
|
||||
> **`!!` вместо локальной `val` - плохой обмен.** Оба компилируются, но второй вариант предсказуем:
|
||||
> ```kotlin
|
||||
> // println(value!!.length) // падение при гонке
|
||||
> val v = value ?: return // гонки нет
|
||||
> ```
|
||||
>
|
||||
> **Приведение дженериков неполное из-за стирания типов:**
|
||||
> ```kotlin
|
||||
> fun check(x: Any) {
|
||||
> if (x is List<*>) {
|
||||
> // элементы остаются Any?, тип параметра неизвестен
|
||||
> val first = x.firstOrNull() // Any?
|
||||
> }
|
||||
> // if (x is List<String>) // ошибка: cannot check for instance of erased type
|
||||
> }
|
||||
> ```
|
||||
> Обход - [reified](../../Дженерики/Reified%20и%20inline.md).
|
||||
>
|
||||
> **Приведение теряется после вызова функции, меняющей состояние** - если это свойство, а не локальная переменная:
|
||||
> ```kotlin
|
||||
> class C {
|
||||
> var s: String? = "x"
|
||||
> fun reset() { s = null }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **В `when` без аргумента приведение работает только для проверенной переменной:**
|
||||
> ```kotlin
|
||||
> fun combine(a: Any?, b: Any?): Int = when {
|
||||
> a is String && b is Int -> a.length + b // оба приведены
|
||||
> else -> 0
|
||||
> }
|
||||
> ```
|
||||
|
||||
Далее: [Платформенные типы](Платформенные%20типы.md)
|
||||
Reference in New Issue
Block a user