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

9.0 KiB
Raw Blame History

title, description, tags
title description tags
Null-безопасность Разделение типов на nullable и non-null, как компилятор предотвращает NPE
kotlin
null-safety

Главная особенность системы типов Kotlin: возможность быть null - часть типа.

var a: String = "text"
// a = null            // ошибка компиляции

var b: String? = "text"
b = null               // ok

String и String? - разные типы. String является подтипом String?, но не наоборот.

graph TD
    SN["String? — допускает null"]
    S["String — не допускает null"]
    N["Nothing? — только null"]
    SN --> S
    SN --> N

Зачем это нужно

NullPointerException в Java возникает в рантайме, иногда далеко от места, где null попал в переменную. В 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

После проверки компилятор сам приводит тип:

fun describe(s: String?): String {
    if (s == null) return "нет значения"
    return "длина ${s.length}"      // здесь s уже String, не String?
}

Это умное приведение.

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

val len: Int? = s?.length    // null, если s == null

3. Elvis - ?:

val len: Int = s?.length ?: 0           // значение по умолчанию
val name = user?.name ?: error("нет пользователя")  // или выход

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

val n: Int? = value as? Int   // null вместо ClassCastException

5. Утверждение - !!

val len: Int = s!!.length     // NPE, если s == null

!! означает «я гарантирую, что здесь не null». Каждое его использование - это место, где вы отключили проверку компилятора.

Подробнее про операторы: Операторы null-безопасности

Nullable в коллекциях

? ставится раздельно для элементов и для самой коллекции:

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 есть готовые функции:

val items: List<String?> = listOf("a", null, "b")
println(items.filterNotNull())   // [a, b]

Nullable и дженерики

Параметр типа по умолчанию допускает null, его верхняя граница - Any?:

fun <T> process(item: T) { }          // T может быть nullable
fun <T : Any> strict(item: T) { }     // T гарантированно non-null

strict("text")
// strict(null)                        // ошибка компиляции

Подробнее: Параметрический полиморфизм

Где NPE всё-таки возможен

Kotlin не даёт абсолютной гарантии. Источники NPE:

  1. Явный !! - вы сами попросили
  2. Явный throw NullPointerException()
  3. Код на Java - типы оттуда компилятор проверить не может (Платформенные типы)
  4. lateinit до инициализации - технически UninitializedPropertyAccessException
  5. Утечка this из конструктора - обращение к ещё не инициализированному свойству
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 упадёт сразу на входе, с понятным сообщением, а не глубже по стеку:

fun greet(name: String) = "Hi, $name"
GreetKt.greet(null);
// NullPointerException: Parameter specified as non-null is null:
// method GreetKt.greet, parameter name

В сообщении указаны и метод, и имя параметра.

Сравнить: Приведение ссылочных типов в Java

[!warning] Подводные камни !! - не решение, а отложенная проблема. Строка user!!.profile!!.name!! падает без указания, какое из трёх звеньев было null.

Умное приведение не работает с var из другого потока и с открытыми свойствами:

class Holder {
    var value: String? = null
    fun use() {
        if (value != null) {
            // println(value.length)   // ошибка: smart cast невозможен
            println(value!!.length)     // или скопировать в локальную val
        }
    }
}

Подробнее: Умные приведения

Платформенные типы из Java проверок не имеют:

val s: String = javaMethodThatReturnsNull()   // скомпилируется, упадёт в рантайме

Boolean? нельзя использовать в if напрямую:

val flag: Boolean? = null
// if (flag) { }           // ошибка компиляции
if (flag == true) { }      // так
if (flag ?: false) { }     // или так

null в строковом шаблоне превращается в текст:

val x: String? = null
println("значение: $x")    // значение: null

Далее: Операторы null-безопасности