Spec-Zone.ru › Kotlin 1.9

Безопасность от ссылок null

Типы, допускающие null, и типы, не допускающие null

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

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

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

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

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

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

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

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

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

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

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

Безопасные вызовы

Второй вариант доступа к свойству в переменной, допускающей 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 — сотрудник, которому может быть назначен отдел (или нет). Этот отдел, в свою очередь, может иметь другого сотрудника в качестве руководителя отдела. Чтобы получить имя руководителя отдела Боба (если он есть), вы напишете следующее:

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()

Получатель, допускающий null

Функции расширения могут быть определены с получателем, допускающим null. Таким образом, вы можете указать поведение для нулевых значений, не прибегая к логике проверки на null в каждом месте вызова.

Например, функция toString() определена для получателя, допускающего null. Она возвращает строку "null" (в отличие от null значения). Это может быть полезно в некоторых ситуациях, например, при ведении журнала:

val person: Person? = null
logger.debug(person.toString()) // Logs "null", does not throw an exception

Если вы хотите, чтобы вызов toString() возвращал строку, допускающую null, используйте оператор безопасного вызова ?.:

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

Когда у вас есть ссылка, допускающая 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. Вы можете написать 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()

Что дальше?

Узнайте, как обрабатывать допуска null в Java и Kotlin.

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

© 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