Null безопасность
Nullable типы и Non-Null типы
Система типов в Kotlin нацелена на то, чтобы искоренить опасность обращения к null
значениям, более известную как
“Ошибка на миллиард”.
Самым распространённым подводным камнем многих языков программирования, в том числе Java, является попытка произвести
доступ к null
значению. Это приводит к ошибке. В Java такая ошибка называется NullPointerException
(сокр. NPE).
В Kotlin NPE могут возникать только в случае:
- Явного указания
throw NullPointerException()
; - Использования оператора
!!
(описано ниже); - Несоответствие данных в отношении инициализации, например, когда:
- Неинициализированное
this
, доступное в конструкторе, передается и где-то используется (“утечкаthis
”); - Конструктор суперкласса вызывает open элемент, реализация которого в производном классе использует неинициализированное состояние.
- Неинициализированное
- Эту ошибку вызвал внешний Java-код:
- Попытка получить доступ к элементу
null
значения платформенного типа; - Проблемы с обнуляемостью при использовании обобщённых типов для взаимодействия с Java. Например, фрагмент кода Java может добавить
null
в KotlinMutableList<String>
, поэтому для работы с ним требуетсяMutableList<String?>
; - Другие проблемы, вызванные внешним Java-кодом.
- Попытка получить доступ к элементу
Система типов Kotlin различает ссылки на те, которые могут иметь значение null
(nullable ссылки) и те, которые
таковыми быть не могут (non-null ссылки). К примеру, переменная часто используемого типа String
не может быть null
.
var a: String = "abc" // Обычная инициализация означает non-null значение по умолчанию
a = null // ошибка компиляции
Для того чтобы разрешить null
значение, вы можете объявить эту строковую переменную как String?
.
var b: String? = "abc" // null-значения возможны
b = null // ok
Теперь, при вызове метода с использованием переменной a
, исключены какие-либо NPE. Вы спокойно можете писать:
val l = a.length
Но в случае, если вы захотите получить доступ к значению b
, это будет небезопасно. Компилятор предупредит об ошибке:
val l = b.length // ошибка: переменная `b` может быть null
Но вам по-прежнему нужен доступ к этому свойству/значению, верно? Есть несколько способов этого достичь.
Проверка на null
Первый способ: вы можете явно проверить b
на null
значение и обработать два варианта по отдельности.
val l = if (b != null) b.length else -1
Компилятор отслеживает информацию о проведённой вами проверке и позволяет вызывать length
внутри блока if
.
Также поддерживаются более сложные конструкции:
if (b != null && b.length > 0) {
print("Строка длиной ${b.length}")
} else {
print("Пустая строка")
}
Обратите внимание: это работает только в том случае, если b
является неизменной переменной (ориг.: immutable).
Например, если это локальная переменная, значение которой не изменяется в период между его проверкой и использованием,
или переменная val
, которая имеет теневое поле и не может быть переопределено. В противном случае может так оказаться,
что переменная b
изменила своё значение на null
после проверки.
Безопасные вызовы
Вторым способом доступа к свойству nullable переменной - это использование оператора безопасного вызова ?.
.
val a = "Kotlin"
val b: String? = null
println(b?.length)
println(a?.length) // Ненужный безопасный вызов
Этот код возвращает b.length
в том, случае, если b
не имеет значение null
. Иначе он возвращает null
. Типом этого
выражения будет Int?
.
Такие безопасные вызовы полезны в цепочках. К примеру, если Bob (Боб), Employee (работник), может быть прикреплён (или нет) к отделу Department, и у отдела может быть управляющий, другой Employee. Для того чтобы обратиться к имени этого управляющего (если такой есть), напишем:
bob?.department?.head?.name
Такая цепочка вернёт null
в случае, если одно из свойств имеет значение null
.
Для проведения каких-либо операций исключительно над non-null значениями вы можете использовать
let
оператор вместе с оператором безопасного вызова.
val listWithNulls: List<String?> = listOf("Kotlin", null)
for (item in listWithNulls) {
item?.let { println(it) } // выводит Kotlin и игнорирует null
}
Безопасный вызов также может быть размещен в левой части присвоения. Затем, если один из получателей в цепочке
безопасных вызовов равен null
, присвоение пропускается, а выражение справа вообще не вычисляется.
// Если значение `person` или `person.department` равно null, функция не вызывается
person?.department?.head = managersPool.getManager()
Элвис-оператор
Если у вас есть nullable ссылка b
, вы можете либо провести проверку этой ссылки и использовать её, либо использовать
non-null значение:
val l: Int = if (b != null) b.length else -1
Вместо того чтобы писать полное if
-выражение, вы можете использовать элвис-оператор ?:
.
val l = b?.length ?: -1
Если выражение, стоящее слева от Элвис-оператора, не является null
, то элвис-оператор его вернёт. В противном случае
в качестве возвращаемого значения послужит то, что стоит справа. Обращаем ваше внимание на то, что часть кода,
расположенная справа, выполняется ТОЛЬКО в случае, если слева получается null
.
Так как throw
и return
тоже являются выражениями в Kotlin, их также можно использовать справа от Элвис-оператора.
Это может быть крайне полезным для проверки аргументов функции.
fun foo(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
// ...
}
Оператор !!
Для любителей NPE существует третий способ: оператор not-null (!!
) преобразует любое значение в non-null тип и выдает
исключение, если значение равно null
. Вы можете написать b!!
и это вернёт нам либо non-null значение b
(в нашем примере вернётся String
), либо выкинет NPE, если b
равно null
.
val l = b!!.length
В случае, если вам нужен NPE, вы можете заполучить её только путём явного указания.
Безопасные приведения типов
Обычное приведение типа может вызвать ClassCastException
в случае, если объект имеет другой тип.
Можно использовать безопасное приведение, которое вернёт null
, если попытка не удалась.
val aInt: Int? = a as? Int
Коллекции nullable типов
Если у вас есть коллекция nullable элементов и вы хотите отфильтровать все non-null элементы, используйте функцию filterNotNull
.
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()