Kotlin教程(四)可空性

寫在開頭:本人打算開始寫一個Kotlin系列的教程,一是使本身記憶和理解的更加深入,二是能夠分享給一樣想學習Kotlin的同窗。系列文章的知識點會以《Kotlin實戰》這本書中順序編寫,在將書中知識點展現出來同時,我也會添加對應的Java代碼用於對比學習和更好的理解。java

Kotlin教程(一)基礎
Kotlin教程(二)函數
Kotlin教程(三)類、對象和接口
Kotlin教程(四)可空性
Kotlin教程(五)類型
Kotlin教程(六)Lambda編程
Kotlin教程(七)運算符重載及其餘約定
Kotlin教程(八)高階函數
Kotlin教程(九)泛型android


這一章實際上在《Kotlin實戰》中是第六章,在Lambda以後,可是這一章的內容其實是Kotlin的一大特點之一。所以,我將此章的內容提到了前面彙總。編程

可空性

可空性是Kotlin類型系統中幫助你避免NullPointerException錯誤的特性。安全

可空類型

若是一個變量可能爲null,對變量的方法的調用就是不安全的,由於這樣會致使NullPointerException。例如這樣一個Java函數:bash

int strLen(String s) {
    return s.length();
}
複製代碼

若是這個函數被調用的時候,傳給它的是一個null實參,它就會拋出NullPointerException。那麼你是否須要在方法中增長對null的檢查呢?這取決與你是否指望這個函數被調用的時候傳給它的實參能夠爲null。若是不能夠的話,咱們用Kotlin能夠這樣定義:框架

fun strLen(s: String) = s.length
複製代碼

看上去與Java沒有區別,可是你嘗試調用strLen(null) 就會發如今編譯期就會被標記成錯誤。由於在Kotlin中String 只能表示字符串,而不能表示null,若是你想支持這個方法能夠傳null,則須要在類型後面加上?ide

fun strLen(s: String?) = if(s != null) s.length else 0
複製代碼

? 能夠加在任何類型的後面來表示這個類型的變量能夠存儲null引用:String?Int?MyCustomType?等。函數

一旦你有一個可空類型的值,能對它進行的操做也會受到限制。例如不能直接調用它的方法:工具

val s: String? = ""
//    s.length  //錯誤,only safe(?.) or non-null asserted (!!.) calls are allowed
    s?.length   //表示若是s不爲null則調用length屬性
    s!!.length  //表示斷言s不爲null,直接調用length屬性,若是s運行時爲null,則一樣會crash
複製代碼

也不能把它賦值給非空類型的變量:post

val x: String? = null
//    val y: String = x  //Type mismatch
複製代碼

也就是說,加? 和不加能夠看作是兩種類型,只有與null進行比較後,編譯器纔會智能轉換這個類型。

fun strLen(s: String?) = if(s != null) s.length else 0  
複製代碼

這個例子就與null進行比較,因而String? 類型被智能轉換成String 類型,因此能夠直接獲取length屬性。

Java有一些幫助解決NullPointerException問題的工具。好比,有些人會使用註解(@Nullable和@NotNull)來表達值得可空性。有些工具能夠利用這些註解來發現可能拋出NullPointerException的位置,但這些工具不是標準Java編譯過程的一部分,因此很難保證他們自始至終都被應用。並且在整個代碼庫中很難使用註解標記全部可能發生錯誤的地方,讓他們都被探測到。

Kotlin的可空類型完美得解決了空指針的發生。 注意,可空的和非空的對象在運行時沒有什麼區別:可空類型並非非空類型的包裝。全部的檢查都發生在編譯器。這意味着使用Kotlin的可空類型並不會在運行時帶來額外的開銷。

安全調用運算符:"?."

Kotlin的彈藥庫中最有效的一種工具就是安全調用運算符:?. ,它容許你爸一次null檢查和一次方法調用合併成一個操做。例如表達式s?.toUpperCase() 等同於if (s != null) s.toUpperCase() else null 。 換句話說,若是你視圖調用一個非空值得方法,此次方法調用會被正常地執行。但若是值是null,此次調用不會發生,而整個表達式的值爲null。所以表達式s?.toUpperCase() 的返回類型是String?

安全調用一樣也能用來訪問屬性,而且能夠連續獲取多層屬性:

class Address(val street: String, val city: String, val country: String)

class Company(val name: String, val address: Address?)

class Person(val name: String, val company: Company?)

