進階:Null 安全
擴充函式
作用域函式
帶有接收者的 Lambda 運算式
類別與介面
物件
Open 與特殊類別
屬性
Null 安全
程式庫與 API
在初學者導覽中,您學習了如何在程式碼中處理 null 值。本章節將介紹 Null 安全特性的常見使用案例,以及如何充分利用這些特性。
智慧轉換與安全轉換
Kotlin 有時可以在沒有顯式宣告的情況下推斷型別。當您告訴 Kotlin 將某個變數或物件視為屬於特定型別時,這個過程稱為 轉換 (casting)。當型別被自動轉換時(例如被推斷時),則稱為 智慧轉換 (smart casting)。
is 與 !is 運算子
在探索轉換如何運作之前,讓我們先看看如何檢查物件是否具有特定型別。為此,您可以在 when 或 if 條件運算式中使用 is 和 !is 運算子:
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)
// 型別為 Int
printObjectType(myInt)
// It's an Integer with value 42
// 型別為 List,因此它不是 Double。
printObjectType(myList)
// It's NOT a Double
// 型別為 Double,因此觸發 else 分支。
printObjectType(myDouble)
// Unknown type
}您已經在 Open 與其他特殊類別 章節中看過如何在
when條件運算式中使用is和!is運算子的範例。
as 與 as? 運算子
要將物件顯式 轉換 為任何其他型別,請使用 as 運算子。這包括從可為 null 型別轉換為其對應的不可為 null 型別。如果無法轉換,程式會在 執行時 (runtime) 崩潰。這就是為什麼它被稱為 非安全 轉換運算子。
fun main() {
val a: String? = null
val b = a as String
// 在執行時觸發錯誤
print(b)
}若要將物件顯式轉換為不可為 null 型別,但在失敗時傳回 null 而不拋出錯誤,請使用 as? 運算子。由於 as? 運算子在失敗時不會觸發錯誤,因此它被稱為 安全 運算子。
fun main() {
val a: String? = null
val b = a as? String
// 傳回 null 值
print(b)
// null
}您可以將 as? 運算子與 Elvis 運算子 ?: 結合使用,將幾行程式碼縮減為一行。例如,下方的 calculateTotalStringLength() 函式會計算混合列表中提供的所有字串總長度:
fun calculateTotalStringLength(items: List<Any>): Int {
var totalLength = 0
for (item in items) {
totalLength += if (item is String) {
item.length
} else {
0 // 對於非字串項目加 0
}
}
return totalLength
}這個範例:
- 使用
totalLength變數作為計數器。 - 使用
for迴圈來遍歷列表中的每個項目。 - 使用
if和is運算子來檢查目前項目是否為字串:- 如果是,則將字串長度加到計數器。
- 如果不是,則計數器不遞增。
- 傳回
totalLength變數的最終值。
這段程式碼可以縮減為:
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 擁有一些實用的函式,讓您在處理 List、Set、Map 和其他類型的集合時,能夠編寫簡潔、高效且 Null 安全的程式碼。
若要從列表中過濾掉 null 值,請使用 filterNotNull() 函式:
fun main() {
val emails: List<String?> = listOf("[email protected]", null, "[email protected]", null, "[email protected]")
val validEmails = emails.filterNotNull()
println(validEmails)
// [[email protected], [email protected], [email protected]]
}如果您想在建立列表時直接過濾 null 值,請使用 listOfNotNull() 函式:
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值。
例如:
fun main() {
// 一週內記錄的溫度
val temperatures = listOf(15, 18, 21, 21, 19, 17, 16)
// 尋找本週最高溫
val maxTemperature = temperatures.maxOrNull()
println("Highest temperature recorded: ${maxTemperature ?: "No data"}")
// Highest temperature recorded: 21
// 尋找本週最低溫
val minTemperature = temperatures.minOrNull()
println("Lowest temperature recorded: ${minTemperature ?: "No data"}")
// Lowest temperature recorded: 15
}此範例使用 Elvis 運算子 ?: 在函式傳回 null 值時傳回列印陳述。
maxOrNull()和minOrNull()函式旨在與 不包含null值的集合配合使用。否則,您將無法判斷函式是因為找不到所需的值還是因為找到了null值而傳回。
您可以將 singleOrNull() 函式與 Lambda 運算式結合使用,以尋找符合條件的單個項目。如果不存在該項目或有多個符合條件的項目,函式將傳回 null 值:
fun main() {
// 一週內記錄的溫度
val temperatures = listOf(15, 18, 21, 21, 19, 17, 16)
// 檢查是否恰好有一天是 30 度
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 值:
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() 函式:
fun main() {
// 購物車中的商品價格
val itemPrices = listOf(20, 35, 15, 40, 10)
// 使用 reduceOrNull() 函式計算總價
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 的 標準函式庫 (standard library) 以尋找更多可用於提高程式碼安全性的函式。
提前傳回與 Elvis 運算子
在初學者導覽中,您學習了如何使用 提前傳回 來停止函式在某個點之後繼續執行。您可以將 Elvis 運算子 ?: 與提前傳回結合使用,以檢查函式中的先決條件。這種方法是保持程式碼簡潔的好方法,因為您不需要使用巢狀檢查。降低程式碼的複雜度也使其更容易維護。例如:
data class User(
val id: Int,
val name: String,
// 好友使用者 ID 列表
val friends: List<Int>
)
// 取得使用者好友數量的函式
fun getNumberOfFriends(users: Map<Int, User>, userId: Int): Int {
// 檢索使用者,若找不到則傳回 -1
val user = users[userId] ?: return -1
// 傳回好友數量
return user.friends.size
}
fun main() {
// 建立一些範例使用者
val user1 = User(1, "Alice", listOf(2, 3))
val user2 = User(2, "Bob", listOf(1))
val user3 = User(3, "Charlie", listOf(1))
// 建立使用者 Map
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執行個體的 Map 和一個整數形式的使用者 ID。 - 使用提供的使用者 ID 存取
User執行個體 Map 的值。 - 使用 Elvis 運算子,如果 Map 的值為
null,則提前傳回-1。 - 將從 Map 中找到的值指派給
user變數。 - 使用
size屬性傳回該使用者好友列表中的好友數量。
- 接收一個
main()函式:- 建立三個
User執行個體。 - 建立這些
User執行個體的 Map 並將其指派給users變數。 - 在
users變數上呼叫getNumberOfFriends()函式,傳入1和2分別傳回"Alice"的兩個好友和"Bob"的一個好友。 - 在
users變數上呼叫getNumberOfFriends()函式,傳入4,這會觸發提前傳回並傳回值-1。
- 建立三個
您可能會注意到,不使用提前傳回的話程式碼可以更簡潔。然而,這種方法需要多次安全呼叫,因為 users[userId] 可能會傳回 null 值,這會使程式碼稍微難以閱讀:
fun getNumberOfFriends(users: Map<Int, User>, userId: Int): Int {
// 檢索使用者,若找不到則傳回 -1
return users[userId]?.friends?.size ?: -1
}雖然此範例僅使用 Elvis 運算子檢查了一個條件,但您可以添加多個檢查來覆蓋任何關鍵的錯誤路徑。帶有 Elvis 運算子的提前傳回可防止您的程式執行不必要的運算,並透過在偵測到 null 值或無效情況時立即停止來提高程式碼的安全性。
有關如何在程式碼中使用 return 的更多資訊,請參閱 傳回與跳轉 (Returns and jumps)。
練習
練習 1
您正在為一個應用程式開發通知系統,使用者可以在其中啟用或停用不同類型的通知。完成 getNotificationPreferences() 函式,以便:
validUser變數使用as?運算子檢查user是否為User類別的執行個體。如果不是,則傳回一個空列表。userName變數使用 Elvis?:運算子確保在使用者名稱為null時預設為"Guest"。- 最終的傳回陳述式使用
.takeIf()函式,僅在啟用電子郵件和 SMS 通知偏好設定時才包含它們。 main()函式成功執行並列印預期的輸出。
takeIf()函式 在給定條件為 true 時傳回原始值,否則傳回null。例如:kotlinfun main() { // 使用者已登入 val userIsLoggedIn = true // 使用者具有活動工作階段 val hasSession = true // 如果使用者已登入且具有活動工作階段,則授予儀表板存取權限 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 = // 在此處編寫您的程式碼
val userName = // 在此處編寫您的程式碼
return listOfNotNull( /* 在此處編寫您的程式碼 */)
}
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() 函式,使其使用帶有述句 (predicate) 的 singleOrNull() 函式,如果有多個啟用的訂閱,則傳回 null 值:
|--|--|
data class Subscription(val name: String, val isActive: Boolean)
fun getActiveSubscription(subscriptions: List<Subscription>): Subscription? // 在此處編寫您的程式碼
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
您正在開發一個社群媒體平台,其中使用者具有使用者名稱 and 帳號狀態。您想要查看目前啟用的使用者名稱列表。完成 getActiveUsernames() 函式,使 mapNotNull() 函式 具有一個述句,如果該使用者是啟用的則傳回其使用者名稱,否則傳回 null 值:
|--|--|
data class User(val username: String, val isActive: Boolean)
fun getActiveUsernames(users: List<User>): List<String> {
return users.mapNotNull { /* 在此處編寫您的程式碼 */ }
}
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() 函式,使其使用提前傳回和 Elvis 運算子(在適用處)來檢查是否:
requested變數為null。available變數為null。requested變數為負值。requested變數中的數量高於available變數中的數量。
在上述所有情況下,函式必須提前傳回值 -1。
|--|--|
fun validateStock(requested: Int?, available: Int?): Int {
// 在此處編寫您的程式碼
}
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
}