Что скрывается за знаком вопроса? Погружаемся в 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