3.3.8 空安全
原文链接: https://kotlinlang.org/docs/kotlin-tour-intermediate-null-safety.html
3.3.8 空安全
在入门导览中,你学会了如何处理代码中的 null 值。本章介绍空安全特性的常见用法,以及如何充分利用它们。
智能转换与安全转换
Kotlin 有时可以在没有显式声明的情况下推断出类型。当你让 Kotlin 把某个变量或对象当作属于某种特定类型来处理时,这个过程称为类型转换。当类型被自动转换(例如通过推断)时,称为智能转换。
is 与 !is 运算符
在探讨类型转换的工作方式之前,我们先看看如何检查一个对象是否具有某种类型。为此,你可以在 when 或 if 条件表达式中使用 is 和 !is 运算符:
is 检查对象是否具有该类型,并返回一个布尔值。!is 检查对象是否不具有该类型,并返回一个布尔值。
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| 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 运算符,其中也包括从可空类型转换为对应的非空类型。如果转换无法完成,程序会在运行时崩溃。这正是它被称为不安全转换运算符的原因。
1
2
3
4
5
6
7
| fun main() {
val a: String? = null
val b = a as String
// Triggers an error at runtime
print(b)
}
|
要把对象显式转换为非空类型,但在失败时返回 null 而不是抛出错误,请使用 as? 运算符。由于 as? 运算符在失败时不会触发错误,它被称为安全运算符。
1
2
3
4
5
6
7
8
| fun main() {
val a: String? = null
val b = a as? String
// Returns null value
print(b)
// null
}
|
你可以把 as? 运算符与 Elvis 运算符 ?: 结合使用,把好几行代码缩减为一行。例如,下面这个 calculateTotalStringLength() 函数会计算混合列表中所提供的所有字符串的总长度:
1
2
3
4
5
6
7
8
9
10
11
12
13
| 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 循环遍历列表中的每个项目。 - 用
if 和 is 运算符检查当前项目是否是字符串: - 如果是,就把该字符串的长度加到计数器上。
- 如果不是,计数器不增加。
- 返回
totalLength 变量的最终值。
这段代码可以缩减为:
1
2
3
| fun calculateTotalStringLength(items: List<Any>): Int {
return items.sumOf { (it as? String)?.length ?: 0 }
}
|
这个示例使用 .sumOf() 扩展函数,并提供一个 lambda 表达式,它:
- 对列表中的每个项目使用
as? 执行到 String 的安全转换。 - 如果调用没有返回
null 值,就用安全调用 ?. 访问 length 属性。 - 如果安全调用返回
null 值,就用 Elvis 运算符 ?: 返回 0。
null 值与集合
在 Kotlin 中,处理集合往往涉及处理 null 值以及过滤掉不需要的元素。在处理列表、集、映射和其他类型的集合时,Kotlin 提供了一些实用的函数,让你能写出干净、高效且空安全的代码。
要从列表中过滤掉 null 值,请使用 filterNotNull() 函数:
1
2
3
4
5
6
7
8
| fun main() {
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]
}
|
如果你想在创建列表时就直接过滤 null 值,请使用 listOfNotNull() 函数:
1
2
3
4
5
6
7
8
9
10
11
12
| fun main() {
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]
}
|
在上面这两个示例中,如果所有项目都是 null 值,就会返回一个空列表。
Kotlin 还提供了一些可以在集合中查找值的函数。如果找不到值,它们会返回 null 值,而不是触发错误:
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| fun main() {
// 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
}
|
这个示例使用 Elvis 运算符 ?:,在函数返回 null 值时返回一句打印内容。
注意: maxOrNull() 和 minOrNull() 函数是为不包含 null 值的集合设计的。否则,你无法判断是函数找不到想要的值,还是它找到了一个 null 值。
你可以把 singleOrNull() 函数与 lambda 表达式一起使用,来查找符合某个条件的单个项目。如果不存在这样的项目,或者存在多个符合条件的项目,函数会返回 null 值:
1
2
3
4
5
6
7
8
9
| fun main() {
// 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
}
|
注意: singleOrNull() 函数是为不包含 null 值的集合设计的。
有些函数使用 lambda 表达式转换集合,并在无法完成其用途时返回 null 值。
要用 lambda 表达式转换集合并返回第一个不为 null 的值,请使用 firstNotNullOfOrNull() 函数。如果不存在这样的值,函数会返回 null 值:
1
2
3
4
5
6
7
8
9
10
11
12
13
| fun main() {
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
}
|
要用 lambda 表达式依次处理集合中的每个项目并生成一个累积值(如果集合为空则返回 null 值),请使用 reduceOrNull() 函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| fun main() {
// 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
}
|
这个示例同样使用 Elvis 运算符 ?:,在函数返回 null 值时返回一句打印内容。
注意: reduceOrNull() 函数是为不包含 null 值的集合设计的。
浏览 Kotlin 的标准库,可以发现更多能让代码更安全的函数。
提前返回与 Elvis 运算符
在入门导览中,你学会了如何使用提前返回让函数在某个点之后不再继续处理。你可以把 Elvis 运算符 ?: 与提前返回结合使用,在函数中检查前置条件。这种方式是保持代码简洁的好办法,因为不需要使用嵌套检查。代码复杂度的降低也让维护更容易。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| 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 实例的映射和一个整数形式的用户 ID。 - 用提供的用户 ID 访问
User 实例映射中的值。 - 如果映射中的值是
null 值,就用 Elvis 运算符带着 -1 提前返回。 - 把从映射中找到的值赋给
user 变量。 - 使用
size 属性返回该用户好友列表中的好友数量。 main() 函数:- 创建三个
User 实例。 - 创建这些
User 实例的映射并赋给 users 变量。 - 用值
1 和 2 在 users 变量上调用 getNumberOfFriends() 函数,分别返回 "Alice" 的两个好友和 "Bob" 的一个好友。 - 用值
4 在 users 变量上调用 getNumberOfFriends() 函数,触发带着 -1 的提前返回。
你可能会注意到,去掉提前返回后代码可以更简洁。不过这种方式需要多次安全调用,因为 users[userId] 可能返回 null 值,这会让代码稍难阅读:
1
2
3
4
| fun getNumberOfFriends(users: Map<Int, User>, userId: Int): Int {
// Retrieve the user or return -1 if not found
return users[userId]?.friends?.size ?: -1
}
|
虽然这个示例只用 Elvis 运算符检查了一个条件,但你可以添加多个检查来覆盖任何关键的错误路径。使用 Elvis 运算符配合提前返回,能避免程序做无用的工作,并在检测到 null 值或无效情况时立即停止,从而让代码更安全。
关于在代码中使用 return 的更多信息,请参阅返回与跳转。
练习
习题 1
你正在为一个应用开发通知系统,用户可以启用或停用不同类型的通知。请补全 getNotificationPreferences() 函数,使得:
validUser 变量使用 as? 运算符检查 user 是否为 User 类的实例。如果不是,返回空列表。userName 变量使用 Elvis 运算符 ?:,确保当用户名为 null 时默认为 "Guest"。- 最后的 return 语句使用
.takeIf() 函数,仅在启用时包含邮件和短信通知偏好。 main() 函数能够正常运行并打印出预期输出。
提示: 如果给定条件为真,takeIf() 函数返回原值,否则返回 null。例如:kotlin 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 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| 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))
// []
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| 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 值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| 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
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| 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
}
|
示例解答 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| 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
}
|
示例解答 2
习题 3
你正在开发一个社交媒体平台,用户有用户名和账号状态。你想看到当前活跃用户名的列表。请补全 getActiveUsernames() 函数,让 mapNotNull() 函数的判断条件在用户活跃时返回用户名、不活跃时返回 null 值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| 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() 函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| 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]
}
|
示例解答 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| 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]
}
|
示例解答 2
习题 4
你正在为一个电商平台开发库存管理系统。在处理销售之前,你需要根据可用库存检查某个商品请求的数量是否有效。
请补全 validateStock() 函数,让它使用提前返回和 Elvis 运算符(在适用时)检查:
requested 变量是否为 null。available 变量是否为 null。requested 变量是否为负数。requested 变量中的数量是否大于 available 变量中的数量。
在上述所有情况中,函数都必须带着值 -1 提前返回。
1
2
3
4
5
6
7
8
9
10
11
12
| 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
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| 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
}
|
示例解答
另请参阅
上一步
下一步