Kotlin學習之-5.8 泛型

Kotlin學習之-5.8 泛型

和Java中一樣,Kotlin 也可以使用類型參數:

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

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

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

但是如果參數的類型可以推斷出來,例如,從構造器的參數或者其他方法可以推斷出類型,這種情況下可以神略類型參數:

val box = Box(1) // 參數‘1’是Int類型,所以編譯器可以推斷出類型參數是Int類型的

變量

在Java類型系統中比較難理解的是wildcard類型。詳見Java泛型。 然而Kotlin中沒有wildcard。 在Kotlin中,有兩個其他東西來替代:declaration-site variancetype projections.
首先,我們想一下爲什麼Java需要這些謎一樣的wildcard。 這個問題在《Effective Java》中解釋過,第28條:Use bounded wildcards to increase API flexibility。第一,Java中的泛型類型是invariant的,意味着List<String>類型不是List<Object>類型的子類型。爲什麼這樣?如果List不是invariant的,那麼它就比Java 的數組Array好不到哪去,因爲如下的代碼就可以編譯通過,但是導致運行時錯誤。

// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // **注意** 這裏會編譯失敗,Java不支持
objs.add(1);
String s = strs.get(0); // **ClassCastException**:Cannot cast Integer to String

因此,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> 的自雷
}

這就是爲什麼addAll()真正的簽名是這樣的:

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

這裏的Wildcard type argument ? extends E 表明這個方法接受一個集合,這個集合的元素的類型都是E 類型的子類,而不是E 本身。這意味着我們可以安全的從元素中讀取E(這個集合的元素是E的子類的實例),但是不能寫入,因爲我們不知道什麼對象可以和那個不知道類型的E 的子類的類型匹配。 因爲這個問題的限制,我們有了想要的行爲:Collection<String>Collection<? extends Object>的子類。行話就是,對於繼承受限的通配符可以讓一個類型協變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 說這些對象你只能從生產者中讀取, 只能寫到消費者中。

“For maximum flexibility, use wildcard types on input parameters that represent producers or consumers”

並且提出了下面的助記方法

PECS stands for Producer-Extends, Consumer-Super.

注意:如果你使用一個生產者對象,假如是List<? extends Foo>, 你不能調用它的add()set() 方法, 但是這並不意味着這個對象是不可變的。例如,沒什麼可以阻止你調用clear()方法來移除所有列表中的元素。因爲clear()方法沒有任何參數。Wildcard 只是用來保證類型安全的。可變性是另外一個不同的問題。

聲明站變量(Declaration-site variance)

假設我們有一個泛型接口Source<T>,這個接口沒有任何方法使用T作爲參數,只是有方法返回T

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

這樣就可以非常安全的使用Source<Object>引用一個Source<String>對象,因爲沒有消費者方法可以調用。但是Java並不知道這些,仍然禁止這樣使用。

void demo(Source<String> strs) {
    Source<Object> objects = strs; // **Java中禁止**
}

爲了解決這個問題,我們必須使用Source<? extends Object>類型來聲明對象,但他其實沒有意義,因爲我們可以用和之前一樣的對象調用它所有的方法,所以沒有任何意義把這個弄成複雜的類型。但是編譯器並不知道這些。

Kotlin中,有一個辦法可以給編譯器解釋這種類型的問題。這就叫做“聲明站變量”declaration-site variance,我們可以給參數類型T的代碼做註解,來保障它只會被Source<T>的成員返回或者生產,並且永遠不會被消費。我們使用out修飾符來完成這個功能。

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

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs
}

基本規則是:當一個類C的參數類型T被聲明成out後,它就只能出現在類C成員的out位置,這樣C<Base>就可以安全當做C<Derived>的父類了。

簡單的說,在參數T中類C是協變的,或者T是一個協變類型的參數。你可以把C當做T的生產者,而不是一個T的消費者。

修飾符out被稱作變量註解,並且因爲它用在類型參數聲明站,所以我們稱爲聲明站變量。 這和Java中的使用站變量相反的,在使用站變量中在類型中使用wildcard會讓類型編程協變的。

除了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 Transformation: Consumer in, Producer out! :-)

類型映射

使用站變量:類型映射

現在非常方便地可以把一個類型參數T定義成out,並且避免在使用站中遇到子類的問題,但是有些類實際上無法被限制成只返回T類型。下面數組的使用時一個很好的例子:

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<? extands Object>, 但是稍微簡單一些。

你也可以用in映射一個類型:

fun fill(dest: Array<in String>, val: 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是一個不可變類型參數,並且有一個上界類型TUpperFoo<*>在讀取值得時候等價於Foo<out TUpper>,在寫入值得時候等價於Foo<in Nothing>

如果一個泛型參數有很多參數類型,並且每一個都可以被獨立地映射。例如,如果類型被定義成interface Function<in T, out U> 我們可以想象下列星號映射

  • Funcion<*, String> 意思是Function<in Nothing, String>
  • Function<Int, *> 意思是Function<Int, out Any?>
  • Funcion<*, *> 意思是Function<in Nothing, out Any?>

注意,星號映射和Java中的元類型非常像, 但是是類型安全的

泛型函數

不止類可以有類型參數,函數也可以有。類型參數放置在函數名的前面:

fun <T> singletonList(item: T): List<T> {

}

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

}

調用一個泛型函數,在調用的時候需要在函數名後面,明確類型參數。

泛型約束

所有可能被用來替換指定類型參數的類型集合都約定成泛型約束

類型上界

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

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

}

在冒號後面指定的類型是類型上界,意味着只有Comparable<T>的子類型可以被替換成T。例如:

// 正確。Int 是Comparable<Int> 的子類型
sort(listOf(1, 2, 3))
// 錯誤。HashMap<Int, String> 不是Comparable<HashMap<Int, String>>的子類型
sort(listOf(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() }
}
---

PS,我會堅持把這個系列寫完,有問題可以留言交流,也關注專欄Kotlin for Android Kotlin安卓開發
這篇泛型好長,裏面有很多內容發現自己Java部分就理解的不全,甚至不正確。 通過本文,還發現一個關於泛型超級詳盡的文檔。 收益匪淺。
本文中有很多用詞可能不是官方的用語, 畢竟kotlin官方中文版還沒有出,我認爲可能會不準確的地方都儘量同時寫上了原版英文,有問題可以交流溝通。

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