195 lines
9.1 KiB
Markdown
195 lines
9.1 KiB
Markdown
---
|
||
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)
|