Урок 7. Null-безопасность — ?, ?., ?: и !!
null — особое значение «ничего нет»: имя не указано, строка не прочитана, запись не найдена. Во многих языках любая переменная может оказаться null, и если программа обратится к ней как к настоящему значению, она упадёт с ошибкой NullPointerException. Это одна из самых частых ошибок в программах на Java.
Kotlin решает задачу так: компилятор знает, где может быть null, а где нет, и не даёт собрать программу, в которой пустое значение используется без проверки.
Тип с вопросом
Обычный тип — String, Int — null хранить не может:
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»:
fun main() {
var name: String? = "Anna"
println(name)
name = null
println(name)
}Anna
nullНо у такой переменной нельзя просто взять свойство или вызвать функцию: name.length не соберётся. Компилятор сообщит, что для типа String? разрешены только безопасные вызовы ?. или вызовы с !!.. Разбор этой ошибки — в справке Ошибка Only safe (?.) or non-null asserted (!!.) calls are allowed. Ниже — все способы с ней справиться.
Проверка if
Самый понятный способ — проверить на null:
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).
Безопасный вызов ?.
?. вызывает функцию или берёт свойство, только если значение не null. Если null — весь вызов даёт null, и ошибки нет:
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:
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 — тогда функция закончится, если значения нет:
fun greet(name: String?) {
val n = name ?: return
println("Hello, $n!")
}
fun main() {
greet("Oleg")
greet(null)
}Hello, Oleg!Ввод, который может оказаться не числом
toIntOrNull() переводит строку в число, а если в строке не число — возвращает null вместо ошибки. Вместе с ?: или проверкой это надёжный способ читать числа:
fun main() {
val n = readln().toIntOrNull()
if (n == null) {
println("Not a number")
} else {
println(n * 2)
}
}abcNot a numberlet
?.let { ... } выполняет блок, только если значение не null. Внутри блока значение доступно как it, уже без вопроса:
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, программа падает:
fun main() {
val s: String? = null
println(s!!.length)
}Exception in thread "main" java.lang.NullPointerExceptionПод этой строкой идут строки at … — где именно случилась ошибка: имя файла и номер строки.
!! возвращает в программу ту самую ошибку, от которой защищает Kotlin. Ставьте его только там, где null действительно невозможен, а компилятор этого не видит. В остальных случаях — ?., ?: или проверка if. Подробнее — в справке NullPointerException в Kotlin.
Где встречается null
toIntOrNull(),toDoubleOrNull()— строка не число;map[key]— в словаре нет такого ключа (урок Коллекции);list.firstOrNull(),list.getOrNull(i)— список пуст или номера нет;readlnOrNull()— ввод закончился;- функции из библиотек Java — они могут вернуть
null, и компилятор Kotlin об этом не всегда знает.
Коротко
Stringне бываетnull,String?— бывает.- Проверка
if (s != null)разрешает работать сsкак с обычным значением. s?.length— безопасный вызов, даётnull, еслиs—null.s?.length ?: 0— значение по умолчанию вместоnull.s!!— «здесь точно не null»; если всё жеnull—NullPointerException.toIntOrNull()— перевод строки в число без падения.