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>。

型變

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

首先,讓我們思考爲什麼 Java 需要那些神祕的通配符。在 Effective Java 解釋了該問題——第28條:利用有限制通配符來提升 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,第25條:列表優先於數組)

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

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

通配符類型參數 ? extends 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 修飾符:

abstract class Source<out T> {
    abstract 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

abstract class Comparable<in T> {
    abstract 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<Any>, 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>,其中 T 是一個具有上界 TUpper 的協變類型參數,Foo <*> 等價於 Foo <out TUpper>。 這意味着當 T 未知時,你可以安全地從 Foo <*> 讀取 TUpper 的值。
  • 對於 Foo <in T>,其中 T 是一個逆變類型參數,Foo <*> 等價於 Foo <in Nothing>。 這意味着當 T 未知時,沒有什麼可以以安全的方式寫入 Foo <*>
  • 對於 Foo <T>,其中 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)

泛型約束

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

上界

最常見的約束類型是與 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> cloneWhenGreater(list: List<T>, threshold: T): List<T>
    where T : Comparable,
          T : Cloneable {
  return list.filter { it > threshold }.map { it.clone() }
}
0 條評論,你可以發表評論,我們會進行改進
Comment author placeholder