Spec-Zone.ru › Kotlin 2

Безопасность при работе с null

В вводном курсе вы узнали, как обрабатывать значения null в коде. В этой главе рассматриваются распространённые сценарии использования средств обеспечения безопасности при работе с null и способы извлечь из них максимум пользы.

Умные и безопасные приведения типов

Иногда Kotlin может определить тип без явного объявления. Когда вы указываете Kotlin считать, что переменная или объект принадлежит определённому типу, этот процесс называется приведением типа. Если тип приводится автоматически, например когда он определяется, такое приведение называется умным приведением типа.

Операторы is и !is

Прежде чем рассмотреть, как работает приведение типов, посмотрим, как проверить, принадлежит ли объект определённому типу. Для этого можно использовать операторы is и !is в условных выражениях when или if:

  • is проверяет, принадлежит ли объект этому типу, и возвращает логическое значение.

  • !is проверяет, не принадлежит ли объект этому типу, и возвращает логическое значение.

Например:

fun printObjectType(obj: Any) {
    when (obj) {
        is Int -> println("It's an Integer with value $obj")
        !is Double -> println("It's NOT a Double")
        else -> println("Unknown type")
    }
}

fun main() {
    val myInt = 42
    val myDouble = 3.14
    val myList = listOf(1, 2, 3)
  
    // The type is Int
    printObjectType(myInt)
    // It's an Integer with value 42

    // The type is List, so it's NOT a Double.
    printObjectType(myList)
    // It's NOT a Double

    // The type is Double, so the else branch is triggered.
    printObjectType(myDouble)
    // Unknown type
}

Вы уже видели пример использования условного выражения when с операторами is и !is в главе Открытые и другие специальные классы.

Операторы as и as?

Чтобы явно привести объект к другому типу, используйте оператор as. Это включает приведение nullable-типа к соответствующему non-nullable-типу. Если выполнить приведение невозможно, программа аварийно завершится во время выполнения. Поэтому этот оператор называется оператором небезопасного приведения.

fun main() {
//sampleStart
    val a: String? = null
    val b = a as String

    // Triggers an error at runtime
    print(b)
//sampleEnd
}

Чтобы явно привести объект к non-nullable-типу и при этом вернуть null, а не выбросить ошибку при неудаче, используйте оператор as?. Поскольку оператор as? не вызывает ошибку при неудаче, его называют оператором безопасного приведения.

fun main() {
//sampleStart
    val a: String? = null
    val b = a as? String

    // Returns null value
    print(b)
    // null
//sampleEnd
}

Можно объединить оператор as? с оператором Элвиса ?:, чтобы сократить несколько строк кода до одной. Например, следующая функция calculateTotalStringLength() вычисляет суммарную длину всех строк в смешанном списке:

fun calculateTotalStringLength(items: List<Any>): Int {
    var totalLength = 0

    for (item in items) {
        totalLength += if (item is String) {
            item.length
        } else {
            0  // Add 0 for non-String items
        }
    }

    return totalLength
}

В этом примере:

  • Переменная totalLength используется как счётчик.

  • Цикл for перебирает каждый элемент списка.

  • Оператор is используется вместе с if, чтобы проверить, является ли текущий элемент строкой:

    • Если да, длина строки добавляется к счётчику.

    • Если нет, значение счётчика не увеличивается.

  • Возвращается итоговое значение переменной totalLength.

Этот код можно сократить до:

fun calculateTotalStringLength(items: List<Any>): Int {
    return items.sumOf { (it as? String)?.length ?: 0 }
}

В примере используется функция-расширение .sumOf() и лямбда-выражение, которое:

  • Для каждого элемента списка выполняет безопасное приведение к String с помощью as?.

  • Использует безопасный вызов ?. для доступа к свойству length, если вызов не возвращает значение null.

  • Использует оператор Элвиса ?:, чтобы вернуть 0, если безопасный вызов возвращает значение null.

Значения null и коллекции