fun Person.countryName(): String {
    val country = this.company?.address?.country  //多個安全調用連接在一塊兒
    return if (country != null) country else "Unknown"
}
複製代碼

Kotlin 可讓null檢查的變得很是簡潔。在這個例子中你用一個值和null比較,若是這個值不爲空就返回這個值,不然返回其餘的值。在Kotlin中有更簡單的寫法。

Elvis運算符:"?:"

if (country != null) country else "Unknown" 經過Elvis運算符改寫成:

country ?: "Unknown"
複製代碼

Elvis運算符接受兩個運算數,若是第一個運算數不爲null,運算結果就是第一個運算數,若是第一個運算數爲null,運算結果就是第二個運算數。 fun strLen(s: String?) = if(s != null) s.length else 0 這個例子也能夠用Elvis運算符簡寫:fun strLen(s: String?) = s?.length ?: 0

安全轉換:"as?"

以前咱們學習了as 運算符用於Kotlin中的類型轉換。和Java同樣,若是被轉換的值不是你試圖轉換的類型,就會拋出ClassCastException異常。固然你能夠結合is 檢查來確保這個值擁有合適的類型。但Kotlin做爲一種安全簡潔的語言,有優雅的解決方案。 as? 運算符嘗試把值轉換成指定的類型,若是值不合適的類型就返回null。 一種常見的模式是把安全轉換和Elvis 運算符結合使用。例如equals方法的時候這樣的用法很是方便:

class Person(val name: String, val company: Company?) {
    override fun equals(other: Any?): Boolean {
        val o = other as? Person ?: return false  //檢查類型不匹配直接返回false
        return o.name == name && o.company == company //在安全轉換後o被智能地轉換爲Person類型
    }

    override fun hashCode(): Int = name.hashCode() * 31 + (company?.hashCode() ?: 0)
}
複製代碼

非空斷言:"!!"

非空斷言是Kotlin提供的最簡單直接的處理可空類型值得工具,它能夠把任何值轉換成非空類型。若是對null值作非空斷言,則會拋出異常。 以前咱們也演示過非空斷言的用法了:s!!.length

你可能注意到雙感嘆號看起來有點粗暴,就像你衝着編譯器咆哮。這是有意爲之的,Kotlin的設計設視圖說服你思考更好的解決方案,這些方案不會使用斷言這種編譯器沒法驗證的方式。

可是確實存在這樣的狀況,某些問題適合用非空斷言來解決。當你在一個函數中檢查一個值是否爲null。而在另外一個函數中使用這個值時,這種狀況下編譯器沒法識別這種用是否安全。若是你確信這樣的檢查必定在其餘某個函數中存在,你可能不想在使用這個值以前重複檢查。這時你就可使用非空斷言。

"let" 函數

let函數讓處理可空表達式變得更容易。和安全調用運算符一塊兒,它容許你對錶達式求值,檢查求值結果是否爲null,並把結果保存爲一個變量。全部這些動做都砸系統一個簡潔的表達式中。 可空參數最多見的一種用法應該就是被傳遞給一個接受非空參數的函數。好比說下面這個函數,它接收一個String類型的參數並向這個地址發送一封郵件,這個函數在Kotlin中是這樣寫的:

fun sendEmailTo(email: String) { ... }
複製代碼

不能把null傳給這個函數,所以一般須要先判斷一下而後調用函數: if(email != null) sendEmailTo(email) 。 但咱們有另外一種方式:使用let函數,並經過安全調用來調用它。let函數作的全部事情就是把一個調用它的對象變成lambda表達式的參數: email?.let{ email -> sendEmailTo(email) } let函數只有在email的值非空時才被調用,若是email值爲null則{} 的代碼不會執行。 使用自動生成的名字it 這種簡明語法以後,能夠寫成:email?.let{ sendEmailTo(it) } 。(Lambda的語法在只有章節會詳細講)

延遲初始化的屬性

不少框架會在對象實例建立以後用專門的方法來初始化對象。例如Android中,Activity的初始化就發生在onCreate方法中。而JUnit則要求你把初始化的邏輯放在用@Brefore註解的方法中。 可是你不能再狗仔方法中徹底放棄非空屬性的初始化器。僅僅在一個特殊的方法裏初始化它。Kotlin一般要求你在構造方法中初始化全部屬性,若是某個屬性時非空類型,你就必須提供非空的初始化值。不然,你就必須使用可空類型。若是你這樣作,該屬性的每次訪問都須要null檢查或者!! 運算符。

