Тип null в Kotlin — Как правильно работать с типом null

У всех переменных и констант, с которыми мы работали до сих пор, были конкретные значения. У переменной типа string, вроде var name, есть строковое значение, которое с ней ассоциируется. К примеру, "Joe Howard". Это может быть и пустая строка вроде "", однако и в ней все же есть значение, к которому можно отсылаться.

Это одна из встроенных особенностей для безопасности в Kotlin. Если  используется тип Int или String, тогда целое число или строка действительно существует — гарантированно.

Содержание статьи

В данном уроке будут рассмотрены nullable типы, которые позволяют представлять не только значение, но и его отсутствие. К концу урока вы узнаете, для чего нужны nullable типы и как их безопасно использовать.

Основы использования Null в Kotlin

Иногда полезно иметь возможность показать отсутствие значения. Представьте случай, когда вам нужно сделать ссылку на персональные данные человека: требуется сохранить его имя, возраст и род занятий. У всех людей есть имя и возраст, так что эти значения определенно будут присутствовать. Однако не у всех есть работа, поэтому важно, чтобы можно было не указывать род занятий.

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

Но что, если человек стал безработным? Может, он достиг просвещения и решил жить на вершине горы. Здесь пригодилась бы возможность указать на отсутствие значения.

Почему бы просто не использовать пустую строку? Это можно сделать, однако nullable типы будут наиболее оптимальным решением. Далее подробнее о том, почему именно.

Сторожевое значение (Sentinel) в Kotlin

Действительное значение, которое представляет специальное условие вроде отсутствия значения, называется сторожевое значением. В предыдущем примере это была бы пустая строка.

Давайте рассмотрим другой пример. Допустим, код запрашивает что-то с сервера, и вы используете переменную для хранения любого возвращаемого кода ошибки:

В случае успеха, отсутствие ошибки обозначается нулем. Это означает, что 0 является сторожевым (sentinel) значением. Как и пустая строка для названия рода деятельности, это работает, но может сбить с толку программиста. 0 на самом деле может быть допустимым кодом ошибки — или может быть в будущем, если сервер изменит свой ответ. В любом случае нельзя быть полностью уверенным в том, что сервер не вернул ошибку, не ознакомившись с документацией. В этих двух примерах было бы лучше, если бы существовал специальный тип, который мог бы представлять отсутствие значения. Тогда было бы понятно, когда значение существует, а когда нет.

Null является названием, которое дается отсутствию значения. Мы рассмотрим, как Kotlin напрямую внедряет данный концепт в язык довольно элегантным способом. Некоторые другие языки программирования просто используют сторожевые значения. В других есть концепт null значения, но это только синоним для нуля. Это просто другое sentinel значение.

Kotlin вводит целый новый набор типов — nullable типы. Они позволяют значению быть равным null. Если вы обрабатываете не-null тип, у вас точно будут какие-то значения, поэтому не нужно беспокоиться о существовании действительного значения. Аналогично, при использовании nullable типа вы должны обрабатывать случаи использования типа null. Так убирается двусмысленность, присущая использованию sentinel значениям.

Основы использования nullable типов

Nullable типы в Kotlin являются решением проблемы представления значения или его отсутствия. Nullable типы могут содержать какое-то значение или содержать null.

Концепт nullable можно сравнить с ящиком: в нем или есть значение, или его нет. Если значения нет, он содержит null. Ящик сам по себе существует всегда. Вы всегда можете его открыть и посмотреть, что внутри.

nullable box

С другой стороны, у типов String или Int нет такого ящика. Вместо этого у них в любом случае будет какое-то значение, к примеру, "hello" или 42. Помните, что у не-null типов гарантированно есть действительное значение.

На заметку: Многие из вас наверняка слышали о коте Шредингера. Идея nullable очень похожа на данный концепт, за тем исключением, что это не вопрос жизни и смерти!

Переменная nullable типа объявляется через использование следующего синтаксиса:

