Глиф Учебники

Урок 7. Null-безопасность — ?, ?., ?: и !!

Курс «Kotlin» · урок 7 из 10 · 3 минуты чтения

null — особое значение «ничего нет»: имя не указано, строка не прочитана, запись не найдена. Во многих языках любая переменная может оказаться null, и если программа обратится к ней как к настоящему значению, она упадёт с ошибкой NullPointerException. Это одна из самых частых ошибок в программах на Java.

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

Тип с вопросом

Обычный тип — String, Int — null хранить не может:

Kotlin
fun main() {
    var name: String = "Anna"
    name = null
}

Эта программа не соберётся: компилятор сообщит, что null не может быть значением типа String (null cannot be a value of a non-null type 'String').

Если null допустим, к типу добавляют знак вопроса: String? — «строка или null», Int? — «число или null»:

Kotlin
fun main() {
    var name: String? = "Anna"
    println(name)
    name = null
    println(name)
}
Вывод
Anna
null
String — только строка "Anna" name = null не соберётся String? — строка или null "Anna" или null name = null можно
В переменную типа String можно положить только строку — name = null не соберётся; тип String? допускает и строку, и null.

Но у такой переменной нельзя просто взять свойство или вызвать функцию: name.length не соберётся. Компилятор сообщит, что для типа String? разрешены только безопасные вызовы ?. или вызовы с !!.. Разбор этой ошибки — в справке Ошибка Only safe (?.) or non-null asserted (!!.) calls are allowed. Ниже — все способы с ней справиться.

Проверка if

Самый понятный способ — проверить на null:

Kotlin
fun printLength(s: String?) {
    if (s != null) {
        println(s.length)
    } else {
        println("no string")
    }
}

fun main() {
    printLength("hello")
    printLength(null)
}
Вывод
5
no string

Внутри if (s != null) компилятор уже знает, что s — не null, и разрешает s.length без всяких знаков. Это называется умным приведением (smart cast).

s: String? может быть null if (s != null) проверка да нет s: String — уже без ? println(s.length) "hello" → 5 println("no string") s — null, длины нет
Внутри ветки if (s != null) переменная s уже имеет тип String без вопроса, и s.length разрешён; в ветке else s — null.

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

?. вызывает функцию или берёт свойство, только если значение не null. Если null — весь вызов даёт null, и ошибки нет:

Kotlin
fun main() {
    val a: String? = "Kotlin"
    val b: String? = null
    println(a?.length)
    println(b?.length)
    println(b?.uppercase())
}
Вывод
6
null
null

Результат безопасного вызова — снова тип с вопросом: a?.length имеет тип Int?.

Значение по умолчанию ?:

Оператор ?: (его называют «Элвис» — из-за сходства с причёской) даёт значение справа, если слева null:

Kotlin
fun main() {
    val b: String? = null
    val len = b?.length ?: 0
    println(len)
    val name: String? = null
    println("Hello, ${name ?: "guest"}!")
}
Вывод
0
Hello, guest!

b?.length ?: 0 читается так: «длина b, а если b — null, то 0». Результат уже без вопроса: len имеет тип Int.

Справа от ?: может стоять и return — тогда функция закончится, если значения нет:

Kotlin
fun greet(name: String?) {
    val n = name ?: return
    println("Hello, $n!")
}

fun main() {
    greet("Oleg")
    greet(null)
}
Вывод
Hello, Oleg!

Ввод, который может оказаться не числом

toIntOrNull() переводит строку в число, а если в строке не число — возвращает null вместо ошибки. Вместе с ?: или проверкой это надёжный способ читать числа:

Kotlin
fun main() {
    val n = readln().toIntOrNull()
    if (n == null) {
        println("Not a number")
    } else {
        println(n * 2)
    }
}
Ввод
abc
Вывод
Not a number

let

?.let { ... } выполняет блок, только если значение не null. Внутри блока значение доступно как it, уже без вопроса:

Kotlin
fun main() {
    val email: String? = "user@example.com"
    email?.let {
        println("Send to $it")
        println(it.length)
    }
    val empty: String? = null
    empty?.let { println("never printed") }
}
Вывод
Send to user@example.com
16

Оператор !!

!! говорит компилятору: «здесь точно не null, поверь». Тип теряет вопрос, и s!!.length собирается. Но если значение всё-таки null, программа падает:

Kotlin
fun main() {
    val s: String? = null
    println(s!!.length)
}
Текст
Exception in thread "main" java.lang.NullPointerException

Под этой строкой идут строки at … — где именно случилась ошибка: имя файла и номер строки.

!! возвращает в программу ту самую ошибку, от которой защищает Kotlin. Ставьте его только там, где null действительно невозможен, а компилятор этого не видит. В остальных случаях — ?., ?: или проверка if. Подробнее — в справке NullPointerException в Kotlin.

выражение s = "Kotlin" s = null s?.length безопасный вызов 6 null s?.length ?: 0 значение по умолчанию 6 0 s!!.length «точно не null» 6 NullPointerException программа падает Ошибки нет в первых двух строках: вместо падения — null или запасное значение.
Что дают s?.length, s?.length ?: 0 и s!!.length для строки "Kotlin" и для null: безопасный вызов даёт null, Элвис — запасной 0, а !! роняет программу.

Где встречается null

Коротко

Проверьте себя

1. Что выведет программа?
Kotlin
fun main() {
    val s: String? = null
    println(s?.length ?: -1)
}
2. Можно ли положить null в переменную типа Int?
3. Что сделает s!!.length, если s равно null?
4. Что вернёт "12a".toIntOrNull()?