В Kotlin работа с коллекциями часто связана с обработкой значений null и отфильтровыванием ненужных элементов. В Kotlin есть полезные функции, которые позволяют писать чистый, эффективный и безопасный при работе с null код для списков, множеств, отображений и других типов коллекций.

Чтобы отфильтровать значения null из списка, используйте функцию filterNotNull():

fun main() {
//sampleStart
    val emails: List<String?> = listOf("alice@example.com", null, "bob@example.com", null, "carol@example.com")

    val validEmails = emails.filterNotNull()

    println(validEmails)
    // [alice@example.com, bob@example.com, carol@example.com]
//sampleEnd
}

Если нужно отфильтровать значения null непосредственно при создании списка, используйте функцию listOfNotNull():

fun main() {
//sampleStart
    val serverConfig = mapOf(
        "appConfig.json" to "App Configuration",
        "dbConfig.json" to "Database Configuration"
    )

    val requestedFile = "appConfig.json"
    val configFiles = listOfNotNull(serverConfig[requestedFile])

    println(configFiles)
    // [App Configuration]
//sampleEnd
}

В обоих примерах, если все элементы — значения null, возвращается пустой список.

В Kotlin также есть функции для поиска значений в коллекциях. Если значение не найдено, они возвращают значения null, а не вызывают ошибку:

  • Функция maxOrNull() находит наибольшее значение. Если такого значения нет, возвращает значение null.

  • Функция minOrNull() находит наименьшее значение. Если такого значения нет, возвращает значение null.

Например:

fun main() {
//sampleStart
    // Temperatures recorded over a week
    val temperatures = listOf(15, 18, 21, 21, 19, 17, 16)
  
    // Find the highest temperature of the week
    val maxTemperature = temperatures.maxOrNull()
    println("Highest temperature recorded: ${maxTemperature ?: "No data"}")
    // Highest temperature recorded: 21

    // Find the lowest temperature of the week
    val minTemperature = temperatures.minOrNull()
    println("Lowest temperature recorded: ${minTemperature ?: "No data"}")
    // Lowest temperature recorded: 15
//sampleEnd
}

В этом примере оператор Элвиса ?: используется для вывода сообщения, если функции возвращают значение null.

Функции maxOrNull() и minOrNull() предназначены для работы с коллекциями, которые не содержат значения null. В противном случае невозможно определить, не удалось ли функции найти нужное значение или же она нашла значение null.

С помощью функции singleOrNull() и лямбда-выражения можно найти единственный элемент, соответствующий условию. Если такого элемента нет или условию соответствуют несколько элементов, функция возвращает значение null:

fun main() {
//sampleStart
    // Temperatures recorded over a week
    val temperatures = listOf(15, 18, 21, 21, 19, 17, 16)

    // Check if there was exactly one day with 30 degrees
    val singleHotDay = temperatures.singleOrNull{ it == 30 }
    println("Single hot day with 30 degrees: ${singleHotDay ?: "None"}")
    // Single hot day with 30 degrees: None
//sampleEnd
}

Функция singleOrNull() предназначена для работы с коллекциями, которые не содержат значения null.

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

Чтобы преобразовать коллекцию с помощью лямбда-выражения и вернуть первое значение, которое не является null, используйте функцию firstNotNullOfOrNull(). Если такого значения нет, функция возвращает значение null:

fun main() {
//sampleStart
    data class User(val name: String?, val age: Int?)

    val users = listOf(
        User(null, 25),
        User("Alice", null),
        User("Bob", 30)
    )

    val firstNonNullName = users.firstNotNullOfOrNull { it.name }
    println(firstNonNullName)
    // Alice
//sampleEnd
}

Чтобы последовательно обработать каждый элемент коллекции с помощью лямбда-выражения и получить накопленное значение (или вернуть значение null, если коллекция пуста), используйте функцию reduceOrNull():