Единственная разница между таким и стандартным способом объявлением переменной, это наличие вопросительного знака после типа. В данном случае errorCode является «nullable Int«. Это значит, что сама переменная похожа на ящик, содержащий либо Int, либо null.

На заметку: Для создания nullable типа вы можете добавить вопросительный знак после любого типа. Например, nullable тип String? . Другими словами, ящик типа String? содержит либо String, либо null.

Также обратите внимание, что nullable тип должен создаваться явным способом с помощью объявления типа (например: Int?). Nullable типы никогда не могут быть выведены из значений инициализации, поскольку у этих значений обычный не-null тип.

Указать значение для такой переменной очень просто. Значение Int можно назначить следующим образом:

Или вы можете указать значение null:

Следующая диаграмма может помочь визуализировать, что происходит в обоих вариантах:

null kotlin

Так называемый nullable ящик существует всегда. Во время присваивания переменной значения 100 вы заполняете ящик значением. Когда вы присваиваете переменной значение null, данный ящик опустошается.

Подумайте о данном концепте. Аналогия с ящиком может сильно помочь разобрать оставшуюся часть урока и начать использовать nullable типы.

Задания для проверки

  1. Создайте nullable строку myFavoriteSong. Если у вас есть любимая песня, пускай ее название и будет значением данной строки. Если у вас много любимых песен или нет вообще, укажите значение null;
  2. Создайте константу parsedInt, которая будет равна "10".toIntOrNull(). Таким образом будет попытки спарсить строку "10" и конвертировать её в тип Int. Проверьте тип константы parsedInt, кликнув на toIntOrNull() и удерживая комбинацию клавиш CTRL + Shift + P. Почему это nullable значение?
  3. Измените строку, рассматриваемую в предыдущем задании, на что-то кроме чисел (к примеру, слово dog). Чему теперь равно константа parsedInt?

Проверка null значений в Kotlin

Конечно, хорошо, что существуют nullable типы. Однако, может возникнуть вопрос, как заглянуть внутрь ящика и изменить значение которое там содержится.

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

Посмотрим, что произойдет при выводе значения типа null:

Выведется просто 30.

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

Код приведет к ошибке:

Код не работает, потому что вы пытаетесь сложить вместе ящик и целое число — то есть не значение внутри ящика, а сам ящик. В этом нет смысла.

Оператор утверждения не-null значения

В самом сообщений об ошибке есть решение нашей задачи — вам сообщают о том, что nullable тип все еще внутри ящика. Требуется удалить значение из ящика.

Посмотрим, как это работает. Рассмотрим следующие объявления:

Есть два разных метода, которые вы можете использовать для nullable типов из ящика. Первым является оператор утверждения не-null значения !!, который используется следующим образом:

Данный код выводит:

Отлично! Чего и следовало ожидать.

Двойной восклицательный знак после названия переменной сообщает компилятору, что вы хотите заглянуть внутрь ящика и извлечь значение. Результатом является значение не-null типа. Это значит, что у ageAfterBirthday тип Int, а не Int?.

При использовании оператора утверждения !! будьте осторожны. Вы должны использовать не-null утверждения с умом. Чтобы понять, почему, подумайте, что произойдет, когда nullable тип не содержит значения:

Код приведет к следующей ошибке:

Исключение null указателя возникает из-за того, что переменная не содержит значения, когда вы пытаетесь ее использовать а там null. Что еще хуже, это исключение возникает во время работы, а не во время компиляции программы. Это означает, что вы заметите ошибку только в том случае, если вы выполните код с некорректным вводом. Что еще хуже, если бы этот код находился внутри приложения, исключение с null указателем привело бы к сбою всего приложения!

Как можно обезопасить себя? Для этого обратимся ко второму способу получения значения из nullable типа.

Smart casts, или умные преобразования в Kotlin

При определенных обстоятельствах можно проверить, есть ли у nullable типа значение кроме null. Если это так, можно использовать переменную таким образом, будто она не является null:

