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

195 lines
9.1 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: Безопасный вызов ?., 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` в байт-коде - накладных расходов нет.
>
> Сравнить: [Приведение ссылочных типов в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Приведение%20ссылочных%20типов.md)
> [!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)