fun main() {
//sampleStart
    // Prices of items in a shopping cart
    val itemPrices = listOf(20, 35, 15, 40, 10)

    // Calculate the total price using the reduceOrNull() function
    val totalPrice = itemPrices.reduceOrNull { runningTotal, price -> runningTotal + price }
    println("Total price of items in the cart: ${totalPrice ?: "No items"}")
    // Total price of items in the cart: 120

    val emptyCart = listOf<Int>()
    val emptyTotalPrice = emptyCart.reduceOrNull { runningTotal, price -> runningTotal + price }
    println("Total price of items in the empty cart: ${emptyTotalPrice ?: "No items"}")
    // Total price of items in the empty cart: No items
//sampleEnd
}

В этом примере также используется оператор Элвиса ?: для вывода сообщения, если функция возвращает значение null.

Функция reduceOrNull() предназначена для работы с коллекциями, которые не содержат значения null.

Изучите стандартную библиотеку Kotlin, чтобы найти другие функции, которые помогут сделать код безопаснее.

Досрочный возврат и оператор Элвиса

В вводном курсе вы узнали, как использовать досрочный возврат, чтобы функция не выполнялась дальше определённого места. Оператор Элвиса ?: можно использовать вместе с досрочным возвратом для проверки предусловий функции. Этот подход позволяет писать более лаконичный код, поскольку не нужно использовать вложенные проверки. Более простую структуру кода также легче поддерживать. Например:

data class User(
    val id: Int,
    val name: String,
    // List of friend user IDs
    val friends: List<Int>
)

// Function to get the number of friends for a user
fun getNumberOfFriends(users: Map<Int, User>, userId: Int): Int {
    // Retrieves the user or return -1 if not found
    val user = users[userId] ?: return -1
    // Returns the number of friends
    return user.friends.size
}

fun main() {
    // Creates some sample users
    val user1 = User(1, "Alice", listOf(2, 3))
    val user2 = User(2, "Bob", listOf(1))
    val user3 = User(3, "Charlie", listOf(1))

    // Creates a map of users
    val users = mapOf(1 to user1, 2 to user2, 3 to user3)

    println(getNumberOfFriends(users, 1))
    // 2
    println(getNumberOfFriends(users, 2))
    // 1
    println(getNumberOfFriends(users, 4))
    // -1
}

В этом примере:

  • Объявлен класс данных User со свойствами для id пользователя, его name и списка друзей.

  • Функция getNumberOfFriends():

    • Принимает отображение экземпляров User и идентификатор пользователя в виде целого числа.

    • Получает значение из отображения экземпляров User по указанному идентификатору пользователя.

    • Использует оператор Элвиса для досрочного возврата из функции со значением -1, если значение в отображении равно null.

    • Присваивает найденное в отображении значение переменной user.

    • Возвращает количество друзей в списке друзей пользователя с помощью свойства size.

  • Функция main():

    • Создаёт три экземпляра User.

    • Создаёт отображение этих экземпляров User и присваивает его переменной users.

    • Вызывает функцию getNumberOfFriends() для переменной users со значениями 1 и 2. Для "Alice" функция возвращает двух друзей, а для "Bob" — одного.

    • Вызывает функцию getNumberOfFriends() для переменной users со значением 4, что приводит к досрочному возврату со значением -1.

Возможно, вы заметили, что без досрочного возврата код можно сделать лаконичнее. Однако для такого подхода требуется несколько безопасных вызовов, поскольку users[userId] может вернуть значение null, из-за чего код становится немного сложнее для чтения:

fun getNumberOfFriends(users: Map<Int, User>, userId: Int): Int {
    // Retrieve the user or return -1 if not found
    return users[userId]?.friends?.size ?: -1
}

Хотя в этом примере оператор Элвиса проверяет только одно условие, можно добавить несколько проверок, охватывающих все критические сценарии ошибок. Досрочный возврат с оператором Элвиса позволяет программе не выполнять лишнюю работу и делает код безопаснее: выполнение останавливается сразу при обнаружении значения null или недопустимого случая.

Дополнительные сведения об использовании return в коде см. в разделе Возвраты и переходы.

