August 26, 2019

Введение в Kotlin: nullable и non-null типы, приведение и проверка типов

Введение в Kotlin: функции, переменные, условия, циклы
Введение в Kotlin: классы, конструкторы, методы и свойства, наследование
Введение в Kotlin: интерфейсы, модификаторы доступа, вложенные классы, ключевые слова this и object
Введение в Kotlin: коллекции, data-классы, деструктуризация и sealed-классы

В пятой статье серии "Введение в Kotlin" мы рассмотрим работу с Nullable и Non-Null типами (Null-safety), а также приведение и проверку типов. Подробнее об этих темах можно почитать в официальной документации: null-safety и приведение и проверка типов.

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

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

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

Kotlin призван исключить ошибки подобного рода из нашего кода. NPE могу возникать только в случае:

  • явного указания throw NullPointerException();
  • использования оператора !! (описано ниже);
  • эту ошибку вызвал внешний Java-код;
  • есть какое-то несоответствие при инициализации данных (в конструкторе использована ссылка this на данные, которые не были ещё проинициализированы).

Система типов Kotlin различает ссылки на те, которые могут иметь значение null (nullable ссылки), и те, которые таковыми быть не могут (non-null ссылки). К примеру, переменная часто используемого типа String не может быть null:

var a: String = "abc"
a = null // ошибка компиляции

Для того чтобы разрешить null значение, мы можем объявить эту строковую переменную как String?:

var b: String? = "abc"
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("String of length ${b.length}")
} else {
    print("Empty string")
}

Обратите внимание: это работает только в том случае, если b является неизменной переменной. Например, если это локальная переменная, значение которой не изменяется в период между его проверкой и использованием. Также такой переменной может служить val. В противном случае может оказаться, что переменная b изменила своё значение на null после проверки.

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

Вторым способом является оператор безопасного вызова ?. :

b?.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("A", null)
for (item in listWithNulls) {
    item?.let { println(it) } // выводит A и игнорирует null
}

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

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

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 существует ещё один способ. Мы можем написать b!!, и это либо вернёт нам non-null значение b (в нашем примере вернётся String), либо выкинет NPE:

val l = b!!.length

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

Приведение и проверка типов

Операторы is и !is

Мы можем проверить, принадлежит ли объект к какому-либо типу, во время исполнения с помощью оператора is или его отрицания !is:

if (obj is String) {
    print(obj.length)
}

if (obj !is String) { // то же самое, что и !(obj is String)
    print("Not a String") 
} else { 
    print(obj.length) 
}

Умные приведения

Во многих случаях в Kotlin вам не нужно использовать явные приведения, потому что компилятор следит за is-проверками для неизменяемых значений и вставляет приведения автоматически там, где они нужны:

fun demo(x: Any) {
    if (x is String) {
        print(x.length) // x автоматически преобразовывается в String 
    }
}

Компилятор достаточно умён для того, чтобы делать автоматические приведения в случаях, когда проверка на несоответствие типу (!is) приводит к выходу из функции:

if (x !is String) return
    print(x.length) // x автоматически преобразовывается в String

или в случаях, когда приводимая переменная находится справа от оператора && или ||:

// x автоматически преобразовывается в String справа от `||`
if (x !is String || x.length == 0) return

// x автоматически преобразовывается в String справа от `&&` 
if (x is String && x.length > 0) {
    print(x.length) // x автоматически преобразовывается в String
}

Такие умные приведения работают вместе с when-выражениями и циклами while:

when (x) {
    is Int -> print(x + 1)
    is String -> print(x.length + 1)
    is IntArray -> print(x.sum())
}

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

  • с локальными val переменными - всегда;
  • с val свойствами - если поле имеет модификатор доступа private или internal, или проверка происходит в том же модуле, в котором объявлено это свойство. Умные при����едения неприменимы к публичным свойствам или свойствам, которые имеют переопределённые getter'ы;
  • с локальными var переменными - если переменная не изменяется между проверкой и использованием и не захватывается лямбдой, которая её модифицирует;
  • с var свойствами - никогда (потому что переменная может быть изменена в любое время другим кодом).

Оператор "небезопасного" приведения

Обычно оператор приведения выбрасывает исключение, если приведение невозможно, поэтому мы называем его небезопасным. Небезопасное приведение в Kotlin выполняется с помощью инфиксного оператора as (см. приоритеты операторов):

val x: String = y as String

Заметьте, что null не может быть приведен к String, так как String не является nullable, т.е. если y - null, код выше выбросит исключение. Чтобы соответствовать семантике приведений в Java, нам нужно указать nullable тип в правой части приведения:

val x: String? = y as String?

Оператор "безопасного" (nullable) приведения

Чтобы избежать исключения, вы можете использовать оператор безопасного приведения as?, который возвращает null в случае неудачи:

val x: String? = y as? String

Заметьте, что, несмотря на то что справа от as? стоит non-null тип String, результат приведения является nullable.

Следующая часть серии статей "Введение в Kotlin" доступна здесь.

Источники:

Документация Kotlin: Null безопасность

Документация Kotlin: Приведение и проверка типов