Null безопасность

Nullable типы и Non-Null типы

Система типов в Kotlin нацелена на то, чтобы искоренить опасность обращения к null значениям, более известную как “Ошибка на миллиард”.

Самым распространённым подводным камнем многих языков программирования, в том числе Java, является попытка произвести доступ к null значению. Это приводит к ошибке. В Java такая ошибка называется NullPointerException (сокр. NPE).

В Kotlin NPE могут возникать только в случае:

  • Явного указания throw NullPointerException();
  • Использования оператора !! (описано ниже);
  • Несоответствие данных в отношении инициализации, например, когда:
    • Неинициализированное this, доступное в конструкторе, передается и где-то используется (“утечка this”);
    • Конструктор суперкласса вызывает open элемент, реализация которого в производном классе использует неинициализированное состояние.
  • Эту ошибку вызвал внешний Java-код:
    • Попытка получить доступ к элементу null значения платформенного типа;
    • Проблемы с обнуляемостью при использовании обобщённых типов для взаимодействия с Java. Например, фрагмент кода Java может добавить null в Kotlin MutableList<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()