Практика

Упражнение 1

Вы разрабатываете систему уведомлений для приложения, в котором пользователи могут включать или отключать разные типы уведомлений. Дополните функцию getNotificationPreferences() так, чтобы:

  1. В переменной validUser использовался оператор as? для проверки, является ли user экземпляром класса User. Если нет, верните пустой список.

  2. В переменной userName использовался оператор Элвиса ?:, чтобы в качестве имени пользователя по умолчанию задавалось значение "Guest", если имя равно null.

  3. В итоговом операторе return использовалась функция .takeIf(), добавляющая настройки уведомлений по электронной почте и SMS, только если они включены.

  4. Функция main() выполнялась без ошибок и выводила ожидаемый результат.

Функция takeIf() возвращает исходное значение, если заданное условие истинно, а в противном случае возвращает null. Например:

fun main() {
    // The user is logged in
    val userIsLoggedIn = true
    // The user has an active session
    val hasSession = true

    // Gives access to the dashboard if the user is logged in
    // and has an active session
    val canAccessDashboard = userIsLoggedIn.takeIf { hasSession }

    println(canAccessDashboard ?: "Access denied")
    // true
}
data class User(val name: String?)

fun getNotificationPreferences(user: Any, emailEnabled: Boolean, smsEnabled: Boolean): List<String> {
    val validUser = // Write your code here
    val userName = // Write your code here

    return listOfNotNull( /* Write your code here */)
}

