Kotlin基礎知識(十一)——Kotlin的類型系統:可空性

1、可空類型

Kotlin和Java的類型系統之間第一條也多是最重要的一條區別是:Kotlin對可空類型的顯式的支持。這意味着:這是一種指出你的程序中哪些變量和屬性容許爲null的方式。若是一個變量能夠爲null,對變量的方法的調用就是不安全的,由於這樣會致使NullPointerExceptionjava

fun strLenSafe(s: String?) = ..
複製代碼

上述代碼中***?能夠加在任何類型的後面來表示這個類型的變量能夠存儲null引用:String?、Int?、MyCustomType?***安全

Type?  =  Type  or  null
可空類型的變量能夠存儲null引用
複製代碼

重申一下,沒有問號的類型表示這種類型的變量不能存儲null引用。這說明全部常見類型默認都是非空的,,除非顯式地把它標記爲可空。markdown

一個可空類型的值限制app

  • 1.不能再調用它的方法
  • 2.不能把它賦值給非空類型的變量
  • 3.不能把可空類型的值傳給擁有非空類型參數的函數

2、安全調用運算符:「?.」

Kotlin的彈藥庫中最有效的一種工具就是安全調用運算符?.,它容許一次null檢查和一次方法調用合併成員一個操做。框架

圖1 安全調用運算符只能調用非空值的方法

注意:此次調用的結果類型也是可空的ide

// 定義
fun printAllCaps(s: String?) {
    // allCaps多是null
    val appCaps: String? = s?.toUpperCase()
    println(appCaps)
}

// 測試
>>> printAllCaps("abc")
ABC
>>> printAllCaps("null")
null
複製代碼

安全調用不光能夠調用方法,也能用來訪問屬性。函數

  • 使用安全調用處理可空屬性
// 定義
class Employee(val name: String, val manager: Employee?)
fun managerName(employee: Employee): String? = employee.manager?.name

// 測試
>>> val ceo = Employee("Da Boss", null)
>>> val developer = Employee("Bob Smith", ceo)
>>> println(managerName(developer))
Da Boss
>>> println(managerName(ceo))
null
複製代碼
  • 連接多個安全調用

2、Elvis運算符:「?.」

Kotlin有方便的運算符來提供代替null的默認值。它被稱做***Elvis運算符***(或者null合併運算符)。工具

// 定義
fun foo(s: String?) {
    val t: String = s ?: ""
}
複製代碼

Elvis運算符接受兩個運算數,第一個運算數不爲null,運算結果就是第一個運算數;不然,運算結果就是第二個運算數。測試

圖2 Elvis運算符用其餘值代替null

Elvis運算符常常和安全調用運算符一塊兒使用,用一個值代替對null對象調用方法時返回的nullui

  • 使用Elvis運算符處理null值
// 定義
class Address(val streetAddress: String, val zipCode: Int,
              val city: String, val country: String)

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

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

fun printShippingLabel(person: Person) {
    val address = person.company?.address
            ?: throw IllegalArgumentException("No address")
    with(address) {
        println(streetAddress)
        println("$zipCode $city, $country")
    }
}

// 測試
>>> val address = Address("Elsestr. 47", 80687, "Munich", "Germany")
>>> val jetbrains = Company("JetBrains", address)
>>> val person = Person("Dmitry", jetbrains)

>>> printShippingLabel(person)
Elsestr. 47
80687 Munich, Germany

>>> printShippingLabel(Person("Alexey", null))
java.lang.IllegalArgumentException: No address
複製代碼

3、安全轉換:「as?」

***as?***運算符把值轉換成指定的類型,若是值不是合適的類型就返回null,以下圖:

圖3 安全轉換運算符"as?"

  • 使用安全轉換實現equals
class Person1(val firstName: String, val lastName: String) {
    override fun equals(other: Any?): Boolean {
        val otherPerson = other as? Person1 ?: return false

        return otherPerson.firstName == firstName &&
                otherPerson.lastName == lastName
    }

    override fun hashCode(): Int =
            firstName.hashCode() * 37 + lastName.hashCode()
}

>>> val p1 = Person1("Dmitry", "Jemerov")
>>> val p2 = Person1("Dmitry", "Jemerov")
>>> println(p1 == p2)
true
>>> println(p1.equals(42))
false
複製代碼

使用這種模式,能夠很是容易地檢查實參是不是適當的類型,轉換它,並在它的類型不正確時返回false,並且這些操做所有在同一個表達式中。

4、非空斷言:「!!」

非空斷言是Kotlin提供的最簡單直率的處理可空類型值的工具。它使用雙感嘆號表示,能夠把任何值轉換成非空類型。若是對null值作非空斷言,則會拋出異常。

fun ignoreNulls(s: String?) {
    // 異常指向這一行
    val sNotNull: String = s!!
    println(sNotNull.length)
}

>>> ignoreNulls(null)
Exception in thread "main" kotlin.KotlinNullPointerException
複製代碼

注意:使用***!!而且它的結果是異常時,異常調用棧的跟蹤信息只代表異常發生在哪一行代碼*,而不會代表異常發生在哪個表達式。爲了讓跟蹤信息更清晰精確地表示哪一個值爲***null*,最好避免在同一行中使用多個!!斷言**:

// 不要寫這樣的代碼!
person.company!!.address!!.country
複製代碼

上面這一行代碼中發生了異常,不能分辨出到底company的值爲null,仍是address的值爲null。

5、「let」函數

