Spec-Zone.ru › Kotlin 1.7

Безопасность при работе с 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.

Чтобы выполнить определённую операцию только для значений, отличных от 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, используйте его, в противном случае используйте некоторое значение, отличное от 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

Коллекции ссылочных типов

Если у вас есть коллекция элементов ссылочного типа и вы хотите отфильтровать элементы, отличные от null, вы можете сделать это, используя filterNotNull:

val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
Последнее изменение: 06 сентября 2022 г.
Использование билдеров с выводом типов билдера Равенство

© 2010–2022 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