Сразу видно, что здесь при использовании 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 типа и извлечь значение, если оно там есть.

Задания для проверки

  1. Используя переменную myFavoriteSong из прошлого задания, используйте null-проверку и smart cast (умное преобразование), чтобы проверить, содержит ли переменная какое либо значение. Если да, то выведите это значение. Если нет, выведите "У меня нет любимой песни";
  2. Измените переменную myFavoriteSong на противоположность тому, чем она сейчас является. Если это null, назначьте ей строку. Если это строка, назначьте ей null. Как изменился результат вывода?

Оператор безопасного вызова в Kotlin

Предположим, вам нужно что-то сделать с nullable строкой, помимо ее вывода в терминале. К примеру, узнать длину строки. Использование умного преобразования внутри проверки значения на null является излишним для такого простого использования строки. Если вы попытаетесь получить доступ к длине строки, как если бы строка была не-nullable и без умного преобразования, вы получите ошибку от компилятора:

Само сообщений об ошибке указывает на решение, которым является использование безопасного вызова при помощи оператора ?.:

При использовании оператора безопасного вызова вы получаете доступ к свойству length.

Безопасные вызовы могут использоваться цепочкой:

Если безопасный вызов выполняется для null значения, выражение перестает выполнять цепочку и возвращает null.

Поскольку результатом безопасного вызова может быть null, выражения, использующие безопасные вызовы для nullable, возвращают nullable типы. К примеру, у используемого выше nameLength тип Int?, а не Int, даже если свойство length в строке является не-nullable. Тип всего выражения является nullable.

Функция let() в Kotlin

Оператор безопасного вызова предоставляет другой способ использования умного преобразования для работы с не-null значением внутри nullable — использование функции let() из стандартной библиотеки:

Внутри вызова функции let переменная становится не-nullable, так что вы можете получить доступ к ее свойствам без использования оператора вызова:

Подробнее о синитаксисе функции let, который называется trailing lambda, будет рассказано в будущих уроках.

Оператор Элвиса в Kotlin

Есть еще один удобный способ получить значение из nullable типа. Он используется в тех случаях, когда требуется извлечь значение из nullable типа несмотря ни на что — и в случае null будет использовать значение по умолчанию.

Это работает следующим образом:

Оператор ?:, используемый на третьей строке, называется оператором Элвиса. Причина в том, что если повернуть его на 90 градусов, форма станет похожей на знаменитую рок-звезду.

оператор элвиса

Использование оператора Элвиса означает, что переменная mustHaveResult будет соответствовать значению внутри nullableInt или 0, если переменная nullableInt содержит null. В данном примере типом переменной mustHaveResult является Int, и она содержит конкретное значение 10.

Предыдущий код использует оператор Элвиса как эквивалент следующему использованию null-проверки и умного преобразования, но в более краткой форме:

Укажем значение пременной nullableInt как null следующим образом:

Теперь переменная mustHaveResult равна 0.

Итоговые задания для проверки

Вы ознакомились с теоретической частью nullable типов в Kotlin. Пришло время проверить своим знания на практике.

Задание 1: Теперь вы компилятор

Какие из следующих утверждений правильные?

Задание 2: Разделяй и властвуй

Сначала создайте функцию, которая возвращает сколько раз целое число может быть разделено на другое целое число без остатка. Функция должна возвращать null, если результатом деления является не целое число. Назовите функцию DivideIfWhole.

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

Должно быть два случая:

  • в случаи успеха выведите "Да, число делится $answer раз";
  • в случаи неудачи, выведите на экран "Не делится :[".

Под конец проверьте свою функцию:

Подсказка 1: Используйте следующий код в качестве изначальной сигнатуры функции:

Вам нужно будет добавить возвращаемый тип, который будет 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 значений вместе с коллекциями и лямбдами.

5 6 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest
1 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Павлик
Павлик
10 месяцев назад

Отличная статья

1
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x