fun main() {
    val user1 = User("Alice")
    val user2 = User(null)
    val invalidUser = "NotAUser"

    println(getNotificationPreferences(user1, emailEnabled = true, smsEnabled = false))
    // [Email Notifications enabled for Alice]
    println(getNotificationPreferences(user2, emailEnabled = false, smsEnabled = true))
    // [SMS Notifications enabled for Guest]
    println(getNotificationPreferences(invalidUser, emailEnabled = true, smsEnabled = true))
    // []
}
data class User(val name: String?) fun getNotificationPreferences(user: Any, emailEnabled: Boolean, smsEnabled: Boolean): List<String> { val validUser = user as? User ?: return emptyList() val userName = validUser.name ?: "Guest" return listOfNotNull( "Email Notifications enabled for $userName".takeIf { emailEnabled }, "SMS Notifications enabled for $userName".takeIf { smsEnabled } ) } fun main() { val user1 = User("Alice") val user2 = User(null) val invalidUser = "NotAUser" println(getNotificationPreferences(user1, emailEnabled = true, smsEnabled = false)) // [Email Notifications enabled for Alice] println(getNotificationPreferences(user2, emailEnabled = false, smsEnabled = true)) // [SMS Notifications enabled for Guest] println(getNotificationPreferences(invalidUser, emailEnabled = true, smsEnabled = true)) // [] }

Упражнение 2

Вы работаете над потоковым сервисом по подписке, в котором у пользователей может быть несколько подписок, но активной может быть только одна. Дополните функцию getActiveSubscription() так, чтобы она использовала функцию singleOrNull() с предикатом и возвращала значение null, если активных подписок больше одной:

data class Subscription(val name: String, val isActive: Boolean)

fun getActiveSubscription(subscriptions: List<Subscription>): Subscription? // Write your code here

fun main() {
    val userWithPremiumPlan = listOf(
        Subscription("Basic Plan", false),
        Subscription("Premium Plan", true)
    )

    val userWithConflictingPlans = listOf(
        Subscription("Basic Plan", true),
        Subscription("Premium Plan", true)
    )

    println(getActiveSubscription(userWithPremiumPlan))
    // Subscription(name=Premium Plan, isActive=true)

    println(getActiveSubscription(userWithConflictingPlans))
    // null
}
data class Subscription(val name: String, val isActive: Boolean) fun getActiveSubscription(subscriptions: List<Subscription>): Subscription? { return subscriptions.singleOrNull { subscription -> subscription.isActive } } fun main() { val userWithPremiumPlan = listOf( Subscription("Basic Plan", false), Subscription("Premium Plan", true) ) val userWithConflictingPlans = listOf( Subscription("Basic Plan", true), Subscription("Premium Plan", true) ) println(getActiveSubscription(userWithPremiumPlan)) // Subscription(name=Premium Plan, isActive=true) println(getActiveSubscription(userWithConflictingPlans)) // null }
data class Subscription(val name: String, val isActive: Boolean) fun getActiveSubscription(subscriptions: List<Subscription>): Subscription? = subscriptions.singleOrNull { it.isActive } fun main() { val userWithPremiumPlan = listOf( Subscription("Basic Plan", false), Subscription("Premium Plan", true) ) val userWithConflictingPlans = listOf( Subscription("Basic Plan", true), Subscription("Premium Plan", true) ) println(getActiveSubscription(userWithPremiumPlan)) // Subscription(name=Premium Plan, isActive=true) println(getActiveSubscription(userWithConflictingPlans)) // null }

Упражнение 3

Вы работаете над платформой социальных сетей, где у пользователей есть имена пользователей и статусы учётных записей. Нужно получить список имён пользователей, активных в данный момент. Дополните функцию getActiveUsernames() так, чтобы функция mapNotNull() использовала предикат, который возвращает имя пользователя, если он активен, или значение null, если он неактивен:

data class User(val username: String, val isActive: Boolean)

fun getActiveUsernames(users: List<User>): List<String> {
    return users.mapNotNull { /* Write your code here */ }
}

fun main() {
    val allUsers = listOf(
        User("alice123", true),
        User("bob_the_builder", false),
        User("charlie99", true)
    )

    println(getActiveUsernames(allUsers))
    // [alice123, charlie99]
}

Как и в упражнении 1, при проверке активности пользователя можно использовать функцию takeIf().

data class User(val username: String, val isActive: Boolean) fun getActiveUsernames(users: List<User>): List<String> { return users.mapNotNull { user -> if (user.isActive) user.username else null } } fun main() { val allUsers = listOf( User("alice123", true), User("bob_the_builder", false), User("charlie99", true) ) println(getActiveUsernames(allUsers)) // [alice123, charlie99] }
data class User(val username: String, val isActive: Boolean) fun getActiveUsernames(users: List<User>): List<String> = users.mapNotNull { user -> user.username.takeIf { user.isActive } } fun main() { val allUsers = listOf( User("alice123", true), User("bob_the_builder", false), User("charlie99", true) ) println(getActiveUsernames(allUsers)) // [alice123, charlie99] }

Упражнение 4

Вы работаете над системой управления запасами для платформы электронной коммерции. Перед обработкой продажи нужно проверить, допустимо ли запрошенное количество товара с учётом имеющихся запасов.

Дополните функцию validateStock(), используя досрочный возврат и оператор Элвиса (где применимо), чтобы проверить, что:

  • Переменная requested равна null.

  • Переменная available равна null.

  • Переменная requested содержит отрицательное значение.

  • Значение переменной requested больше значения переменной available.

Во всех перечисленных случаях функция должна выполнить досрочный возврат со значением -1.

fun validateStock(requested: Int?, available: Int?): Int {
    // Write your code here
}

fun main() {
    println(validateStock(5,10))
    // 5
    println(validateStock(null,10))
    // -1
    println(validateStock(-2,10))
    // -1
}
fun validateStock(requested: Int?, available: Int?): Int { val validRequested = requested ?: return -1 val validAvailable = available ?: return -1 if (validRequested < 0) return -1 if (validRequested > validAvailable) return -1 return validRequested } fun main() { println(validateStock(5,10)) // 5 println(validateStock(null,10)) // -1 println(validateStock(-2,10)) // -1 }
  • Предыдущий шаг

  • Следующий шаг

1 июля 2026 г.
СвойстваБиблиотеки и API

© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/kotlin-tour-intermediate-null-safety.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API