Kotlin進階知識(十一)——變型:泛型和子類型化

變型:描述擁有相同基礎類型和不一樣類型實參的(泛型)類型之間是如何關聯的,例如,List和List之間如何關聯。安全

1、爲何存在變型:給函數傳遞實參

子類型:任什麼時候候若是須要的是類型A的值,都可以使用類型B的值(看成A的值),類型B就稱爲類型A的子類型markdown

超類型:是子類型的反義詞。若是A是B的子類型,那麼B就是A的超類型。函數

  • 檢查一個類型是不是另外一個的子類型
fun test1(i: Int) {
    // 編譯經過,由於Int是Number的子類型
    val n: Number = i

    fun f(s: String) { println(s) }
    // 不能編譯,由於Int 不是String的子類型
    f(i)
}
複製代碼

一個非空類型是它的可空版本的子類型,但它們都對應着同一個類。你始終能在可空類型的變量中存儲非空類型的值,但反過來卻不行(null不是非空類型的變量能夠接受的值)。oop

2、協變:保留子類型化關係

一個協變類是一個泛型類(以Producer爲例),對這種類來講,下面的描述是成立的:若是A是B的子類型,那麼Producer就是Producer的子類型。咱們說子類型被保留了。ui

在Kotlin中,要聲明類在某個類型參數上是能夠協變的,在該類型參數的名稱前加上out關鍵字便可:spa

// 類被聲明成在T上協變
interface Producer<out T> {
    fun produce(): T
}
複製代碼
  • 定義一個不變型的相似集合的類
open class Animal {
    fun feed() { ... }
}

// 類型參數沒有聲明成協變的
class Herd<T: Animal> {
    val size: Int get() = ...

    operator fun get(i: Int): T { ... }
}
複製代碼
  • **使用一個不變型的相似集合的類
// Cat是一個Animal
class Cat: Animal() {
    fun cleanLitter() { ... }
}

fun takeCareOfCats(cats: Herd<Cat>) {
    for(i in 0 until cats.size) {
        cats[i].cleanLitter()
        // 錯誤:推導的類型是Herd<Cat>,但指望的倒是Herd<Animal>
        // feedAll(cats)
    }
}
複製代碼

若是嘗試把貓羣傳給feedAll()函數,在編譯期會獲得類型不匹配的錯誤。由於Herd類中的類型參數T沒有任何變型修飾符,貓羣不是畜羣的子類。code

於是可使用協變來解決。orm

  • 使用一個協變的相似集合的類
// 類型參數T如今是協變的
class Herd<out T: Animal> {
    ...
}
複製代碼

不能把任何類都變成協變的:這樣不安全。讓類在某個類型參數變成協變,限制了該類中對該參數使用的可能性。接口

圖1:函數參數的類型叫作in位置,而函數返回類型叫做out位置

圖1中,類的類型參數out關鍵字要求全部使用T的方法只能把T放在out位置而不能放在in位置。這個關鍵字約束了使用T的可能性,這保證了對應子類型關係的安全性get

class Herd<out T: Animal> {
    val size: Int get() = ...
    
    // 把T做爲返回類型使用
    operator fun get(i: Int): T { ... }
}
複製代碼

這裏一個out位置,能夠安全地把類聲明成協變的。

重申一下,類型參數T上的關鍵字out有兩層含義:

  • 子類型化被保留(Producer是Producer的子類型)
  • T只能用在out位置

類型參數不光能夠直接看成參數類型或者返回類型使用,還能夠看成另外一個類型類型實參

注意構造方法參數既不在in位置,也不在out位置

位置規則只覆蓋了類外部可見的(publicprotectedinternalAPI私有方法的參數既不在in位置也不在out位置

變型規則只會防止外部使用者對類的誤用但不會對類本身的實現起做用:

class Herd<out T: Animal>(private var leadAnimal: T, varage animals: T) { ... }
複製代碼

3、逆變:反轉子類型化關係

逆變的概念能夠被當作協變的鏡像:對一個逆變類來講,它的子類型化關係與用做類型實參的類的子類型化關係是相反的。

interface Comparator<in T> {
    // 在「in」位置使用T
    fun compare(e1: T, e2: T): Int { ... }
}
複製代碼

這個接口方法只是消費類型爲T的值。這說明T只在in位置使用,所以它的聲明以前用了in關鍵字。

in關鍵字的含義是:對應類型的值是傳遞進來給這個類的方法的,而且被這些方法消費。

表1 協變的、逆變的和不變型的表

協變 逆變 不變型
Producer Consumer MutableList
類的子類型化保留了:Producer是Producer的子類型 子類型化反轉了:Consumer 是 Consumer的子類型 沒有子類型化
T只能在out位置 T只能在in位置 T能夠在任何位置

4、使用點變型:在類型出現的地方指定變型

**聲明點變型:**在類聲明時就可以制定變型修飾符是很方便的,由於這些修飾符會應用到全部類被使用的地方。

Java中,每一次使用帶類型參數的類型的時候,還能夠制定這個類型參數是否能夠用它的子類型或者超類型替換,這叫做使用點變型

  • 帶out投影類型參數的數據拷貝函數
// 能夠給類型的用法加上「out」關鍵字:沒有使用那些T用在「in」位置的方法
fun <T> copyDataByOut(source: MutableList<out T>, destination: MutableList<T>) {
    for(item in source) {
        destination.add(item)
    }
}
複製代碼
  • 帶in投影類型參數的數據拷貝函數
// 容許目標元素的類型是來源元素類型的超類型
fun <T> copyDataByIn(source: MutableList<T>, destination: MutableList<in T>) {
    for(item in source) {
        destination.add(item)
    }
}
複製代碼

注意: Kotlin使用點類型直接對應**Java限界通配符**。Kotlin中的MutableList<out T>和Java中的MutableList<? extends T>是一個意思。in投影的MutableList<in T>對應到Java的MutableList<? super T>

5、星號投影:使用 * 代替類型參數

星號投影語法,代表不知道關於泛型實參的任何信息

星號投影的語義:

  • 須要注意的是**MutableList<*>MutableList<Any?>** 不同(這裏很是重要的是MutableList<T>在T上是不可變的)。
  • MutableList<*>這種列表包含的是任何類型的元素
  • MutableList<*>是包含某種特定類型元素的列表

使用星號投影的語法:不須要使用任何在簽名中引用類型參數的方法,或者只是讀取數據不關心它的具體類型

// 每一種列表都是可能的實參
fun printFirst(list: List<*>) {
    // isNotEmpty()沒有使用泛型類型參數
    if(list.isNotEmpty())
        // first()如今返回的是Any?,可是這裏足夠了
        println(list.first())
}

fun printFirstTest() {
    println(printFirst(listOf("Svetlana", "Dmitry")))
}
複製代碼

在使用點類型的狀況下,有一個替代方案——引入一個泛型類型參數:

// 再一次,每一種列表都是可能的實參
fun <T> printFirstByT(list: List<T>) {
    if(list.isNotEmpty()) {
        // first()如今返回的實T的值
        println(list.first())
    }
}
複製代碼

星號投影的語法很簡潔,但只能用在對泛型類型實參的確切值不感興趣的地方:只是使用生產值的方法,並且不關心那些值的類型

相關文章
相關標籤/搜索