Работа с null-значениями #

Васильев Андрей Михайлович, 2024

Версии презентации

Одна из самых распространённых ошибок #

Рассмотрим следующий код, написанный на Java:

private String getData() { ... }
private void process() {
    String data = getData();
    System.out.println(data.length().toString());
}

В переменную data мы записываем результат работы функции getData

Возможными значениями являются:

  • Некоторая строка
  • Значение null

В случае null на строке 2 приложение завершит свою работу с ошибкой

Ошибка NullPointerException в Kotlin #

В Kotlin возможны только следующие ситуации, когда это исключение может быть выброшено:

  • Явное выбрасывание NullPointerException
  • Некорректный код при использовании оператора !!
  • Неконситентная инициализация объекта
  • Результат взаимодействия с Java-кодом

Null-значения в Kotlin #

Kotlin явно отделяет выражения, которые могут принимать значение null и те, которые не могут его принимать

Например следующий код вызовет ошибку компилятора:

var message:String = "Как прекрасен этот мир"
message = null // Ошибка компилятора

Но если мы изменим тип переменной, то код скомпилируется:

var message:String? = "Как непонятен этот мир"
message = null

? в конце обозначает не отдельный тип, а то, что значение может либо принимать значение типа, либо null

Давайте использовать везде ? #

Если есть спокойный способ не думать о null-значении, то зачем этим заниматься?

Ответ: компилятор kotlin будет требовать от вас доказательства того, что внутри переменной нет null, чтобы писать код удобно:

val goodMessage:String = "Надёжная строка"
val goodMessageLength = goodMessage.length
val unknownMessage:String? = "Ненадёжная строка"
val unknownMessageLength = unknownMessage.length // Ошибка!

Существует несколько способов работы со выражениями, которые могут принимать значение null

Явная проверка на null-значение #

Во-первых можно воспользоваться условным выражением, чтобы обработать оба возможных состояния выражения

val message:String? = "Странная, но нужная строка"
val length = if (message != null) message.length else -1

В случае отсутствия null-значения компилятор позволит обратиться к свойству объекта, т.к. в коде явно указано, что в данном пространстве не произойдёт NPE

Можно выполнять и более сложные проверки

val message: String? = "Kotlin"
if (message != null && message.length > 0) {
    print("Длина строки ${message.length}")
} else {
    print("Строка пустая")
}

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

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

Вторым способом доступа к свойству nullable переменной - это использование оператора безопасного вызова ?.

val message = "Kotlin"
val nullMessage: String? = null
println(nullMessage?.length) // Результат выражения - null
println(nullMessage?.length) // Ненужный безопасный вызов

Этот код возвращает nullMessage.length в том, случае, если nullMessage не имеет значение null. Иначе он возвращает null. Типом этого выражения будет Int?

Такие вызовы можно выстраивать в цепочку, когда мы хотим обратиться к сложным вложенным объектам:

bob?.department?.head?.name

Такая цепочка вернёт null в случае, если одно из свойств имеет значение null

Стоит ограниченно прибегать к данному подходу

Выполнение нескольких операций #

Безопасный вызов позволяет нам выполнить только одну операцию над значением, которое не равно null

Для проведения каких-либо операций исключительно над не-null значениями вы можете использовать let оператор вместе с оператором безопасного вызова.

val listWithNulls: List<String?> = listOf("Kotlin", null)
for (item in listWithNulls) {
    item?.let {
        val lenghth = it.length
        println("$it: $length")
    } // выводит Kotlin:6 и игнорирует null
}

Элвис-оператор #

Если у вас есть nullable ссылка b, вы можете либо провести проверку этой ссылки и использовать её, либо использовать не-null значение:

val l: Int = if (b != null) b.length else -1

Вместо того чтобы писать полное if-выражение, вы можете использовать элвис-оператор ?:

val l = b?.length ?: -1

Если выражение, стоящее слева от Элвис-оператора, не является null, то элвис-оператор его вернёт. В противном случае в качестве возвращаемого значения послужит то, что стоит справа. Обращаем ваше внимание на то, что часть кода, расположенная справа, выполняется ТОЛЬКО в случае, если слева получается null.

Защитные выражения #

При разработке функции удобно избавляться от неясности данных, переданных в качестве аргументов в самом начале функции

Мы можем выйти из функции в тот момент, когда становится понятно, что далее обработка данных уже невозможна

fun foo(node: Node): String {
    val parent = node.getParent() ?: return ""
    ...
}

Если не удалось получить значение из переданного аргумента, функция завершает свою работу

Такие проверки можно выполнять не только на null, но и на другие значения

Оператор !! #

Для любителей NPE существует третий способ: оператор not-null (!!) преобразует любое значение в non-null тип и выдает исключение, если значение равно null

Рассмотрим следующую переменную:

val message: String? = ...

Вы можете написать message!! и это вернёт нам либо non-null значение message (в нашем примере вернётся String), либо выкинет NPE, если message равно null

val length = message!!.length

В случае, если вам нужен NPE, вы можете заполучить её только путём явного указания

Использование данного оператора при написании кода в курсе запрещено