Avatar ·

Лучший способ проверки на null в Kotlin: сравнение с null, безопасный вызов или элвис-оператор?

Привет, коллеги! Я недавно начал изучать Kotlin после Java и столкнулся с проблемой выбора правильного способа проверки на null. В Kotlin, как я понял, есть много разных подходов, и я запутался, какой из них считается "best practice". Например, у меня есть такой код: ```kotlin data class User(val name: String?, val email: String?) fun printUserName(user: User?) { // Вариант 1: Проверка if if (user != null) { println(user.name) } // Вариант 2: Использование ?.let user?.let { println(it.name) } // Вариант 3: Использование !! println(user!!.name) } ``` Первый вариант мне кажется слишком многословным, второй — непривычным, а третий я знаю, что использовать не стоит, так как он может выбросить NPE. Но что выбрать для разных ситуаций? Также я часто вижу в чужом коде использование элвис-оператора `?:` с `throw` или `return`. Когда это уместно? И есть ли какие-то рекомендации по производительности разных подходов? Буду благодарен за развернутый ответ с примерами, чтобы я наконец-то разобрался и перестал писать "if (x != null)" везде, как в старые добрые Java-времена. <<>>

Основные принципы 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. Это позволит компилятору выполнять смарт-каст и сделает код безопаснее.