- 原文地址:Convincing the Kotlin compiler that code is safe
- 原文作者:Dan Lew
- 譯文出自:掘金翻譯計劃
- 譯者:wilsonandusa
- 校對者:mnikn,zaraguo
Kotlin 這門語言最出色的特點之一就是它內部自帶的空值安全系統。如果你在要求非空的情況下使用空值,那麼編譯器會就會發出警告。
不過確保空值安全偶爾也會造成一些棘手的情況。你所熟知的毋庸置疑的程式碼也會佈滿空值的隱患...至少從編譯器的角度來說是這樣。
操縱 Map
來看一個例子。假設我們想將一個 List<String>
轉化為 Map<String, Int>
, 其中每一個 Int
代表對應 String
在數列中所出現的次數。我們可以這麼寫:
fun countInstances(list: List<String>): Map<String, Int> {
val map = mutableMapOf<String, Int>()
for (key in list) {
if (key !in map) {
map[key] = 0
}
map[key] = map[key] + 1
}
return map
}複製程式碼
程式碼邏輯正確但卻無法編譯。 Kotlin 認為這行程式碼有問題:
map[key] = map[key] + 1複製程式碼
map[key]
等同於 map.get(key)
。嚴格上來講 get()
會返回 T?
型別,因為你可以給它提供一個本身不存在的關鍵詞。即使你知道 map[key]
不是空值,編譯器意識不到你在每次使用 map[key]
前都會將其初始化。
我發現我在使用 Map.get()
時經常出現這個問題。我自己總是通過思考程式碼的邏輯來保障非空值的使用是否安全,但編譯器無法對此進行核實。
我可以依賴使用運算子!!
,但它看上去就像是一種警告 - 你不能無視編譯器所產生的的錯誤。以下是其他幾種可以解決這種問題的方法。
空值檢查
不直接在 Map 上進行操作,而是通過先提取數值並儲存於本地變數中再進行空值檢查。
val oldValue = map[key]
if (oldValue != null) {
map[key] = oldValue + 1
}
else {
map[key] = 1
}複製程式碼
雖然 oldValue
是可為空型別( Int?
),但它是一個本地變數,所以其他執行緒無法接觸到它。這意味著編譯器能確保在條件判斷後這個變數的值不會再發生改變。結果就是 Kotlin 將其視為非空變數。
空值檢查可以用,但是這個方法較為繁瑣。
Elvis運算子
我們可以通過結合 Elvis 運算子將空值檢查的解法壓縮為單行程式碼:
map[key] = (map[key] ?: 0) + 1複製程式碼
Elvis 運算子會選擇 map[key]
和0
中第一個為非空值的那一個。這樣能保證結果為整數型別,以便後期對其進行增值。
絕地心術
如果我們直接宣告“這些都不是非空值”會發生什麼呢?
其實 Kotlint 專門為此提供了 Map.getValue()
。這個函式會返回 T
類而不是 T?
類。因此, map.getValue(key)
具有 map[key]
所不具備的功能:
map[key] = map.getValue(key) + 1複製程式碼
如果本來就沒有值會發生什麼?這種情況下,它會生成一個異常! getValue()
本身長這樣:
val value = map[key] ?: throw new NoSuchElementException()複製程式碼
結合前文可得知, getValue()
和!!
其實差不多。如果有空值存在它們都會生成異常,然而...
預設值
你可以通過使用 Map.withDefault()
來給你的 Map 提供預設值。使用這個方法的話, Map.getValue()
在找不到關鍵詞的情況下會返回預設值:
fun countInstances(list: List<String>): Map<String, Int> {
val map = mutableMapOf<String, Int>().withDefault { 0 }
for (key in list) {
map[key] = map.getValue(key) + 1
}
return map
}複製程式碼
在這種情況下, Map.getValue()
肯定比!!
好,因為它不可能產生異常。
如果你不想為整個 Map 設定預設值,你也可以分情況使用預設值,比如用 Map.getOrDefault()
:
map[key] = map.getOrDefault(key, 0) + 1複製程式碼
除了使用數值作為預設值,你還可以使用 Map.getOrElse()
將函式作為預設值:
map[key] = map.getOrElse(key, { 0 }) + 1複製程式碼
在這個例子裡這麼寫很不明智,但如果預設值的計算很費時,這個方法會節省很多時間。(同時,由於 getOrDefault()
最近才新增到 Android 中,除非你所使用的最低開發版本為24,你還得用 Kotlin 的 getOrElse()
函式。)在這個例子中,設預設值和使用 Elvis 運算子都行。
集合的變形
除了遍歷集合中的每一個元素,我們也可以將整個集合進行一次變形。變形能避免空值檢查,因為我們所遍歷的元素一定存在於集合中。
Kotlin 的標準庫中內建很多不錯的函式正好能解決我們的問題:
fun countInstances(list: List<String>) = list.groupingBy { it }.eachCount()複製程式碼
這裡我們首先將 List
轉化為 Grouping
,然後我們用 Grouping.eachCount()
將其變形為 Map<String, Int>
。
集合層面的操作能力十分強大,經常會比遍歷整個集合有用很多(主要是因為標準庫會在背後進行優化)。
哪個最好?
我已經示範了幾種能保證程式碼通過編譯器的策略:
- 空值檢查
- Elvis 運算子
- 轉化為非空值 (可能出現異常)
- 預設值
- 集合變形
(我這麼寫並不意味著這就是所有的策略;不同情況可能有其它選擇)
一般要根據程式碼的上下文來判斷哪種方法最適合。在我們的例子中, groupingBy().eachCount()
肯定最好。它簡潔,有效,不難理解,而且完全避免了空值檢查。
感謝 Jake Wharton 對這篇文章的幫助
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃。