Null safety в Kotlin: ?, !! и Elvis operator

Null safety в Kotlin нужен, чтобы ошибка с null была видна в типе, а не всплывала случайным падением в runtime. В этом уроке разберем String?, safe call ?., Elvis operator ?:, опасный !! и практическое правило: если хочется поставить !!, сначала подумайте, почему компилятор вам не верит

К концу урока вы напишете код, который безопасно обрабатывает отсутствующее имя пользователя и не падает на пустом значении

Что получится в конце

Пример:

fun userLabel(name: String?): String {
    val cleanName = name?.trim()
    val visibleName = cleanName?.takeIf { it.isNotEmpty() } ?: "Guest"

    return "User: $visibleName"
}

fun main() {
    println(userLabel(" Dinar "))
    println(userLabel(null))
    println(userLabel("   "))
}

Ожидаемый вывод:

User: Dinar
User: Guest
User: Guest

Здесь name может быть null, но программа не падает и дает понятный запасной вариант

Nullable и non-nullable типы

Обычный String не может хранить null:

val name: String = "Kotlin"

Nullable-строка пишется с вопросительным знаком:

val name: String? = null

Этот ? — не украшение. Он меняет тип. Kotlin теперь знает, что в переменной может быть значение, а может быть null

Если попробовать вызвать:

name.length

компилятор остановит код. Он не знает, есть ли внутри строка. Сначала нужно обработать null

Safe call ?

Safe call:

val length = name?.length

означает: если name не null, взять длину, иначе вернуть null. Тип length будет Int?, потому что результата тоже может не быть

Цепочки safe call удобны, когда есть вложенные объекты:

val cityName = user?.profile?.city?.name

Если на любом шаге встретится null, вся цепочка вернет null, а не упадет

Но не превращайте safe call в способ замолчать все проблемы. Если значение обязательно для бизнес-логики, лучше явно показать ошибку или вернуть понятный fallback

Elvis operator ?

Elvis operator дает значение по умолчанию:

val visibleName = name ?: "Guest"

Если name не null, будет использовано оно. Если null, будет "Guest"

С safe call это особенно удобно:

val length = name?.length ?: 0

Теперь length имеет тип Int, а не Int?, потому что на случай null у нас есть 0

В реальном коде fallback должен быть осмысленным. Для имени "Guest" подходит. Для цены 0 может быть опасным решением, если ноль изменит бизнес-смысл

Почему !! опасен

Оператор:

name!!.length

говорит компилятору: «поверь мне, здесь не null». Если значение все-таки null, программа получит NullPointerException

!! иногда нужен на границе с Java или старым API, где вы точно знаете больше компилятора. Но в учебном и прикладном коде его часто используют как пластырь. Это плохая привычка

Перед !! задайте три вопроса:

  1. Можно ли проверить null через if
  2. Можно ли использовать ?. и ?:
  3. Можно ли изменить тип или API так, чтобы null не приходил сюда

Если ответ «да», !! не нужен

Smart cast через if

Kotlin умеет сужать nullable-тип после проверки:

fun printName(name: String?) {
    if (name != null) {
        println(name.length)
    }
}

Внутри if компилятор понимает, что name уже не null. Это называется smart cast

Но smart cast работает не во всех случаях. Если значение может измениться между проверкой и использованием, компилятор не будет рисковать. Тогда лучше сохранить результат в локальный val

let для действия только при наличии значения

Если нужно выполнить действие только когда значение есть:

val email: String? = "user@example.com"

email?.let {
    println("Send email to $it")
}

Блок let выполнится только для non-null значения. Внутри it имеет обычный non-null тип

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

Домашка: безопасный профиль

Создайте функцию:

fun profileSummary(name: String?, city: String?): String

Правила:

  • если имени нет или оно пустое, использовать "Guest"
  • если города нет или он пустой, использовать "unknown city"
  • результат должен быть строкой вида Guest from unknown city

Подсказка: используйте trim, takeIf, ?. и ?:

Nullable-поля в data class

Null safety особенно хорошо видна в моделях данных:

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

fun contactLabel(user: User): String {
    val email = user.email ?: "no email"
    return "${user.name}: $email"
}

name обязателен, а email может отсутствовать. Это видно прямо в типах. Читателю кода не нужно угадывать, можно ли получить null

Если поле nullable, обработайте его рядом с использованием. Не передавайте String? через пять функций только для того, чтобы в самом конце поставить !!

requireNotNull и понятная ошибка

Иногда значение действительно обязательно, и без него продолжать нельзя. Вместо голого !! можно использовать:

val token = requireNotNull(inputToken) {
    "Token is required"
}

Так код все равно упадет при null, но сообщение будет понятнее. Это лучше для внутренних инвариантов, где отсутствие значения означает ошибку программиста или неправильную конфигурацию

Но для пользовательского ввода requireNotNull не всегда уместен. Если человек может не заполнить поле, лучше вернуть сообщение, подсветить форму или дать fallback, а не падать

Домашка: безопасная скидка

Создайте функцию:

fun discountLabel(percent: Int?): String

Правила:

  • если percent == null, вернуть "No discount"
  • если percent <= 0, вернуть "No discount"
  • иначе вернуть "Discount: 15%" с реальным числом

Подсказка:

val safePercent = percent?.takeIf { it > 0 }

Дальше используйте Elvis operator. Проверьте null, 0, 15, -5

Как не прятать ошибку пустой строкой

Иногда хочется написать:

val name = inputName ?: ""

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

Частые ошибки и порядок проверки

Ставите !! вместо проверки Почти всегда сначала попробуйте ?., ?: или if (value != null). !! оставляйте для редких ситуаций, где падение действительно допустимо

Думаете, что String и String? одно и то же Это разные типы. String? требует обработки отсутствующего значения

Даете опасный fallback ?: 0 может быть нормальным для длины строки, но опасным для цены, баланса или количества товара

Злоупотребляете let let удобен для короткого действия. Для сложной логики обычный if часто читается лучше

Мини-чек для ревью nullable-кода

Когда видите nullable-значение, проверьте три вещи. Первое: действительно ли значение может отсутствовать, или тип String? появился «на всякий случай». Второе: где отсутствие значения превращается в понятное решение. Третье: нет ли рядом случайного !!

Пример хорошего места обработки:

val visibleName = name?.trim()?.takeIf { it.isNotEmpty() } ?: "Guest"

Пример подозрительного места:

println(name!!.length)

Если !! стоит в коде пользовательского ввода, формы, API или Java interop, почти всегда стоит остановиться и написать более честную обработку

Что может быть еще интересно по этой теме

Null safety полностью убирает NullPointerException? Нет. !!, Java interop, lateinit и некоторые ошибки инициализации могут привести к NPE. Но обычный Kotlin-код становится заметно безопаснее

Почему Java-значения иногда странные? На границе с Java Kotlin видит platform types. Компилятор не всегда знает, может ли значение быть null, поэтому там нужна дополнительная аккуратность

Когда использовать nullable, а когда пустую строку? Если значения действительно нет, используйте null. Если значение есть, но оно пустое, это другой случай. Не смешивайте смысл

Что дальше после null safety? Функции и data class. Там nullable-типы сразу пригодятся для полей, параметров и безопасной обработки пользовательских данных

Что почитать дальше по Kotlin

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x