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

9.1 KiB
Raw Blame History

title, description, tags
title description tags
Операторы null-безопасности Безопасный вызов ?., elvis ?:, as?, !! и функции let, requireNotNull
kotlin
null-safety

Четыре оператора и несколько функций стандартной библиотеки для работы с nullable-типами.

Безопасный вызов - ?.

Вызывает член, если значение не null, иначе возвращает null:

val s: String? = null
println(s?.length)      // null
println(s?.uppercase()) // null

val t: String? = "text"
println(t?.length)      // 4

Результат ?. всегда nullable - даже если сам член возвращает non-null тип.

Цепочки

class Address(val city: String?)
class User(val address: Address?)

val user: User? = null
val city: String? = user?.address?.city   // null, если оборвалось где угодно

Цепочка прерывается на первом null, остальные вызовы не выполняются. Это «безопасная навигация» - в Java пришлось бы писать три вложенных if.

С присваиванием

class Box { var value: Int = 0 }
val box: Box? = null
box?.value = 42      // ничего не произойдёт, если box == null

Elvis - ?:

Возвращает левую часть, если она не null, иначе правую:

val name: String? = null
println(name ?: "анонимный")      // анонимный

val len = name?.length ?: 0       // 0

Назван так из-за схожести ?: с причёской Элвиса Пресли.

Правая часть вычисляется только при null (ленивость):

val value = cached ?: expensiveCompute()   // вызов только при необходимости

Для раннего выхода

Справа может стоять return или throw - они имеют тип Nothing, поэтому подходят под любой тип:

fun process(input: String?): Int {
    val text = input ?: return -1                 // выход из функции
    val n = text.toIntOrNull() ?: error("не число") // исключение
    return n * 2
}

Это идиома: вместо вложенных проверок - плоская последовательность «достали или вышли».

Безопасное приведение - as?

Возвращает null вместо ClassCastException:

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 при несовпадении типов падает:

// val bad = value as Int   // ClassCastException

Утверждение non-null - !!

Превращает T? в T, бросая NPE при null:

val s: String? = "text"
println(s!!.length)       // 4

val n: String? = null
// println(n!!.length)    // NullPointerException

Применять только когда null означает ошибку программиста, и падение предпочтительнее тихого продолжения. В остальных случаях есть ?:, ?. или проверка.

let для nullable

Выполняет блок, только если значение не null:

val name: String? = "Kotlin"
name?.let {
    println(it.length)    // внутри it — уже String, не String?
}

val upper = name?.let { it.uppercase() } ?: "ПУСТО"

Полезно, когда нужно несколько операций и нежелательно повторять ?.:

user?.let {
    log(it.name)
    send(it.email)
    it.lastSeen = now()
}

Подробнее про let, run, also, apply: Scope-функции

Функции проверки

val value: String? = getValue()

val a = requireNotNull(value)              // IllegalArgumentException
val b = requireNotNull(value) { "нет значения" }
val c = checkNotNull(value)                // IllegalStateException

Обе возвращают non-null значение, то есть заменяют !! с внятным сообщением. Разница - в типе исключения: require* для проверки аргументов, check* для проверки состояния. Подробнее: Проверки и контракты

Полезные nullable-функции stdlib

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

[!warning] Подводные камни ?. возвращает nullable, цепочка «заражает» весь результат:

val list: List<String>? = listOf("a")
val size: Int? = list?.size      // Int?, не Int
val sure: Int = list?.size ?: 0  // нужен elvis

Приоритет ?: ниже арифметики - без скобок получится не то:

val a: Int? = null
println((a ?: 0) + 1)   // 1
// println(a ?: 0 + 1)  // это a ?: (0+1), то есть 1 — здесь совпало,
//                      // но в сложных выражениях расхождение будет

as? не спасает от null, только от неверного типа:

val x: Any? = null
val s: String? = x as? String    // null — потому что x был null

!! в цепочке скрывает, что именно упало. a!!.b!!.c!! даёт NPE без указания звена. Либо проверяйте по шагам, либо используйте requireNotNull с сообщением.

?.let возвращает null, если получатель null - а не то, что вернул блок:

val s: String? = null
val r: Int? = s?.let { 42 }   // null, блок не выполнялся

Безопасный вызов на функции-расширении с nullable-получателем работает иначе. s.isNullOrEmpty() вызывается без ?., потому что расширение объявлено для String? и само обрабатывает null внутри.

Дополнительно: Scope-функции Далее: Умные приведения