Безопасность при работе с null
Типы с возможностью null и типы без null
Система типов Kotlin направлена на устранение опасности ссылок на null, также известной как Ошибка миллиарда долларов.
Одна из самых распространённых проблем во многих языках программирования, включая Java, заключается в том, что обращение к члену ссылки на null приводит к исключению NullPointerException. В Java это эквивалентно NullPointerException, или NPE вкратце.
Единственными возможными причинами возникновения NPE в Kotlin являются:
Явное обращение к
throw NullPointerException().Использование оператора
!!, описанного ниже.-
Несоответствие данных в отношении инициализации, например, когда:
Неинициализированный
thisдоступный в конструкторе передаётся и используется где-либо («утечкаthis»).Конструктор суперкласса вызывает открытый член, реализация которого в производном классе использует неинициализированное состояние.
-
Взаимодействие с Java:
Попытки получить доступ к члену ссылки на
nullтипа платформы;Проблемы с применимостью к типам с параметрами, используемым для взаимодействия с Java. Например, код Java может добавить
nullв KotlinMutableList<String>, поэтому для работы с ним требуетсяMutableList<String?>.Другие проблемы, вызванные внешним кодом Java.
В Kotlin система типов различает ссылки, которые могут содержать null (ссылки с возможностью null) и те, которые не могут (ссылки без null). Например, обычная переменная типа String не может содержать null:
fun main() {
//sampleStart
var a: String = "abc" // Regular initialization means non-null by default
a = null // compilation error
//sampleEnd
}
Чтобы разрешить null, вы можете объявить переменную как nullable строку, написав String?:
fun main() {
//sampleStart
var b: String? = "abc" // can be set to null
b = null // ok
print(b)
//sampleEnd
}
Теперь, если вы вызываете метод или получаете доступ к свойству по a, гарантируется, что это не вызовет NPE, поэтому вы можете безопасно сказать:
val l = a.length
Но если вы хотите получить доступ к тому же свойству по b, это будет небезопасно, и компилятор сообщит об ошибке:
val l = b.length // error: variable 'b' can be null
Но вам всё равно нужно получить доступ к этому свойству, верно? Есть несколько способов сделать это.
Проверка на null в условиях
Во-первых, вы можете явно проверить, является ли b null, и обработать два варианта отдельно:
val l = if (b != null) b.length else -1
Компилятор отслеживает информацию о проведённой проверке и разрешает вызов length внутри if. Поддерживаются и более сложные условия:
fun main() {
//sampleStart
val b: String? = "Kotlin"
if (b != null && b.length > 0) {
print("String of length ${b.length}")
} else {
print("Empty string")
}
//sampleEnd
}
Обратите внимание, что это работает только в том случае, когда b является неизменяемым (т.е. это локальная переменная, которая не изменяется между проверкой и её использованием, или это член val с поддерживающим полем и не переопределяемым), поскольку в противном случае может произойти изменение b на null после проверки.
Безопасные вызовы
Второй вариант доступа к свойству в nullable переменной — использование оператора безопасного вызова ?.:
fun main() {
//sampleStart
val a = "Kotlin"
val b: String? = null
println(b?.length)
println(a?.length) // Unnecessary safe call
//sampleEnd
}
Он возвращает b.length если b не null, и null в противном случае. Тип этого выражения — Int?.
Безопасные вызовы полезны в цепочках. Например, у сотрудника может быть отдел (или нет). В свою очередь, этот отдел может иметь руководителя. Чтобы получить имя руководителя отдела сотрудника, вы напишете следующее:
bob?.department?.head?.name
Такая цепочка возвращает null если какое-либо из свойств в ней имеет значение null.
Чтобы выполнить определённое действие только для значений, отличных от null, вы можете использовать оператор безопасного вызова вместе с let:
fun main() {
//sampleStart
val listWithNulls: List<String?> = listOf("Kotlin", null)
for (item in listWithNulls) {
item?.let { println(it) } // prints Kotlin and ignores null
}
//sampleEnd
}
Безопасный вызов также может быть размещён слева от присваивания. Затем, если какой-либо из получателей в цепочке безопасных вызовов является null, присваивание пропускается, и выражение справа вообще не оценивается:
// If either `person` or `person.department` is null, the function is not called: person?.department?.head = managersPool.getManager()
Nullable получатель
Функции расширения могут быть определены для nullable получателя. Таким образом, вы можете указать поведение для значений null без необходимости использования логики проверки null в каждом месте вызова.
Например, функция toString() определена для nullable получателя. Она возвращает строку "null" (в отличие от значения null). Это может быть полезно в определённых ситуациях, например, в ведении логов:
val person: Person? = null logger.debug(person.toString()) // Logs "null", does not throw an exception
Если вы хотите, чтобы вызов toString() возвращал nullable строку, используйте оператор безопасного вызова ?.:
var timestamp: Instant? = null
val isoTimestamp = timestamp?.toString() // Returns a String? object which is `null`
if (isoTimestamp == null) {
// Handle the case where timestamp was `null`
}
Оператор Elvis
Когда у вас есть nullable ссылка, b, вы можете сказать: «если b не null, используйте его, в противном случае используйте какое-либо значение, отличное от null»:
val l: Int = if (b != null) b.length else -1
Вместо написания полного выражения if, вы также можете выразить это с помощью оператора Elvis ?::
val l = b?.length ?: -1
Если выражение слева от ?: не null, оператор Elvis возвращает его, в противном случае он возвращает выражение справа. Обратите внимание, что выражение справа оценивается только в том случае, если выражение слева является null.
Поскольку throw и return являются выражениями в Kotlin, они также могут быть использованы справа от оператора Elvis. Это может быть полезно, например, при проверке аргументов функции:
fun foo(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
// ...
}
Оператор !!
Третий вариант для любителей NPE: оператор неявного утверждения не null (!!) преобразует любое значение в тип, не допускающий null, и выбрасывает исключение, если значение является null. Вы можете написать b!!, и это вернёт значение, не допускающее null, типа b (например, String в нашем примере) или выбросит NPE, если b является null:
val l = b!!.length
Таким образом, если вы хотите NPE, вы можете его получить, но вам нужно его явно запросить, и оно не появится само по себе.
Безопасные преобразования типов
Обычные преобразования типов могут привести к ClassCastException если объект не является объектом целевого типа. Другой вариант — использовать безопасные преобразования типов, которые возвращают null если попытка не удалась:
val aInt: Int? = a as? Int
Коллекции nullable типов
Если у вас есть коллекция элементов nullable типа и вы хотите отфильтровать элементы, отличные от null, вы можете сделать это, используя filterNotNull:
val nullableList: List<Int?> = listOf(1, 2, null, 4) val intList: List<Int> = nullableList.filterNotNull()
Что дальше?
Узнайте, как обрабатывать null в Java и Kotlin.
© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/null-safety.html