Kotlin泛型[文檔]

泛型

與 Java 類似,Kotlin 中的類也可以有類型參數:

class Box<T>(t: T) {
    var value = t
}

一般來說,要創建這樣類的實例,我們需要提供類型參數:

val box: Box<Int> = Box<Int>(1)

但是如果類型參數可以推斷出來,例如從構造函數的參數或者從其他途徑,允許省略類型參數:

val box = Box(1) // 1 具有類型 Int,所以編譯器知道我們說的是 Box<Int>。

型變 ( Variance )

Java 類型系統中最棘手的部分之一是通配符類型(參見 Java Generics FAQ)。
而 Kotlin 中沒有。 相反,它有兩個其他的東西:聲明處型變(declaration-site variance)與類型投影(type projections)。

首先,讓我們思考爲什麼 Java 需要那些神祕的通配符。在 《Effective Java》第三版 解釋了該問題——第 31 條:利用有限制通配符來提升 API 的靈活性
首先,Java 中的泛型是不型變的,這意味着 List<String>不是 List<Object> 的子類型。
爲什麼這樣? 如果 List 不是不型變的,它就沒比 Java 的數組好到哪去,因爲如下代碼會通過編譯然後導致運行時異常:

// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!!即將來臨的問題的原因就在這裏。Java 禁止這樣!
objs.add(1); // 這裏我們把一個整數放入一個字符串列表
String s = strs.get(0); // !!! ClassCastException:無法將整數轉換爲字符串

因此,Java 禁止這樣的事情以保證運行時的安全。但這樣會有一些影響。例如,考慮 Collection 接口中的 addAll()
方法。該方法的簽名應該是什麼?直覺上,我們會這樣:

// Java
interface Collection<E> …… {
  void addAll(Collection<E> items);
}

但隨後,我們就無法做到以下簡單的事情(這是完全安全):

// Java
void copyAll(Collection<Object> to, Collection<String> from) {
  to.addAll(from);
  // !!!對於這種簡單聲明的 addAll 將不能編譯:
  // Collection<String> 不是 Collection<Object> 的子類型
}

(在 Java 中,我們艱難地學到了這個教訓,參見《Effective Java》第三版,第 28 條:列表優先於數組

這就是爲什麼 addAll() 的實際簽名是以下這樣:

// Java
interface Collection<E> …… {
  void addAll(Collection<? extends E> items);
}

通配符類型參數 ? extends E 表示此方法接受 E 或者 E 的 一些子類型對象的集合,而不只是 E 自身。
這意味着我們可以安全地從其中(該集合中的元素是 E 的子類的實例)讀取 E,但不能寫入
因爲我們不知道什麼對象符合那個未知的 E 的子類型。
反過來,該限制可以讓Collection<String>表示爲Collection<? extends Object>的子類型。
簡而言之,帶 extends 限定(上界)的通配符類型使得類型是協變的(covariant)

理解爲什麼這個技巧能夠工作的關鍵相當簡單:如果只能從集合中獲取項目,那麼使用 String 的集合,
並且從其中讀取 Object 也沒問題 。反過來,如果只能向集合中 放入 項目,就可以用
Object 集合並向其中放入 String:在 Java 中有 List<? super String>List<Object> 的一個超類

後者稱爲逆變性(contravariance),並且對於 List <? super String> 你只能調用接受 String 作爲參數的方法
(例如,你可以調用 add(String) 或者 set(int, String)),當然如果調用函數返回 List<T> 中的 T,你得到的並非一個 String 而是一個 Object

Joshua Bloch 稱那些你只能從中讀取的對象爲生產者,並稱那些你只能寫入的對象爲消費者。他建議:“爲了靈活性最大化,在表示生產者或消費者的輸入參數上使用通配符類型”,並提出了以下助記符:

PECS 代表生產者-Extens,消費者-Super(Producer-Extends, Consumer-Super)。

注意:如果你使用一個生產者對象,如 List<? extends Foo>,在該對象上不允許調用 add()set()。但這並不意味着該對象是不可變的:例如,沒有什麼阻止你調用 clear()從列表中刪除所有項目,因爲 clear()
根本無需任何參數。通配符(或其他類型的型變)保證的唯一的事情是類型安全。不可變性完全是另一回事。

聲明處型變

假設有一個泛型接口 Source<T>,該接口中不存在任何以 T 作爲參數的方法,只是方法返回 T 類型值:

// Java
interface Source<T> {
  T nextT();
}

那麼,在 Source <Object> 類型的變量中存儲 Source <String> 實例的引用是極爲安全的——沒有消費者-方法可以調用。但是 Java 並不知道這一點,並且仍然禁止這樣操作:

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

爲了修正這一點,我們必須聲明對象的類型爲 Source<? extends Object>,這是毫無意義的,因爲我們可以像以前一樣在該對象上調用所有相同的方法,所以更復雜的類型並沒有帶來價值。但編譯器並不知道。

在 Kotlin 中,有一種方法向編譯器解釋這種情況。這稱爲聲明處型變:我們可以標註 Source類型參數 T 來確保它僅從 Source<T> 成員中返回(生產),並從不被消費。
爲此,我們提供 out 修飾符:

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

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

一般原則是:當一個類 C 的類型參數 T 被聲明爲 out 時,它就只能出現在 C 的成員的輸出-位置,但回報是 C<Base> 可以安全地作爲
C<Derived>的超類。

簡而言之,他們說類 C 是在參數 T 上是協變的,或者說 T 是一個協變的類型參數。
你可以認爲 CT生產者,而不是 T消費者

out修飾符稱爲型變註解,並且由於它在類型參數聲明處提供,所以我們稱之爲聲明處型變
這與 Java 的使用處型變相反,其類型用途通配符使得類型協變。

另外除了 out,Kotlin 又補充了一個型變註釋:in。它使得一個類型參數逆變:只可以被消費而不可以被生產。逆變類型的一個很好的例子是 Comparable

interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 擁有類型 Double,它是 Number 的子類型
    // 因此,我們可以將 x 賦給類型爲 Comparable <Double> 的變量
    val y: Comparable<Double> = x // OK!
}

