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 值,而不是触发错误:

  • maxOrNull() 查找最大值。如果不存在,返回 null 值。
  • minOrNull() 查找最小值。如果不存在,返回 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() 函数,使得:

  1. validUser 变量使用 as? 运算符检查 user 是否为 User 类的实例。如果不是,返回空列表。
  2. userName 变量使用 Elvis 运算符 ?:,确保当用户名为 null 时默认为 "Guest"。
  3. 最后的 return 语句使用 .takeIf() 函数,仅在启用时包含邮件和短信通知偏好。
  4. 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
}

示例解答

另请参阅

上一步

下一步