Kotlin入門-似曾相識的泛型

在這裏插入圖片描述

前言

在Kotlin中,總的來說,可以理解泛型爲:
①向上解決類型不通用
②向下解決類型限定

如果看着累,建議先看小結,寥寥幾字,大致瞭解下。

需要理解幾個問題?
① 理解 型變是什麼?逆變又是什麼?
② 泛型存在的價值是什麼?
③ 泛型註解out、in有什麼用?Invariant又是什麼?
④ 類型擦除 有什麼影響?

本節的目錄結構是這樣的

  • 泛型說明
  • 型變
  • 類型投影
  • 泛型函數
  • 泛型約束
  • 類型擦除

泛型說明

Kotlin的泛型,功能與 Java 一樣

看一個範例
fun main(args: Array<String>) {
    val age = 23
    val name = "runoob"
    val bool = true

    doPrintln(age)    // 整型
    doPrintln(name)   // 字符串
    doPrintln(bool)   // 布爾型
}

fun <T> doPrintln(content: T) {
    when (content) {
        is Int -> println("整型數字爲 $content")
        is String -> println("字符串轉換爲大寫:${content.toUpperCase()}")
        else -> println("T 不是整型,也不是字符串")
    }
}

輸出了什麼呢

整型數字爲 23
字符串轉換爲大寫:RUNOOB
T 不是整型,也不是字符串

這便是泛型了,範例中允許接受不同的類型,類型通用。


型變

即類型轉變。通過轉變類型,提升API的適配度。

java爲什麼需要通配符來提升API的靈活性?

我們來看看Java的型變是怎麼樣的。

  • Java的泛型是不型變的
  • 要想實現多類型的列表存儲,就需要有很多個不同的定義,這就很麻煩了
  • 必須要有一個 xxx 提供一個通用的解決方案,
    否則,列表跟數組又有何區別~

聲明處型變(declaration-site variance)

即在聲明變量、參數時就確定T的類型。

說起來有點繞口,來看範例,進行一下對比

// Java

void demo(Source<String> strs) {
  Source<Object> objects = strs; // !!!在 Java 中不允許
  // ……
}

// Kotlin

interface Source<out T> {
    fun nextT(): T
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // 這個沒問題,因爲 T 是一個 out-參數
    // ……
}

注意看這裏Source部分

  • java是不支持直接轉變的
  • Kotlin支持

在Source是使用處,Kotlin的型變解決了 子類型化的麻煩。
這裏引入一個概念,型變註解:這裏的out
同樣的型變註解還有 in


類型投影(type projections)

投影就是 某事物的影子,影子之下才有天地。
只有在影子內的東西方可以動。

說的就是 限定了。

使用處型變:類型投影

這講的是一種將 某個類型投影 到某處。
你想想一下,只允許投影之下的暗處,這不正是限制嗎?

類型投影,不就是類型限制嗎?怎麼限定呢?通過out\in的類型註解。

來看個範例
fun copy(from: Array<Any>, to: Array<Any>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}
fun main() {
   val ints: Array<Int> = arrayOf(1, 2, 3)
   val any = Array<Any>(3) { "" } 
   copy(ints, any)  //  這裏編譯是不通過的,ints類型爲 Array<Int> 但此處期望 Array<Any>
}
會碰上什麼問題呢?

Array 在 T 上是不型變的,因此 Array 和 Array 都不是另一個的子類型。

爲什麼? 再次重複,因爲 copy 可能做壞事。爲了避免這種錯誤,編譯器禁止了這種操作。

我們唯一要確保的是 copy() 不會做任何壞事。我們想阻止它寫到 from去。

fun copy(from: Array, to: Array) { …… }

這便是類型投影。所有out(協變)是輸出,in(逆變)是輸入。

複習一下out、in

對於 out 泛型,可以將使用子類泛型的對象 -> 賦值給使用父類泛型的對象。
對於 in 泛型,可以將使用父類泛型的對象 -> 賦值給使用子類泛型的對象。

星投影

這使用於並不知道類型參數的任何信息。 * 代表通喫。
這個泛型類型的所有的實體實例, 都是這個投射的子類型。

看個例子
Function<*, String>

這裏用到了*

var list:ArrayList<> = arrayListOf(1) //<>必不可少 相當於java的無泛型
當 cList:ArrayList<> = javaList 時<>相當於
當 javaList:ArrayList<> = cList 時<>相當於

這就是什麼都接收了


泛型函數

在簡單的學習下,泛型的函數是怎麼編寫的

fun <T> singletonList(item: T): List<T> {
    // ……
}

fun <T> T.basicToString(): String {  // 擴展函數
    // ……
}

這麼用
val l = singletonList(1)
可以省略能夠從上下文中推斷出來的類型參數
val l = singletonList(1)


泛型約束

這跟java是類似的
//泛型約束 <佔位符:類型>

fun <T:Number> play(vararg param: T):Double{
    return param.sumByDouble { it.toDouble() }
}

//多個約束,T有多個上限 , where T:類型,T:類型

fun <T> getBetterBig(list:Array<T>,threhold:T):List<T> where T:Number,T:Comparable<T>{
    return list.filter { it>= threhold }.sorted()
}

類型擦除

泛型聲明的類型安全檢測是僅在編譯期進行的。
例如,Foo 與 Foo<Baz?> 的實例都會被擦除爲 Foo<*>。

運行時泛型類型的實例不保留關於其類型實參的任何信息。

運行時不保留其類型實參的任何信息,即類型擦除。

編譯器會禁止由於類型擦除而無法執行的 is 檢測

看個熟悉的範例
fun handleStrings(list: List<String>) {
    if (list is ArrayList) {
        // `list` 會智能轉換爲 `ArrayList<String>`
    }
}

泛型函數調用的類型參數也同樣只在編譯期檢

這便是
在編譯器會進行安全類型檢查,在運行期則擦除類型,相當於是*類型


小結

泛型,牽扯到 Out (協變)、In(逆變)、Invariant(不變),這是必須要理解的部分。

所謂泛型,即 類型限制,類型轉變的稱呼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章