1. 程式人生 > >Kotlin自學之旅(八)型別系統

Kotlin自學之旅(八)型別系統

可空型別

Kotlin 和 Java 的型別系統之間第一條也可能是最重要的一條區別是, Kotlin 對可空型別的顯式的支援。這是一種指出你的程式中哪些變數和屬性允許為 null 的方式。如果一個變數可以為 null ,對變數的方法的呼叫就是不安全的,因為這樣會導致 NullPointerException 。
Kotlin使用在型別名稱後面加問號的方式來表示這個型別的變數可以儲存null引用,String?、 Int?、 MyCustomType?,等等,而沒有問號的型別表示這種型別的變數不能儲存null引用,這也說明所有常見型別預設都是非空的,除非顯式地把它標記為可空。一旦你有一個可空型別的值,你就不能再呼叫它的方法,也不能把它賦值給非空型別的變數:

//Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
fun strLenSafe(s: String?) = s.length


val x: String? = null
var y: String = x //Type mismatch. Required: String. Found: String?

但是如果你對一個可空變數與null進行了比較操作,那麼在這次比較發生的作用域裡,你可以把這個變數當作非空變數使用:

fun strLenSafe(s: String
?): Int { //return s.length //報錯 if (s != null) { return s.length //OK } return 0 }

處理可空型別

事實上,Kotlin 有比if-else更好的方法來處理可空變數,比如安全呼叫運算子:?. ,這個運算子將一次null檢查和一次方法呼叫合併成一個操作,舉個例子,以下兩個語句是等價的:

var result1 = s?.toUpperCase()
var result2 = if (s == null) null else s.toUpperCase()

也就是說,如果這個使用 ?.

運算子可空變數不為空,這個方法會正常的執行,否則這次呼叫不會發生,而整個表示式的值是null。此外,要注意,這個呼叫的結果型別也是可空的,因為它可能為 null。
?. 會在呼叫變數為null的時候返回null,但有些時候,我們更想在變數為null的時候返回一個預設值,比如開始的例子,我們會在字串s為null的時候把它當作長度為0的字串 “”。這個時候,我們可以使用另一個null合併運算子: ?: ,我們可以使用它把strLenSafe改造得簡單一點:

fun strLenSafe(s: String?): Int  {
    val result = s?:""
    return  result.length
}

?:運算子接收兩個引數,如果第一個引數不為null,就返回第一個引數,否則返回第二個引數。
我們還可以把這兩個運算子一起使用:

fun strLenSafe(s: String?): Int  {
    return  s?.length?:0
}

上面這個表示式 先使用 ?. 運算子得到一個Int?型別的返回值,然後再使用 ?: 運算子在返回值為null的時候返回0作為結果。
上面兩個運算子用來處理需要安全檢查變數是否為空的情況,還有一個常見的情形是把一個變數轉換成某個型別,這個時候也有一個安全檢查的運算子。
在Kotlin中,有一個和常規Java型別轉換一樣的 as 運算子,這個運算子的使用方式就像下面這樣:

fun castNumberToInt(number: Number):Int {
    return number as Int
}

println(castNumberToInt(15)) //15
println(castNumberToInt(12.3)) //java.lang.ClassCastException

as 運算子在被轉換的值不是你試圖轉換的型別時,就會丟擲一個ClassCastException異常。 這顯然很是麻煩,如果我們想不丟擲異常的話,就要在每次使用 as 之前用 is 檢查來確保型別正常。於是這個時候我們就需要用到 as? 了。as?運算子會在被轉換的值和轉換型別不匹配的時候返回null:

fun castNumberToInt(number: Number):Int? {
    return number as? Int
}

println(castNumberToInt(15))  //15
println(castNumberToInt(12.3)) //null

同樣的,我們也可以將 as? 運算子和 ?:運算子一起使用,以在轉換失敗之後返回一個預設值:

  fun castNumberToInt(number: Number):Int {
    return number as? Int?:-1
 } 

延遲初始化

Kotlin通常要求我們在構造方法中初始化所有屬性,如果某個屬性是非空型別,我們就必須提供非空的初始化值。否則,就必須使用可空型別,但是可空型別每次使用的時候都必須進行繁瑣的null檢查,尤其是這個屬性要經常時候的時候。為了解決這個問題,我們可以在宣告一個不想給初始化值的屬性的時候,給它加上 lateinit 修飾符:

class Person() {
    //var name:String    //error:Property must be initialized or be abstract
    lateinit var name:String
    //lateinit var age:Int //error:'ateinit' modifier is not allowed on properties of primitive types
    var age:Int = -1
}

var xiaoming = Person()
println(xiaoming.name) //error:lateinit property name has not been initialized

延遲初始化的屬性都是 var,因為需要在構造方法外修改它的值 , 而 val 屬性會被編譯成必須在構造方法中初始化的 final 欄位 。如果在屬性被初始化之前就訪問了它,會得到這個異常 “lateinit property name has not been initialized” 。 該異常清楚地說明了發生了什麼。
此外還要注意的是,Int這樣的基本型別是不能延遲初始化的,因為事實上Kotlin會使用null來對每一個用lateinit修飾的屬性做初始化,而基礎型別是沒有null型別的,所以像我上面那樣對一個基本型別使用 lateinit 的時候就會報錯。

資料型別

眾所周知, Java 把基本資料型別和引用型別做了區分。一個基本資料型別(如int)的變數直接儲存了它的值,而一個引用型別(如 String )的變數儲存的是指向包含該物件的記憶體地址的引用。基本資料型別的值能夠更高效地儲存和傳遞,但是你不能對這些值呼叫方法,或是把它們存放在集合中。為此Java提供了包裝型別。
而Kotlin則並不區分基本資料型別和包裝型別,對於Int之類的基本型別來說,我們使用的總是同一型別,你可以對把它儲存在集合裡,也可以對它呼叫函式:

val i:Int = 1
val list:List<Int> = listOf(1,2,3)
println(i.dec())

基本型別

雖然看起來Kotlin是在使用物件來表示所有的數字,但事實上 ,在執行時,數字型別會盡量使用高效的方式表示,對於變數、屬性、引數和返回型別——Kotlin 的 Int 型別會被編譯成 Java 基本資料型別 int 。 用作泛型型別引數的基本資料型別會被編譯成對應的 Java 包裝型別 。
像 Int 這樣的 Kotlin 型別在底層可以輕易地編譯成對應的 Java 基本資料型別,因為兩種型別都不能儲存 null 引用。而對於可空的基本資料型別,因為 null 只能被儲存在 Java 的引用型別的變數中。這意味著任何時候只要使用了基本資料型別的可空版本,它就會編譯成對應的包裝型別。

數字轉換

Kotlin 和 Java 之間一條重要的區別就是處理數字轉換的方式 。 Kotlin 不會自動地把數字從一種型別轉換成另外一種,即便是轉換成範圍更大的型別。例如,Kotlin中下面這段程式碼不會編譯,而必須使用顯示轉換:

val i:Int = 1
//val l: Long = i  //Type mismatch.
val l: Long = i.toLong()

每一種基本資料型別( Boolean 除外)都定義有轉換函式:toByte()、toShort()、toChar()等。這些函式支援雙向轉換:既可以把小範圍的型別括展到大範圍, 比如 Int.toLong(),也可以把大範圍的型別擷取到小範圍,比如Long.toInt()。
為了避免意外情況 ,Kotlin 要求轉換必須是顯式的,尤其是在比較裝箱值的時候。比較兩個裝箱值的 equals 方法不僅會檢查它們儲存的值,還要比較裝箱型別,因為Kotiin 要求只有型別相同的值才能比較。

根型別和空型別

和 Object 作為 Java 類層級結構的根差不多, Any 型別是 Kotlin 所有非空型別的超型別(非空型別的根 )。但是在 Java 中, Object 只是所有引用型別的超型別(引用型別的根),而基本資料型別並不是類層級結構的一部分。這意味著當你需Object的時候,不得不使用像 java.lang.Integer 這樣的包裝型別來表示基本資料型的值。而在 Kotlin 中, Any 是所有型別的超型別(所有型別的根),包括像 Int 這樣的基本資料型別 。
Any 是非空型別 ,所以 Any 型別的變數不可以持有 null 值 。 在 Kotlin中如果你需要可以持有任何可能值的變數,包括 null 在內,必須使用 Any?型別 。
而Kotlin 中的 Unit 型別完成了 Java 中的 void 一樣的功能。當函式沒什麼有意義的結果要返回時,可以使用它作為函式的返回型別。Unit和void的差別在於Unit是一個完備的型別,可以作為型別引數,而void卻不行。只存在一個值是Unit型別,這個值也叫作 Unit,並且(在函式中)會被隱式地返回。
對某些 Kotlin 函式來說,“返回型別”的概念沒有任何意義,這時候知道函式永遠不會正常終止是很有幫助的。Kotiin使用一種特殊的返回型別Nothing來表示。Nothing型別沒有任何值,只有被當作函式返回值使用,或者被當作泛型函式返回值的型別引數使用才會有意義。在其他所有情況下,宣告一個不能儲存任何值的變數沒有任何意義 。

集合

Kotlin 以 Java集合庫為基礎構建,並通過擴充套件函式增加的特性來增強它。

集合的可空性

Kotlin完全支援型別引數的可空性。 就像變數的型別可以加上 ?字元來表示變數科二以持有 null 一樣,型別在被當作型別引數時也可以用同樣的方式標記:

 val list = ArrayList<Int?>()

注意,變數自己型別的可空性和用作型別引數的型別的可空性是有區別的。在第一種情況下 , 列表本身始終不為 null ,但列表中的每個值都可以為null 。 第二種型別 的變數可能包含空引用而不是列表例項,但列表中的元素保證是非空的 。

只讀集合和可變集合

Kotlin 的 集合設計和 Java 不同的一項重要特質是,它把訪問集合資料的介面和修改集合資料的介面分開了 。 這種區別存在於最基礎的使用集合的介面之中 :kotlin .collections.Collection。使用這個介面,可以遍歷集合中的元素、獲取集合大小、判斷集合中是否包含某個元素,以及執行其他從該集合中讀取資料的操作。但這個介面沒有任何新增或移除元素的方法 。而使用 kotlin.collections.MutableCollection 介面可以修改集合中的資料。它繼承了普通的 kotlin.collections.Collection介面,還提供了方法來新增和移除元素、清空集合等。
但事實上,使用集合介面時需要牢記的一個關鍵點是隻讀集合不一定是不可變的。如果你使用的變數擁有一個只讀介面型別,它可能只是同一個集合的眾多引用中的一個。任何其他的引用都可能擁有一個可變介面型別,如果你正在使用集合的時候它被其他程式碼修改了,這會導致 concurrentModificationException 錯誤和其他一些問題。因此,必須瞭解只讀集合並不總是執行緒安全的。

陣列

Kotiin中的一個數組是一個帶有型別引數的類,其元素型別被指定為相應的型別引數。要在 Kotlin 中建立陣列,可以選擇下面的方式:

  • arrayOf 函式建立一個數組,它包含的元素是指定為該函式的實參
  • arrayOfNulls 建立一個給定大小的陣列,包含的是 null 元素。當然,它只能用來建立包含元素型別可空的陣列。
  • Array 構造方法接收陣列的大小和一個 lambda 表示式,呼叫 lambda 表示式
    來建立每一個數組元素 。

和其他型別一樣,陣列型別的型別引數始終會變成物件型別.因此如果你聲明瞭 一個Array< Int>,它將會是一個包含裝箱整型的陣列(它的 Java 型別將是java.lang . Integer[])。
為了表示基本資料型別的陣列, Kotlin 提供了若干獨立的類,每一種基本資料型別都對應一個。例如,Int 型別值的陣列叫作IntArray。Kotlin 還提供了ByteArray、CharArray、BooleanArray等給其他型別。所有這些型別都被編譯成普通的Java基本資料型別陣列,比如 int[]、byte[]、char[]等。因此這些陣列中的值儲存時並沒有裝箱,而是使用了可能的最高效的方式。

總結

Kotlin對可空型別的支援,可以幫助我們在編譯期,檢測出潛在的NullPointerException錯誤。同時提供了像安全呼叫(?.)、Elvis 運算子(?:)這樣的工具來簡潔地處理可空型別。Kotlin使用標準 Java 集合類,並通過區分只讀和可變集合來增強它們 。