我們相信 inout 兩詞是自解釋的(因爲它們已經在 C# 中成功使用很長時間了),
因此上面提到的助記符不是真正需要的,並且可以將其改寫爲更高的目標:

存在性(The Existential) 轉換:消費者 in, 生產者 out! 😃

類型投影

{:#使用處型變類型投影}

使用處型變:類型投影

將類型參數 T 聲明爲 out 非常方便,並且能避免使用處子類型化的麻煩,但是有些類實際上不能限制爲只返回 T
一個很好的例子是 Array:

class Array<T>(val size: Int) {
    fun get(index: Int): T { …… }
    fun set(index: Int, value: T) { …… }
}

該類在 T 上既不能是協變的也不能是逆變的。這造成了一些不靈活性。考慮下述函數:

fun copy(from: Array<Any>, to: Array<Any>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}

這個函數應該將項目從一個數組複製到另一個數組。讓我們嘗試在實踐中應用它:

val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" } 
copy(ints, any)
//   ^ 其類型爲 Array<Int> 但此處期望 Array<Any>

這裏我們遇到同樣熟悉的問題:Array <T>T 上是不型變的,因此 Array <Int>Array <Any> 都不是另一個的子類型。爲什麼? 再次重複,因爲 copy 可能做壞事,也就是說,例如它可能嘗試一個 String 到 from
並且如果我們實際上傳遞一個 Int 的數組,一段時間後將會拋出一個 ClassCastException 異常。

那麼,我們唯一要確保的是 copy() 不會做任何壞事。我們想阻止它from,我們可以:

fun copy(from: Array<out Any>, to: Array<Any>) { …… }

這裏發生的事情稱爲類型投影:我們說from不僅僅是一個數組,而是一個受限制的(投影的)數組:我們只可以調用返回類型爲類型參數
T 的方法,如上,這意味着我們只能調用 get()。這就是我們的使用處型變的用法,並且是對應於 Java 的 Array<? extends Object>
但使用更簡單些的方式。

你也可以使用 in 投影一個類型:

fun fill(dest: Array<in String>, value: String) { …… }

Array<in String> 對應於 Java 的 Array<? super String>,也就是說,你可以傳遞一個 CharSequence 數組或一個 Object 數組給 fill() 函數。

星投影

有時你想說,你對類型參數一無所知,但仍然希望以安全的方式使用它。
這裏的安全方式是定義泛型類型的這種投影,該泛型類型的每個具體實例化將是該投影的子類型。

Kotlin 爲此提供了所謂的星投影語法:

  • 對於 Foo <out T : TUpper>,其中 T 是一個具有上界 TUpper 的協變類型參數,Foo <*> 等價於 Foo <out TUpper>。 這意味着當 T 未知時,你可以安全地從 Foo <*> 讀取 TUpper 的值。
  • 對於 Foo <in T>,其中 T 是一個逆變類型參數,Foo <*> 等價於 Foo <in Nothing>。 這意味着當 T 未知時,沒有什麼可以以安全的方式寫入 Foo <*>
  • 對於 Foo <T : TUpper>,其中 T 是一個具有上界 TUpper 的不型變類型參數,Foo<*> 對於讀取值時等價於 Foo<out TUpper> 而對於寫值時等價於 Foo<in Nothing>

如果泛型類型具有多個類型參數,則每個類型參數都可以單獨投影。
例如,如果類型被聲明爲 interface Function <in T, out U>,我們可以想象以下星投影:

  • Function<*, String> 表示 Function<in Nothing, String>
  • Function<Int, *> 表示 Function<Int, out Any?>
  • Function<*, *> 表示 Function<in Nothing, out Any?>

注意:星投影非常像 Java 的原始類型,但是安全。

泛型函數

不僅類可以有類型參數。函數也可以有。類型參數要放在函數名稱之前

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

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

要調用泛型函數,在調用處函數名之後指定類型參數即可:

val l = singletonList<Int>(1)

可以省略能夠從上下文中推斷出來的類型參數,所以以下示例同樣適用:

val l = singletonList(1)

泛型約束

能夠替換給定類型參數的所有可能類型的集合可以由泛型約束限制。

上界

最常見的約束類型是與 Java 的 extends 關鍵字對應的 上界

fun <T : Comparable<T>> sort(list: List<T>) {  …… }

冒號之後指定的類型是上界:只有 Comparable<T> 的子類型可以替代 T。 例如:

sort(listOf(1, 2, 3)) // OK。Int 是 Comparable<Int> 的子類型
sort(listOf(HashMap<Int, String>())) // 錯誤:HashMap<Int, String> 不是 Comparable<HashMap<Int, String>> 的子類型

默認的上界(如果沒有聲明)是 Any?。在尖括號中只能指定一個上界。
如果同一類型參數需要多個上界,我們需要一個單獨的 where-子句:

fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
    where T : CharSequence,
          T : Comparable<T> {
    return list.filter { it > threshold }.map { it.toString() }
}

所傳遞的類型必須同時滿足 where 子句的所有條件。在上述示例中,類型 T 必須實現了 CharSequence 實現了 Comparable

類型擦除

Kotlin 爲泛型聲明用法執行的類型安全檢測僅在編譯期進行。
運行時泛型類型的實例不保留關於其類型實參的任何信息。
其類型信息稱爲被擦除。例如,Foo<Bar>Foo<Baz?> 的實例都會被擦除爲
Foo<*>

因此,並沒有通用的方法在運行時檢測一個泛型類型的實例是否通過指定類型參數所創建
,並且編譯器禁止這種 is{: .keyword } 檢測

類型轉換爲帶有具體類型參數的泛型類型,如 foo as List<String> 無法在運行時檢測。
當高級程序邏輯隱含了類型轉換的類型安全而無法直接通過編譯器推斷時,
可以使用這種非受檢類型轉換。編譯器會對非受檢類型轉換髮出警告,並且在運行時只對非泛型部分檢測(相當於 foo as List<*>)。

泛型函數調用的類型參數也同樣只在編譯期檢測。在函數體內部,
類型參數不能用於類型檢測,並且類型轉換爲類型參數(foo as T)也是非受檢的。然而,
內聯函數的具體化的類型參數會由調用處內聯函數體中的類型實參所代入,因此可以用於類型檢測與轉換,
與上述泛型類型的實例具有相同限制。

文檔地址

https://www.kotlincn.net/docs/reference

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