Spec-Zone.ru › Kotlin 1.8

Безопасность при работе с null

Типы с возможностью null и типы без null

Система типов Kotlin направлена на устранение опасности ссылок на null, также известной как Ошибка миллиарда долларов.

Одна из самых распространённых проблем во многих языках программирования, включая Java, заключается в том, что обращение к члену ссылки на null приводит к исключению NullPointerException. В Java это эквивалентно NullPointerException, или NPE вкратце.

Единственными возможными причинами возникновения NPE в Kotlin являются:

  • Явное обращение к throw NullPointerException().

  • Использование оператора !!, описанного ниже.

  • Несоответствие данных в отношении инициализации, например, когда:

    • Неинициализированный this доступный в конструкторе передаётся и используется где-либо («утечка this»).

    • Конструктор суперкласса вызывает открытый член, реализация которого в производном классе использует неинициализированное состояние.

  • Взаимодействие с Java:

    • Попытки получить доступ к члену ссылки на null типа платформы;

    • Проблемы с применимостью к типам с параметрами, используемым для взаимодействия с Java. Например, код Java может добавить null в Kotlin MutableList<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.

Последнее изменение: 10 января 2023 г.
Использование билдеров с выводом типов билдера Равенство

© 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API