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)
|
||||
Reference in New Issue
Block a user