Безопасность при работе с 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
}
Операторы 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.
С помощью функции 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
}
Некоторые функции преобразуют коллекцию с помощью лямбда-выражения и возвращают значения 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.
Изучите стандартную библиотеку 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() так, чтобы:
В переменной
validUserиспользовался операторas?для проверки, является лиuserэкземпляром классаUser. Если нет, верните пустой список.В переменной
userNameиспользовался оператор Элвиса?:, чтобы в качестве имени пользователя по умолчанию задавалось значение"Guest", если имя равноnull.В итоговом операторе return использовалась функция
.takeIf(), добавляющая настройки уведомлений по электронной почте и SMS, только если они включены.Функция
main()выполнялась без ошибок и выводила ожидаемый результат.
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))
// []
}
Упражнение 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
}
Упражнение 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]
}
Упражнение 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
}
© 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