Nullable в Kotlin: как работать со значениями null без боли? Полный гайд

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что скрывается за знаком вопроса? Погружаемся в nullable в Kotlin

Вы когда-нибудь писали на Java и ловили NullPointerException в самый неподходящий момент? Если да — вы знаете, как это больно. Разработчики Kotlin решили эту проблему раз и навсегда, сделав null safety не просто фичей, а философией языка. Но новичков часто пугает вопрос: "In Kotlin, what is the idiomatic way to deal with nullable values, referencing or converting them?" (именно так звучит популярнейший вопрос на Stack Overflow).

Сегодня мы разберем все идиомы работы с nullable: от простого ?. до хитрого let и магического !!. И самое главное — заглянем под капот, чтобы понять, как это работает на уровне байт-кода. Погнали!

Краткий ликбез: что такое nullable?

В Kotlin типы делятся на два лагеря: nullable (могут быть null) и non-nullable (не могут). Если вы напишете var name: String = null — компилятор выдаст ошибку. А вот var name: String? = null — ок. Вопросительный знак — это маркер, который говорит: "Эй, компилятор, я знаю, что тут может быть пусто, не ругайся".

Но как с этим работать? Просто проверять на null через if — это скучно и не по-котлиновски. Давайте посмотрим на реальные инструменты.

Спасительная троица: ?, ?: и ?.

Kotlin предлагает три кита, на которых держится вся работа с nullable. Запомните их — они сэкономят вам часы дебага.

1. Safe call operator (?.)

Это самый частый гость в вашем коде. Оператор safe call позволяет вызвать метод или свойство только в том случае, если объект не равен null. Если объект null — оператор вернет null, и никакого исключения.

val user: User? = null
val name: String? = user?.name  // name = null, никакой ошибки

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

2. Elvis operator (?:)

Элвис — ваш друг, когда нужно подставить значение по умолчанию. Если левая часть не null — берем ее, иначе — то, что справа.

val name: String? = null
val displayName: String = name ?: "Гость"  // "Гость"

Элвис можно использовать и для досрочного выхода из функции:

fun getUserName(user: User?): String {
    val name = user?.name ?: return "Аноним"
    return name
}

3. Not-null assertion (!!)

Это оператор-бунтарь. Он говорит: "Я уверен, что тут не null, и готов умереть (в смысле, выбросить NPE), если ошибаюсь". Используйте его с осторожностью, только когда 100% уверены.

val user: User? = getUserFromDatabase() // мы уверены, что он есть
val name: String = user!!.name

Под капотом !! компилируется в обычную проверку с выбросом NullPointerException при null. Ничего сложного, но опасного.

Идиоматический способ: let, run, apply и компания

Теперь переходим к самому интересному. Вопрос на Stack Overflow как раз про это: как идиоматически преобразовывать nullable значения. Ответ — функции-расширения из стандартной библиотеки.

let — король nullable преобразований

Функция let выполняет блок кода только если объект не null. При этом внутри блока объект доступен как it (или можно переименовать).

val user: User? = getUser()
user?.let { 
    println("Имя пользователя: ${it.name}")
    // Здесь it — гарантированно не null
}

Это идиоматический способ выполнить действие только при не-null значении. Без let пришлось бы писать:

if (user != null) {
    println("Имя пользователя: ${user.name}")
}

Согласитесь, let выглядит лаконичнее. А если нужно еще и преобразовать значение:

val length: Int? = nullableString?.let { it.length }

run и apply — для инициализации

Если нужно не просто выполнить действие, а еще и вернуть результат — используйте run:

val nameLength: Int? = nullableString?.run { 
    trim().length 
}

А apply хорош для настройки объекта, который может быть null:

val config: Config? = getConfig()
config?.apply {
    this.host = "localhost"
    this.port = 8080
}

Как это работает под капотом?

Давайте заглянем внутрь. Когда вы пишете:

val name: String? = "Hello"
val length: Int? = name?.let { it.length }

Компилятор Kotlin преобразует это в байт-код, который делает примерно следующее (упрощенно):

// Псевдокод на Java
String name = "Hello";
Integer length;
if (name != null) {
    length = name.length();  // внутри let
} else {
    length = null;
}

То есть let — это просто синтаксический сахар для условного блока. Но сахар очень вкусный, потому что:

  • Не нужно писать лишние if
  • Код становится цепочкой вызовов (chainable)
  • Легко читается и поддерживается

Элвис-оператор под капотом — это тернарный оператор с проверкой на null:

// Kotlin: a ?: b
// Java: a != null ? a : b

Safe call — это условный вызов метода:

// Kotlin: user?.name
// Java: (user != null) ? user.name : null

Практические примеры из реальной жизни

Пример 1: Работа с API

Допустим, вы получаете JSON с сервера, и поле email может отсутствовать.

data class UserResponse(
    val name: String?,
    val email: String?
)

fun processUser(response: UserResponse?) {
    val displayName = response?.name ?: "Безымянный"
    val emailForLog = response?.email?.let { 
        "Email: $it" 
    } ?: "Email не указан"
    
    println("$displayName, $emailForLog")
}

Пример 2: Цепочка вызовов

Представьте, что у вас есть вложенные nullable объекты.

class Address                

Блоги

Рекомендаций по книгам