Безопасность от ссылок на null
Типы, допускающие null, и типы, не допускающие null
Система типов Kotlin направлена на устранение опасности ссылок на null в коде, также известной как Ошибка в миллиард долларов.
Одной из наиболее распространённых проблем во многих языках программирования, включая Java, является то, что обращение к члену ссылки на null приводит к исключению NullPointerException. В Java это эквивалентно NullPointerException или NPE вкратце.
Система типов Kotlin направлена на устранение NullPointerException из нашего кода. Единственные возможные причины возникновения NPE:
- Явное обращение к
throw NullPointerException(); - Использование оператора
!!, который описан ниже; - Некоторые несоответствия данных в отношении инициализации, такие как, когда:
- Неинициализированный this, доступный в конструкторе, передаётся и используется где-то («утечка this»);
- Конструктор суперкласса вызывает открытый член, реализация которого в производном классе использует неинициализированное состояние;
- Взаимодействие с Java:
- Попытки доступа к члену по ссылке на
nullссылку типа платформенного типа; - Обобщённые типы, используемые для взаимодействия с Java, с неправильной указанностью на возможность null, например, фрагмент кода 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, мы можем объявить переменную как строку, допускающую null, написанную String?:
fun main() {
//sampleStart
var b: String? = "abc" // can be set 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 после проверки.
Безопасные вызовы
Ваш второй вариант — оператор безопасного вызова, написанный ?.:
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.
Чтобы выполнить определённую операцию только для ненулевых значений, вы можете использовать оператор безопасного вызова вместе с 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()
Оператор Elvis
Когда у нас есть ссылка, допускающая null, b, мы можем сказать: «Если b не равно 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!!, и это вернёт ненулевое значение b (например, String в нашем примере) или вызовет NPE, если b равно null:
val l = b!!.length
Таким образом, если вы хотите NPE, вы можете его получить, но вы должны явно его запросить, и он не появится из ниоткуда.
Безопасные преобразования типов
Обычные преобразования типов могут привести к ClassCastException, если объект не принадлежит целевому типу. Другой вариант — использовать безопасные преобразования типов, которые возвращают null, если попытка не увенчалась успехом:
val aInt: Int? = a as? Int
Коллекции типов, допускающих null
Если у вас есть коллекция элементов допускающего null типа, и вы хотите отфильтровать ненулевые элементы, вы можете сделать это, используя filterNotNull:
val nullableList: List<Int?> = listOf(1, 2, null, 4) val intList: List<Int> = nullableList.filterNotNull()
© 2010–2020 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/reference/null-safety.html