Files
obsidian/Kotlin/Синтаксис и типы/Null-безопасность/Null-безопасность.md
T

186 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)