Kotlin 中的判空操作 Elvis 操作符使用踩坑

Kotlin 作爲一門有着所謂空安全特性的(年輕)編程語言,有時出於實際業務場景需要還是會把變量聲明成可空(Null-able)的,好在由於空安全特性,編譯器會強制我們對可空變量進行判空檢查(除非你使用了非空斷言 !! 強制讓編譯器閉嘴)。Kotlin 以完全兼容 Java 爲設計原則,設計者們該是在設計階段就預見到了使用者們可能還是需要進行很多判空檢查,於是引入了非常簡潔優雅的 Elvis 操作符,請看代碼:

Java 典型判空操作:

/**
     * <p>控制檯打印字符串的長度是多少</>
     * @param str:給定的字符串
     */
    public void askStringLength(String str){
        int i;
        //Java 中我們一般這樣判空,或者使用那個三目運算符
        if (str == null){
            i = 0;
        } else{
            i = str.length();
        }

        //或者使用三目運算符
        i = str == null? 0 : str.length();

        System.out.println("The length is " + i);
    }

如上,Java 沒有所謂空安全特性。你可能需要寫大量上面那樣的判空模板代碼,而且你肯定無法在每一個變量可能爲空的地方加上這種防禦性代碼,畢竟項目裏很容易積攢到成千上萬的變量,給這麼多變量寫上防禦性代碼想想都頭大!當然對於很大概率會拋出空指針異常的地方編譯器會做高亮提示,但會給出提示的地方放之整個項目而言實在微不足道。我們手動 new 出來的對象和 null,作爲有着完全不同行爲的兩種實體居然可以賦值給同一個變量,如此,如果在該判空的地方沒做判空,就是一個在運行時可能會拋出 NPE(NullPointerException) 的bug!

Kotlin 重新設計了一套完全不同於 Java 的類型系統,聲稱可大大較少項目中的 NullPointerExceptio(Kotlin因以完全兼容 Java 爲設計原則,因而無法完全杜絕 NPE)。就我本人經驗看來,效果還不賴,使用 Koltin 項目中的 NPE 確實少多了。而且 Koltin 中對可能拋出 NPE 的地方做判空檢查的方式比之 Java 而言實在優雅,從此遠離那些煩人的 if-else,只需藉助所謂 Elvis 操作符。

關於 Kotlin 的類型系統與 Java 之異同非本文重點,重點是在 Kotlin 中,如果一個變量是可空的,你在訪問此變量時如果不做判空檢查是通不過編譯的(除非你使用了非空斷言 !! 強制讓編譯器閉嘴)!如此使得我們的代碼在運行時拋出 NPE 的概率大大減少!嗯能在編譯階段發現的問題就別留到運行時再發現!下面我們直接看看上面包含判空操作的 Java 代碼其等價的 Koltin 代碼是怎麼樣的。

Kotlin 典型判空操作:

/**
     *
     * <p>控制檯打印字符串的長度是多少</>
     * @param str:給定的字符串
     */
    fun askStringLength(str: String?) {
        val i: Int
        // Kotlin 中我們一般這樣對變量判空
        // ?. 是 Kotlin 中所謂的安全調用操作符
        // 如果 str 非空,就返回 str.length,否則返回 null,表達式(str?.length)返回的類型是 Int?   嗯有可能爲 null
        // ?; 就是所謂的 Elvis 操作符
        // 如果 ?: 左側表達式非空,elvis 操作符就返回其左側表達式,否則返回右側表達式
        i = str?.length?:0//str.length 這麼寫是通不過編譯的
        // 一路流式操作下來是不是感覺很順暢,那可比 if-else 舒服多了!注意 Kotlin 中的 Elvis 操作符不是三目操作符,Kotlin 中是不存在三木操作符的

        println("The length is $i")
    }

Kotlin 中我們一般這樣對變量判空:

i = str?.length?:0

?. 是 Kotlin 中所謂的安全調用操作符,如果 str 非空,就返回 str.length,否則返回 null,表達式(str?.length)返回的類型是 Int? , 嗯有可能爲 null,?; 就是所謂的 Elvis 操作符,如果 ?: 左側表達式非空,elvis 操作符就返回其左側表達式,否則返回右側表達式(做默認值)。

此時如果我們有了新需求,要求計算一個 List 中所有字符串的長度之和,你很可能會這麼寫:

/**
     *<p>控制檯打印列表中所有字符串長度之和</>
     * @param strings:給定的字符串列表
     */
    fun askStringLength(strings: List<String?>) {
        var i: Int = 0
        strings.forEach { str ->
            i = str?.length?:0 + i //注意這行代碼,當然你更可能寫成 i += str?.length?:0,爲了說明問題先按我寫的來
        }
        println("The length is $i")
    }

如果真像上面那樣寫了,那你肯定得不到正確的結果!你可以運行試試!

原因在於 Elvis 操作符的優先級較低,請看下方我從 Kotlin 官網扒來的操作符優先級表:

Precedence Title Symbols
Highest Postfix ++, --, ., ?., ?
Prefix -, +, ++, --, !, label
Type RHS :, as, as?
Multiplicative *, /, %
Additive +, -
Range ..
Infix function simpleIdentifier
Elvis ?:
Named checks in, !in, is, !is
Comparison <, >, <=, >=
Equality ==, !==
Conjunction &&
Disjunction ` `
Lowest Assignment =, +=, -=, *=, /=, %=

注意 Elvis 操作符的位置!

這行代碼:

i = str?.length?:0 + i

其實等價於

i = str?.length ?: (0 + i)

所以想讓代碼正確運行我們其實應該寫成這樣:

i = (str?.length?:0) + i

使用時千萬要注意操作符的優先級!

完。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章