Лучший способ проверки на null в Kotlin: сравнение с null, безопасный вызов или элвис-оператор?
Основные принципы null-безопасности в Kotlin
В Kotlin проверка на null — это не просто техническая необходимость, а часть философии языка. Главное правило: никогда не используйте !! (non-null assertion), если только вы не пишете взаимодействие с Java-кодом и на 100% уверены, что значение не null. Это самый опасный оператор, который может выбросить NullPointerException.
Рассмотрим все подходы по порядку, от самых безопасных к менее предпочтительным:
1. Безопасный вызов (?.) — основной инструмент
Используйте ?. когда вам нужно просто получить значение или вызвать метод, а null допустим как результат:
val userName = user?.name // вернет null, если user == null
println(userName ?: "Гость") // элвис-оператор для значения по умолчанию
Это аналог if (user != null) user.name else null, но гораздо короче.
2. Элвис-оператор (?:) для значений по умолчанию
Идеален, когда нужно подставить значение по умолчанию или выполнить действие при null:
val displayName = user?.name ?: "Аноним"
val email = user?.email ?: throw IllegalArgumentException("Email обязателен")
Обратите внимание: ?: с throw или return — это отличный способ раннего выхода из функции, если данные некорректны.
3. ?.let — для выполнения кода только при non-null
Используйте, когда нужно выполнить блок кода только если значение не null:
user?.let { u ->
println("Имя: ${u.name}")
println("Email: ${u.email}")
sendEmail(u.email)
}
Это чище, чем if (user != null) { ... }, особенно когда вложенность большая. Но не злоупотребляйте: для простых проверок лучше if.
4. Классический if (x != null) — когда нужно больше контроля
Не стоит избегать его полностью. Он уместен, когда:
- Нужно выполнить разный код для null и non-null (например, в
elseблоке) - Работаете с мутабельными переменными (
var), которые могут измениться между проверкой и использованием
if (user != null) {
// Компилятор смарт-кастит user к non-null
println(user.name)
} else {
println("Пользователь не найден")
}
5. requireNotNull и checkNotNull — для проверок в публичных API
Используйте в функциях, чтобы гарантировать, что аргумент не null:
fun processUser(user: User?) {
val safeUser = requireNotNull(user) { "User не должен быть null" }
// safeUser теперь non-null
}
Рекомендации по выбору
- Цепочки вызовов: всегда используйте
?.:user?.address?.city - Значение по умолчанию:
?:с константой или лямбдой - Блок кода для non-null:
?.letилиifдля сложной логики - Ранний выход при ошибке:
?: returnили?: throw - Никогда:
!!в production коде без крайней необходимости
Производительность
Все перечисленные подходы (кроме !!) компилируются в одинаковый байт-код с проверкой на null. Разница в производительности отсутствует. Выбирайте по читаемости.
Пример правильного рефакторинга вашего кода:
fun printUserName(user: User?) {
user?.name?.let { name ->
println(name)
} ?: println("Имя не указано")
}
Или проще, если нужен только вывод:
fun printUserName(user: User?) {
println(user?.name ?: "Имя не указано")
}
<<Дополнение: работа с коллекциями и расширениями
Для коллекций есть специальные функции, которые могут заменить ручные проверки на null:
val list: List<String?> = listOf("a", null, "b")
// Фильтрация null
val nonNull = list.filterNotNull() // ["a", "b"]
// Преобразование с игнорированием null
val lengths = list.mapNotNull { it?.length } // [1, 1]
// Первый не-null элемент
val first = list.firstOrNull() // "a"
Также полезно знать про scope functions для работы с nullable:
?.also— когда нужно выполнить побочное действие (например, логирование) с non-null значением?.run— когда нужно выполнить блок кода в контексте non-null объекта
user?.also {
log("Обрабатываем пользователя: $it")
}?.run {
println("Имя: $name")
}
И последнее: если вы часто пишете if (x != null) { ... } с мутабельными переменными, рассмотрите возможность использования val вместо var. Это позволит компилятору выполнять смарт-каст и сделает код безопаснее.