class Activity {
    var view: View? = null

    fun onCreate() {
        view = View()
    }

    fun other() {
        //use view
        view!!.onLongClickListener = ...
    }
}
複製代碼

這樣使用起來比較麻煩,爲了解決這個麻煩,使用lateinit 修飾符來聲明一個不須要初始化器的非空類型的屬性:

class Activity {
    lateinit var view: View

    fun onCreate() {
        view = View()
    }

    fun other() {
        //use view
        view.onLongClickListener = ...
    }
}
複製代碼

注意,延遲初始化的屬性都是var 由於須要在構造方法外修改它的值,而val 屬性會被編譯成必須在構造方法中初始化的final字段。儘管這個屬性時非空類型,可是你不須要再構造方法中初始化它。若是在屬性被初始化以前就訪問了它,會獲得異常"lateinit property xx has not been initialized" ,說明屬性尚未被初始化。

注意lateinit屬性常見的一種用法是依賴注入。在這種狀況下,lateinit屬性的值是被依賴注入框架從外部設置的。爲了保證和各類Java框架的兼容性,Kotlin會自動生成一個和lateinit屬性具備相同可見性的字段,若是屬性的可見性是public,申城字段的可見性也是public。

public final class Activity {
   public View view;

   public final View getView() {
      View var10000 = this.view;
      if(this.view == null) {
         Intrinsics.throwUninitializedPropertyAccessException("view");
      }
      return var10000;
   }

   public final void setView(@NotNull View var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.view = var1;
   }

   public final void onCreate() {
      this.view = new View();
   }

   public final void other() {
   }
}
複製代碼

可空性的擴展

爲可空類型定義擴展函數是一種更強大的處理null值的方式。能夠容許接收者爲null的(擴展函數)調用,並在該函數中處理null,而不是在確保變量爲null以後再調用它的方法。 Kotlin標準庫中定義的String的兩個擴展函數isEmptyisBlank 就是這樣的例子。第一個函數判斷字符串是不是一個空的字符串"" 。第二個函數判斷它是不是空的或則只包含空白字符。一般用這些函數來檢查字符串是有價值的,以確保對它的操做是有意義的。你可能意識到,像處理無心義的空字符串和空白字符串這樣處理null也頗有用。事實上,你的確能夠這樣作:函數isEmptyOrNullisNullOrBlank 就能夠由String? 類型的接收者調用。

fun verifyUserInput(input: String?) {
    if (input.isNullOrBlank()) { //此方法是String?的方法,不須要安全調用
        println("Please fill in the required fields")
    }
}
複製代碼

不管input是null仍是字符串都不會致使任何異常。咱們來看下isNullOrBlank 函數的定義:

public inline fun CharSequence?.isNullOrBlank(): Boolean = this == null || this.isBlank()
複製代碼

能夠看到擴展函數是定義給CharSequence? (String的父類),所以不像調用String的方法那樣須要安全調用。 當你爲一個可空類型定義擴展函數時,這覺得這你能夠對可空的值調用這個函數;而且函數體中this可能爲null,因此你必須顯示地檢查。在Java中,this永遠是非空的,由於他引用的時當前你所在這個類的實例。而在Kotlin中,這並不永遠成立:在可空類型的擴展函數中,this能夠爲null。 以前討論的let 函數也能被可空的接收者調用,但它並不檢查值是否爲null。若是你在一個可空類型直接調用let 函數,而沒有使用安全調用運算符,lambda的實參將會是可空的:

val person: Person? = ...
person.let { sendEmailTo(it) }  //沒有安全調用,因此it是可空類型

ERROR: Type mismatch:inferred type is Person? but Person was expected
複製代碼

所以,若是想要使用let來檢查非空的實參,你就必須使用安全調用運算符?. 就像以前看到的代碼同樣:person?.let{ sentEmailTo(it) }

當你定義本身的擴展函數時,須要考慮該擴展是否須要可空類型定義。默認狀況下,應該把它定義成非空類型的擴展函數。若是發現大部分狀況下須要在可空類型上使用這個函數,你能夠稍後再安全地修改他(不會破壞其餘代碼)。

類型參數的可空性

Kotlin中全部泛型和泛型函數的類型參數默認都是可空的。任何類型,包括可空類型在內,均可以替換類型參數。這種狀況下,使用類型參數做爲類型聲明都容許爲null,儘管類型參數T並無用問號結尾。

