Kotlin刨根問底(一):你真的瞭解Kotlin中的空安全嗎?

coder-pig發表於2020-01-16

這篇之前寫過,刪了一些廢話,修正了一些錯誤,弄了下排版,最近開始連載這個系列,
行文結構:要點提煉(方便回顧) + 常規操作 + 原始碼層面摸索實現原理
內容部分摘取自:《Kotlin實用指南》,感興趣的可以訂閱一波 ~


0x1、要點提煉


  • Kotlin中通過「非空型別」與「可空型別」來規避 NPE
  • 可空操作符「`?`」、安全呼叫操作符「`?.`」、非空斷言運算子「`!!`」
  • Elvis操作符「`?:`」如果不為空返回它,否則返回另一個值;
  • 安全的型別轉換「`as?`」
  • ?.let{}
  • 不是絕對的空指標安全:Kotlin呼叫返回空的Java程式碼;
  • Kotlin空安全的實現原理:


0x2、Kotlin裡的空安全用法


NullPointerException(NPE),空指標異常,發生在執行時引用一個空物件的方法時會丟擲

Java中常見的規避手段是:

在物件使用前進行判空:if (obj != null)

多層判空時顯得不是很美觀,而Kotlin中是「空安全」的:

編譯期 就處理Null

空安全不是Kotlin特有的,其他很多程式語言也有,下面簡述下Kotlin空安全的相關用法。


① 非空型別與可空型別

Kotlin中通過「非空型別」和「可空型別」來規避NPE,非空型別不能設定為Null值:

可空型別可以設定為Null值,在型別後加上 可空操作符(?) 即可,程式碼示例如下:

可空型別,直接訪問它的屬性或方法,會報錯:

可以通過 安全呼叫操作符(?.)非空斷言運算子(!!) 來解決,程式碼示例如下:

執行結果如下


② Elvis操作符(?:)

三目條件運算子的簡略寫法:如果不是空,就返回它,否則返回另一個值。程式碼示例如下:


③ 安全的型別轉換

Kotlin中可以使用as關鍵字來進行型別轉換,而使用as?表示安全型別的轉換。最常見的使用場合,後臺介面返回一個引數,需要我們自己做下型別轉換,直接用as的話,如果引數為null或者非String型別,就可能引發異常,如果用as? ,引數異常則不會轉換,程式碼示例如下:

執行結果如下


④ ?.let{}

let函式除了可用於在同一個作用域下操作變數外(程式碼更優雅)外:

tvTitle.let {
    it.text = "標題"
    it.textSize = 18.0f
    it.gravity = Gravity.LEFT or Gravity.CENTER
    it.setOnClickListener {}
}
複製程式碼

還可以用作做判null操作,比如下面這樣的程式碼:

test我們已經設定了一個值,但是還是報錯,新增上一個非空斷言即可:

還有個更優雅的寫程式碼方式,就是用2,修改後的程式碼如下:

另外?.let{}遍歷集合會忽略null值,只對非null值執行操作;除此之外,還可以用集合操作函式filterNotNull過濾出非空元素。


0x3、不是絕對的空指標安全


如題,Kotlin中並不是絕對的空指標安全,最常見的就是在Kotlin去調Java程式碼,比如下面這個例子:

//Java
public class Test {
    public static String getMsg() {
        return null;
    }
}

//Kotlin
fun main() {
    println(Test.getMsg().length)
}
複製程式碼

執行後就直接報錯

當你與Java程式碼進行互操作時,Null安全性確實被破壞了,當然想規避這個問題也很簡單,加個?即可,示例如下:

fun main() {
    println(Test.getMsg()?.length)
}
複製程式碼

執行結果如下

Tips:也可以為Java程式碼@NotNull註解來解決~


0x4、Kotlin是如何實現空安全的


接著我們來探究下,Kotlin中到底是怎麼實現空安全的,寫下簡單的程式碼:

fun test_1(str: String) = str.length
fun test_2(str: String?) = str?.length
fun test_3(str: String?) = str!!.length
fun test_4(str: Any?) { str as String }
fun test_5(str: Any?) { str as? String }
複製程式碼

接著依次點選:Tools -> Kotlin -> Show Kotlin Bytecode,生成位元組碼:

生成後的Java程式碼如下:

接著分析一波,首先是test_1函式,可以看到這裡有一個@NotNull的註解,然後是:

Intrinsics.checkParameterIsNotNull(str, "str");
複製程式碼

Intrinsics是Kotlin的一個內部類,定位到checkParameterIsNotNull函式:

再跟:

行吧,其實就是直接對引數判空,如果為空丟擲引數為空的異常。接著看下test_2函式,比較簡單,判斷是否為空,不為空的話,呼叫對應的方法,否則返回一個null。再接著是test_3函式,直接判斷是否為空,空的話直接丟擲Npe異常。

然後是test_4函式,判空,如果空丟擲型別轉換異常,否則執行後續程式碼;最後是test_5函式,建立一個新物件把引數的值傳給他,然後進行型別判斷,如果不是特定型別,物件賦值null,然後把執行型別強轉後的物件賦值給一個新的物件。

綜上,Kotlin中對空安全背後的處理套路如下:

  • 1、非空型別的屬性編譯器新增@NotNull註解,可空型別新增@Nullable註解;
  • 2、非空型別直接對引數進行判空,如果為空直接丟擲異常;
  • 3、可空型別,如果是?.判空,不空才執行後續程式碼,否則返回null;如果是!!,空的話直接丟擲NPE異常。
  • 4、as操作符會判空,空的話直接丟擲異常,不為空才執行後續操作,沒做型別判斷!執行時可能會報錯!
  • 5、as?則是新建一個變數,引數賦值給它,然後判斷是否為特定型別,賦值為null,接著把這個變數的值賦值給另一個新的變數,這裡有一點注意:as?處理後的引數可能為空!!!所以呼叫as?轉換後的物件還需要新增安全呼叫操作符(?.)

提醒:(這裡直接用編譯後Java程式碼的原因是比較直觀~)

  • @NotNull註解對應位元組碼裡的:@Lorg/jetbrains/annotations/NotNull;
  • @Nullable註解對應位元組碼裡的:@Lorg/jetbrains/annotations/Nullable;

參考文獻



相關文章