У всех переменных и констант, с которыми мы работали до сих пор, были конкретные значения. У переменной типа string
, вроде var name
, есть строковое значение, которое с ней ассоциируется. К примеру, "Joe Howard"
. Это может быть и пустая строка вроде ""
, однако и в ней все же есть значение, к которому можно отсылаться.
Это одна из встроенных особенностей для безопасности в Kotlin. Если используется тип Int
или String, тогда целое число или строка действительно существует — гарантированно.
Содержание статьи
- Основы использования Null в Kotlin
- Сторожевое значение (Sentinel) в Kotlin
- Основы использования nullable типов
- Проверка null значений в Kotlin
- Оператор утверждения не-null значения
- Smart casts, или умные преобразования в Kotlin
- Оператор безопасного вызова в Kotlin
- Функция let() в Kotlin
- Оператор Элвиса в Kotlin
- Итоговые задания для проверки
- Ключевые особенности null типов в Kotlin
В данном уроке будут рассмотрены nullable типы, которые позволяют представлять не только значение, но и его отсутствие. К концу урока вы узнаете, для чего нужны nullable типы и как их безопасно использовать.
Основы использования Null в Kotlin
Иногда полезно иметь возможность показать отсутствие значения. Представьте случай, когда вам нужно сделать ссылку на персональные данные человека: требуется сохранить его имя, возраст и род занятий. У всех людей есть имя и возраст, так что эти значения определенно будут присутствовать. Однако не у всех есть работа, поэтому важно, чтобы можно было не указывать род занятий.
Допустим, мы не знаем про null, тогда имя, возраст и род занятий человека можно было бы указать следующим образом:
1 2 3 | var name = "Джо Говард" var age = 24 var occupation = "Программист и Автор" |
Но что, если человек стал безработным? Может, он достиг просвещения и решил жить на вершине горы. Здесь пригодилась бы возможность указать на отсутствие значения.
Почему бы просто не использовать пустую строку? Это можно сделать, однако nullable типы будут наиболее оптимальным решением. Далее подробнее о том, почему именно.
Сторожевое значение (Sentinel) в Kotlin
Действительное значение, которое представляет специальное условие вроде отсутствия значения, называется сторожевое значением. В предыдущем примере это была бы пустая строка.
Давайте рассмотрим другой пример. Допустим, код запрашивает что-то с сервера, и вы используете переменную для хранения любого возвращаемого кода ошибки:
1 | var errorCode = 0 |
В случае успеха, отсутствие ошибки обозначается нулем. Это означает, что 0
является сторожевым (sentinel) значением. Как и пустая строка для названия рода деятельности, это работает, но может сбить с толку программиста. 0
на самом деле может быть допустимым кодом ошибки — или может быть в будущем, если сервер изменит свой ответ. В любом случае нельзя быть полностью уверенным в том, что сервер не вернул ошибку, не ознакомившись с документацией. В этих двух примерах было бы лучше, если бы существовал специальный тип, который мог бы представлять отсутствие значения. Тогда было бы понятно, когда значение существует, а когда нет.
Null является названием, которое дается отсутствию значения. Мы рассмотрим, как Kotlin напрямую внедряет данный концепт в язык довольно элегантным способом. Некоторые другие языки программирования просто используют сторожевые значения. В других есть концепт null значения, но это только синоним для нуля. Это просто другое sentinel значение.
Kotlin вводит целый новый набор типов — nullable типы. Они позволяют значению быть равным null
. Если вы обрабатываете не-null тип, у вас точно будут какие-то значения, поэтому не нужно беспокоиться о существовании действительного значения. Аналогично, при использовании nullable типа вы должны обрабатывать случаи использования типа null
. Так убирается двусмысленность, присущая использованию sentinel значениям.
Основы использования nullable типов
Nullable типы в Kotlin являются решением проблемы представления значения или его отсутствия. Nullable типы могут содержать какое-то значение или содержать null
.
Концепт nullable можно сравнить с ящиком: в нем или есть значение, или его нет. Если значения нет, он содержит null
. Ящик сам по себе существует всегда. Вы всегда можете его открыть и посмотреть, что внутри.
С другой стороны, у типов String
или Int
нет такого ящика. Вместо этого у них в любом случае будет какое-то значение, к примеру, "hello"
или 42
. Помните, что у не-null типов гарантированно есть действительное значение.
На заметку: Многие из вас наверняка слышали о коте Шредингера. Идея nullable очень похожа на данный концепт, за тем исключением, что это не вопрос жизни и смерти!
Переменная nullable типа объявляется через использование следующего синтаксиса:
1 | var errorCode: Int? |
Единственная разница между таким и стандартным способом объявлением переменной, это наличие вопросительного знака после типа. В данном случае errorCode
является «nullable Int
«. Это значит, что сама переменная похожа на ящик, содержащий либо Int
, либо null
.
На заметку: Для создания nullable типа вы можете добавить вопросительный знак после любого типа. Например, nullable тип
String?
. Другими словами, ящик типаString?
содержит либоString
, либоnull
.Также обратите внимание, что nullable тип должен создаваться явным способом с помощью объявления типа (например:
Int?
). Nullable типы никогда не могут быть выведены из значений инициализации, поскольку у этих значений обычный не-null тип.
Указать значение для такой переменной очень просто. Значение Int
можно назначить следующим образом:
1 | errorCode = 100 |
Или вы можете указать значение null
:
1 | errorCode = null |
Следующая диаграмма может помочь визуализировать, что происходит в обоих вариантах:
Так называемый nullable ящик существует всегда. Во время присваивания переменной значения 100
вы заполняете ящик значением. Когда вы присваиваете переменной значение null
, данный ящик опустошается.
Подумайте о данном концепте. Аналогия с ящиком может сильно помочь разобрать оставшуюся часть урока и начать использовать nullable типы.
Задания для проверки
- Создайте nullable строку
myFavoriteSong
. Если у вас есть любимая песня, пускай ее название и будет значением данной строки. Если у вас много любимых песен или нет вообще, укажите значениеnull
; - Создайте константу
parsedInt
, которая будет равна"10".toIntOrNull()
. Таким образом будет попытки спарсить строку"10"
и конвертировать её в типInt
. Проверьте тип константыparsedInt
, кликнув наtoIntOrNull()
и удерживая комбинацию клавишCTRL + Shift + P
. Почему это nullable значение? - Измените строку, рассматриваемую в предыдущем задании, на что-то кроме чисел (к примеру, слово
dog
). Чему теперь равно константаparsedInt
?
Проверка null значений в Kotlin
Конечно, хорошо, что существуют nullable типы. Однако, может возникнуть вопрос, как заглянуть внутрь ящика и изменить значение которое там содержится.
В некоторых случаях можно просто использовать тип null как любой другой тип.
Посмотрим, что произойдет при выводе значения типа null:
1 2 | var result: Int? = 30 println(result) |
Выведется просто 30
.
Рассмотрим, как тип null отличается от других типов, использовав переменную result
в выражении, будто это обычное целое число:
1 | println(result + 1) |
Код приведет к ошибке:
1 2 3 | Operator call corresponds to a dot-qualified call 'result.plus(1)' which is not allowed on a nullable receiver 'result'. // Вызов оператора соответствует вызову с точкой 'result.plus(1)', который не разрешен для nullable получателя 'result'. |
Код не работает, потому что вы пытаетесь сложить вместе ящик и целое число — то есть не значение внутри ящика, а сам ящик. В этом нет смысла.
Оператор утверждения не-null значения
В самом сообщений об ошибке есть решение нашей задачи — вам сообщают о том, что nullable тип все еще внутри ящика. Требуется удалить значение из ящика.
Посмотрим, как это работает. Рассмотрим следующие объявления:
1 2 3 4 | fun main() { var authorName: String? = "Джо Говард" var authorAge: Int? = 24 } |
Есть два разных метода, которые вы можете использовать для nullable типов из ящика. Первым является оператор утверждения не-null значения !!
, который используется следующим образом:
1 2 3 4 5 6 | fun main() { var authorAge: Int? = 26 val ageAfterBirthday = authorAge!! + 1 println("После следующего дня рождения автору будет $ageAfterBirthday") } |
Данный код выводит:
1 | После следующего дня рождения автору будет 27 |
Отлично! Чего и следовало ожидать.
Двойной восклицательный знак после названия переменной сообщает компилятору, что вы хотите заглянуть внутрь ящика и извлечь значение. Результатом является значение не-null типа. Это значит, что у ageAfterBirthday
тип Int
, а не Int?
.
При использовании оператора утверждения !!
будьте осторожны. Вы должны использовать не-null утверждения с умом. Чтобы понять, почему, подумайте, что произойдет, когда nullable тип не содержит значения:
1 2 | authorAge = null println("После двух дней рождений автору будет ${authorAge!! + 2}") |
Код приведет к следующей ошибке:
1 2 3 | Exception in thread "main" kotlin.KotlinNullPointerException // Исключение в потоке "main" kotlin.KotlinNullPointerException |
Исключение null указателя возникает из-за того, что переменная не содержит значения, когда вы пытаетесь ее использовать а там null. Что еще хуже, это исключение возникает во время работы, а не во время компиляции программы. Это означает, что вы заметите ошибку только в том случае, если вы выполните код с некорректным вводом. Что еще хуже, если бы этот код находился внутри приложения, исключение с null указателем привело бы к сбою всего приложения!
Как можно обезопасить себя? Для этого обратимся ко второму способу получения значения из nullable типа.
Smart casts, или умные преобразования в Kotlin
При определенных обстоятельствах можно проверить, есть ли у nullable типа значение кроме null. Если это так, можно использовать переменную таким образом, будто она не является null
:
1 2 3 4 5 6 7 8 9 10 11 | fun main() { var authorName: String? = "Джо Говард" var nonNullableAuthor: String var nullableAuthor: String? if (authorName != null) { nonNullableAuthor = authorName } else { nullableAuthor = authorName } } |
Сразу видно, что здесь при использовании nullable значения authorName
нет восклицательных знаков. Такое использование nullable проверок является примером smart cast, или умного преобразования типов.
Если nullable переменная содержит значение, тогда if выражение выполняет первый блок кода, в котором Kotlin умно преобразует authorName
к обычному не-null типу String
. Если nullable переменная не содержит значения (null), тогда выражение if
выполняет блок else
.
Видно, что использование smart cast намного безопаснее, чем утверждение не-null значения при помощи оператора !!
. Лучше использовать smart cast каждый раз, когда у nullable типа может быть значение null
. Утверждение не-null значения подходит только в том случае, если nullable гарантированно содержит значение.
Использование smart cast полезно в том случае, если nullable проверен и не будет изменен после этого. К примеру, если nullable присваивается переменной, которая не изменяется после умного преобразования и перед использованием или присвоения константе.
Теперь вы знаете, как безопасно заглянуть внутрь nullable типа и извлечь значение, если оно там есть.
Задания для проверки
- Используя переменную
myFavoriteSong
из прошлого задания, используйте null-проверку и smart cast (умное преобразование), чтобы проверить, содержит ли переменная какое либо значение. Если да, то выведите это значение. Если нет, выведите"У меня нет любимой песни"
; - Измените переменную
myFavoriteSong
на противоположность тому, чем она сейчас является. Если это null, назначьте ей строку. Если это строка, назначьте ей null. Как изменился результат вывода?
Оператор безопасного вызова в Kotlin
Предположим, вам нужно что-то сделать с nullable строкой, помимо ее вывода в терминале. К примеру, узнать длину строки. Использование умного преобразования внутри проверки значения на null является излишним для такого простого использования строки. Если вы попытаетесь получить доступ к длине строки, как если бы строка была не-nullable и без умного преобразования, вы получите ошибку от компилятора:
1 | Only safe (?.) or non-null asserted (!!) calls are allowed on a nullable receiver of type String? |
Само сообщений об ошибке указывает на решение, которым является использование безопасного вызова при помощи оператора ?.
:
1 2 3 4 5 | fun main() { var authorName: String? = "Джо Говард" var nameLength = authorName?.length println("Длина имени автора $nameLength.") // > Длина имени автора 10. } |
При использовании оператора безопасного вызова вы получаете доступ к свойству length
.
Безопасные вызовы могут использоваться цепочкой:
1 2 3 4 5 6 | fun main() { var authorName: String? = "Джо Говард" val nameLengthPlus5 = authorName?.length?.plus(5) println("Длина имени автора плюс 5 является $nameLengthPlus5.") // Результат: Длина имени автора плюс 5 является 15. } |
Если безопасный вызов выполняется для null значения, выражение перестает выполнять цепочку и возвращает null.
Поскольку результатом безопасного вызова может быть null
, выражения, использующие безопасные вызовы для nullable, возвращают nullable типы. К примеру, у используемого выше nameLength
тип Int?
, а не Int
, даже если свойство length
в строке является не-nullable. Тип всего выражения является nullable.
Функция let() в Kotlin
Оператор безопасного вызова предоставляет другой способ использования умного преобразования для работы с не-null значением внутри nullable — использование функции let()
из стандартной библиотеки:
1 2 3 | authorName?.let { nonNullableAuthor = authorName } |
Внутри вызова функции let
переменная становится не-nullable, так что вы можете получить доступ к ее свойствам без использования оператора вызова:
1 2 3 | authorName?.let { nameLength = authorName.length } |
Подробнее о синитаксисе функции let
, который называется trailing lambda, будет рассказано в будущих уроках.
Оператор Элвиса в Kotlin
Есть еще один удобный способ получить значение из nullable типа. Он используется в тех случаях, когда требуется извлечь значение из nullable типа несмотря ни на что — и в случае null
будет использовать значение по умолчанию.
Это работает следующим образом:
1 2 3 4 5 6 | fun main() { var nullableInt: Int? = 10 var mustHaveResult = nullableInt ?: 0 println(mustHaveResult) // Результат: 10 } |
Оператор ?:
, используемый на третьей строке, называется оператором Элвиса. Причина в том, что если повернуть его на 90 градусов, форма станет похожей на знаменитую рок-звезду.
Использование оператора Элвиса означает, что переменная mustHaveResult
будет соответствовать значению внутри nullableInt
или 0
, если переменная nullableInt
содержит null
. В данном примере типом переменной mustHaveResult
является Int
, и она содержит конкретное значение 10
.
Предыдущий код использует оператор Элвиса как эквивалент следующему использованию null-проверки и умного преобразования, но в более краткой форме:
1 2 3 4 5 6 | fun main() { var nullableInt: Int? = 10 var mustHaveResult = if (nullableInt != null) nullableInt else 0 println(mustHaveResult) // Результат: 10 } |
Укажем значение пременной nullableInt
как null
следующим образом:
1 2 3 4 5 6 | fun main() { var nullableInt: Int? = null var mustHaveResult = nullableInt ?: 0 println(mustHaveResult) // Результат: 0 } |
Теперь переменная mustHaveResult
равна 0
.
Итоговые задания для проверки
Вы ознакомились с теоретической частью nullable типов в Kotlin. Пришло время проверить своим знания на практике.
Задание 1: Теперь вы компилятор
Какие из следующих утверждений правильные?
1 2 3 4 | var name: String? = "Ray" var age: Int = null val distance: Float = 26.7 var middleName: String? = null |
Задание 2: Разделяй и властвуй
Сначала создайте функцию, которая возвращает сколько раз целое число может быть разделено на другое целое число без остатка. Функция должна возвращать null
, если результатом деления является не целое число. Назовите функцию DivideIfWhole
.
Затем напишите код, который пытается извлечь nullable результат функции.
Должно быть два случая:
- в случаи успеха выведите
"Да, число делится $answer раз"
; - в случаи неудачи, выведите на экран
"Не делится :["
.
Под конец проверьте свою функцию:
1 2 | 1. Поделить 10 на 2. Должно вывести `"Да, число делится 5 раз"` 2. Поделить 10 на 3. Должно вывести `"Не делится :["` |
Подсказка 1: Используйте следующий код в качестве изначальной сигнатуры функции:
1 | fun divideIfWhole(value: Int, divisor: Int) |
Вам нужно будет добавить возвращаемый тип, который будет nullable!
Подсказка 2: Можно использовать оператор модуля (%
) для определения, делится ли одно целое число на другое. Этот оператор возвращает остаток от деления двух чисел. К примеру, 10 % 2 = 0
значит, что 10 делится на 2 без остатка, в то время как 10 % 3 = 1
значит, что 10 делится на 3 с остатком 1.
Задание 3: Рефакторинг и сокращение
Код из последнего задания использовал операторы if
. Для данного задания реорганизуйте данный код, чтобы использовать оператор Элвиса. На этот раз пускай он выводит "Число делится Х раз"
во всех случаях, но если деление не дает целого числа, то X
должен заменяться на 0
.
Ключевые особенности nullable типов в Kotlin
null
представляет собой отсутствие значения;- Не-null переменные и константы всегда содержать не-null значение;
- Nullable переменные и константы похожи на ящики, которые могут содержать значение или быть пустыми (
null
); - Для работы со значением внутри nullable типа сначала нужно проверить, не является ли значение null;
- Самым безопасным способом работы с nullable значением является использование безопасных вызовов или оператора Элвиса;
- Используйте оператор
!!
утверждения не-null значения только в случае необходимости, так как они зачастую приводят к ошибке работы программы.
Что теперь?
Nullable типы являются незаменимой особенностью Kotlin, которая делает язык безопасным и простым в использовании. Они позволяют выбирать, когда значения должны и не должны быть nullable. Вы должны использовать их при необходимости, делая свой код безопасным, обеспечивая явную обработку в случае отсутствия значения.
Nullable типы определенно сделают вашу работу с Kotlin намного интереснее! В будущих уроках мы рассмотрим использование nullable значений вместе с коллекциями и лямбдами.
Отличная статья