fun <T> printHashCode(t: T) {
    println(t?.hashCode())
}
複製代碼

在該函數中,類型參數T推導出的類型是可空類型Any? 所以,儘管沒有用問號結尾。實參t依然容許持有null。 要使用類型參數非空,必需要爲它指定一個非空的上界,那樣泛型會拒絕可空值做爲實參:

fun <T: Any> printHashCode(t: T) {
    println(t.hashCode())
}
複製代碼

後續章節會講更多的泛型細節,這裏你只須要記得這一點就能夠了。

可空性和Java

咱們在Kotlin中經過可空性能夠完美地處理null了,可是若是是與Java交叉的項目中呢?Java的類型系統是不支持可空性的,那麼該若是處理呢? Java中可空性信息一般是經過註解來表達的,當代碼中出現這種信息時,Kotlin就會識別它,轉換成對應的Kotlin類型。例如:@Nullable String -> String?@NotNull String -> String。 Kotlin能夠識別多種不一樣風格的可空性註解,包括JSR-305標準的註解(javax.annotation包下)、Android的註解(android.support.annitation) 和JetBrans工具支持的註解(org.jetbrains.annotations)。那麼還剩下一個問題,若是沒有註解怎麼辦呢?

平臺類型

沒有註解的Java類型會變成Kotlin中的平臺類型 。平臺類型本質上就是Kotlin不知道可空性信息的類型。便可以把它當作可空類型處理,也能夠當作非空類型處理。這意味着,你要像在Java中同樣,對你在這個類型上作的操做負有所有責任。編譯器將會容許全部操做,它不會把對這些值得空安全操做高亮成多餘的,但它平時倒是這樣對待非空類型值上的空安全操做的。 好比咱們在Java中定義一個Person類:

public class Person {
    private  String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
複製代碼

咱們在Kotlin中使用這個類:

fun yellAt(person: Person) {
    println(person.name.toUpperCase()) //不考慮null狀況,可是若是爲null則拋出異常
    println((person.name ?: "Anyone").toUpperCase()) //考慮null的可能
}
複製代碼

咱們便可以當成非空類型處理,也能夠當成可空類型處理。

Kotlin平臺類型在表現爲:Type!

val i: Int = person.name

ERROR: Type mistach: inferred type is String! but Int was expected
複製代碼

可是你不能聲明一個平臺類型的變量,這些類型只能來自Java代碼。你能夠用你喜歡的方式來解釋平臺類型:

val person = Person()
val name: String = person.name
val name2: String? = person.name
複製代碼

固然若是平臺類型是null,賦值給非空類型時仍是會拋出異常。

爲何須要平臺類型? 對Kotlin來講,把來自Java的全部值都當成可空的是否是更安全?這種設計也許可行,可是這須要對永遠不爲空的值作大量冗餘的null檢查,由於Kotlin編譯器沒法瞭解到這些信息。 涉及泛型的話這樣狀況就更糟糕了。例如,在Kotlin中,每一個來自Java的ArrayList 都被看成ArrayList<String?>?,每次訪問或者轉換類型都須要檢查這些值是否爲null,這將抵消掉安全性帶來的好處。編寫這樣的檢查很是使人厭煩,因此Kotlin的設計者做出了更實用的選擇,讓開發者負責正確處理來自Java的值。

繼承

當在Kotlin中重寫Java的方法時,能夠選擇把參數和返回類型定義成可空的,也能夠選擇把它們定義成非空的。例如,咱們來看一個例子:

/* Java */
interface StringProcessor {
    void process(String value);
}
複製代碼

Kotlin中下面兩種實現編譯器均可以接收:

class StringPrinter : StringProcessor {
    override fun process(value: String) {
        println(value)
    }
}

class NullableStringPrinter : StringProcessor {
    override fun process(value: String?) {
        if (value != null) {
            println(value)
        }
    }
}
複製代碼

注意,在實現Java類或者接口的方法時必定要搞清楚它的可空性。由於方法的實現能夠在非Kotlin的代碼中被調用,Kotlin編譯器會爲你聲明的每個非空的參數生成非空斷言。若是Java代碼傳給這個方法一個null值,斷言將會觸發,你會獲得一個異常,即使你從沒有在你的實現中訪問過這個參數的值。

所以,建議你只有在確保調用該方法時絕對不會出現空值時,才用非空類型取接收平臺類型。

相關文章
相關標籤/搜索