186 lines
9.0 KiB
Markdown
186 lines
9.0 KiB
Markdown
---
|
||
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)
|