let函數容許對表達式求值,檢查求值結果是否爲null**,並把結果保存爲一個變量。全部這些動做都在同一個簡潔的表達式中。

可空參數最多見的一種用法應該就是被傳遞給一個接受非空參數的函數。

  • 示例:
fun sendEmailTo(email: String) { /* ... */ }

>>> val  email: String? = ...
>>> sendEmailTo(email)
ERROR: Type mismatch: inferred type is String? but String was expected
複製代碼

必須顯示地檢查這個值不爲null:

if (email != null) sendEmailTo(email)
複製代碼

另一種處理方式:使用***let函數,並經過安全調用來調用它。let函數作的全部事情就是把一個調用它的對象變成lambda表達式的參數*。

foo?.let { ... it ... } 中
若:
foo != null   ->  在lambda內部it是非空的
foo == null  ->  什麼都不會發生
複製代碼

*let*函數只在email的值非空時才被調用,因此在lambda中把email看成非空的實參使用。

email?.let { email -> sendEmailTo(email) }
複製代碼

使用自動生成的名字it這種簡明語法周,上面的代碼就更短了:email?.let { sendEmailTo(it)

6、延遲初始化的屬性

  • 使用非空斷言訪問可空屬性
class MyService {
    fun performAction(): String = "foo"
}

class MyTest {
    // 聲明一個可空類型的屬性並初始化爲null
    private var myService: MyService? = null

    // 在setUp方法中提供真正的初始化器
    @Before fun setUp() {
        myService = MyService()
    }

    @Test fun testAction() {
        // 必須注意可空性:要麼用!!,要麼用?.
        Assert.assertEquals("foo",
            myService!!.performAction())
    }
}
複製代碼

上述代碼中能夠把myService屬性聲明成能夠延遲初始化的,使用***lateinit***修飾符來完成這樣的聲明。

  • 使用延遲初始化屬性
class MyService {
    fun performAction(): String = "foo"
}

class MyTest {
    // 聲明一個可空類型的屬性並初始化爲null
    private lateinit var myService: MyService? = null

    // 在setUp方法中提供真正的初始化器
    @Before fun setUp() {
        myService = MyService()
    }

    @Test fun testAction() {
        // 不須要null檢查直接訪問屬性
        Assert.assertEquals("foo",
            myService.performAction())
    }
}
複製代碼

注意:延遲初始化的屬性都是***var***,由於須要在構造方法外修改它的值,而val屬性會被編譯成必須在構造方法中初始化的final字段。 若是在屬性被初始化以前就訪問了它,會獲得這個異常「lateinit property myService has not been initialized」。

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

7、可空類性的擴展

可空類型定義擴展函數是一種更強大的處理***null值的方式。能夠容許接收者爲null的(擴展函數)調用,並在該函數中處理null,而不是在確保變量不爲null***以後再調用它的方法。只有擴展函數才能作到這一點,普通成員方法的調用時經過對象實例來分發的。

// 定義
fun verifyUserInput(input: String?) {
    // 這裏不須要安全調用
    if(input.isNullOrBlank()) {
        println("Please fill in the required fields");
    }
}

// 測試
>>> verifyUserInput(" ");
Please fill in the required fields
// 這個接受者調用isNullOrBlank並不會致使任何異常
>>> verifyUserInput(null)
Please fill in the required fields
複製代碼

上述測試方法中的***isNullOrBlank***的講解:

可空類型的值
|----| |-可空類型的擴展- |
input.isNullOrBlank()
    |-|
不須要安全調用
複製代碼

函數***isNullOrBlank***顯式地檢查了null,這種狀況下返回true,而後調用isBlank,它只能在非空String上調用:

// 可空字符串的擴展
fun String?.isNullOrBlank(): Boolean = 
    // 第二個「this」使用了智能轉換
    this == null || this.isBlank()
複製代碼

當一個可空類型(以?)定義擴展函數時,這意味着能夠對可空的值調用這個函數;而且函數體中的**this可能爲null,因此必須顯示地檢查**。而Java中,this永遠是非空的。

注意let函數也能被可空的接受者調用,但它並不檢查值是否爲null。若是在一個可空類型直接上調用***let***,而沒有使用安全調用運算符,lambda的實參將會是可空的:

10、類型參數的可控性

Kotlin中全部泛型類和泛型函數的類型參數默認都是可空的。

  • 處理可空的類型參數
fun <T> printHashCode(t: T) {
    // 由於「t」可能爲null,故必須使用安全調用
    println(t?.hashCode())
}

// 「T」被推導成「Any?」
>>> printHashCode(null)
null
複製代碼

在printHashCode調用中,類型參數***T推導出的類型是可空類型Any?***。所以,儘管沒有用問號結尾,實參t依然容許持有null。

  • 爲類型參數聲明非空上界
// 如今「T」就不是可空的
fun <T: Any> printHashCode(t: T) {
    println(t.hashCode())
}

// 這段代碼是沒法編譯的:
// 不能傳遞null,由於指望的是非空值
>>> printHashCode(null)
Error:Type parameter bound for T in fun <T : Any> printHashCode(t: T): Unit
 is not satisfied: inferred type Nothing? is not a subtype of Any
>>> printHashCode(42)
42
複製代碼

11、可空性和Java

針對可空,Java和Kotlin的一一對應關係:

Java -> Kotlin

@Nullable + Type = Type?

@NotNull + Type = Type

相關文章
相關標籤/搜索