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 variance
和type 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!
}
我們相信關鍵則in
和out
可以很好的自解釋(因爲他們已經在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
是一個不可變類型參數,並且有一個上界類型TUpper
,Foo<*>
在讀取值得時候等價於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官方中文版還沒有出,我認爲可能會不準確的地方都儘量同時寫上了原版英文,有問題可以交流溝通。