Kotlin Reference: Generics
Kotlin 的类可以具有类型参数,就像 Java 一样:
[code lang=”kotlin”]class Box
var value = t
}[/code]
一般来说,要创建这种类的实例,需要提供类型参数:
[code lang=”kotlin”]val box: Box
但如果类型参数可以被推断出来,例如通过构造器参数或其他途径,则可以省略类型参数:
[code lang=”kotlin”]val box = Box(1) // 1 has type Int, so the compiler figures out that we are talking about Box
Contents
Variance
通配符是 Java 类型系统中最复杂部分之一(见 Java Generics FAQ),Kotlin 中没有通配符,取而代之的是声明处变型(Declaration-Site Variance)和类型投射(Type Projection)。
首先,让我们来思考为什么 Java 需要这些神秘的通配符。Effective Java 在 “第28条:利用有限制通配符来提升 API 的灵活性(Item 28: Use bounded wildcards to increase API flexibility)”中解释了需要通配符的原因。首先,Java 中的泛型类型是不可变的(Invariant),这意味着 List<String>
不是 List<Object>
的子类。如果 List
是可变的,那它就和 Java 的数组有一样的问题(【注】无法保证运行时的类型安全),如下面的代码可以通过编译,但在运行时抛出异常:
[code lang=”java”]// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!! The cause of the upcoming problem sits here. Java prohibits this!
objs.add(1); // Here we put an Integer into a list of Strings
String s = strs.get(0); // !!! ClassCastException: Cannot cast Integer to String[/code]
所以 Java 禁止这种的用法,以保证运行时的安全。但这种限制也带来一些额外的影响,比如对于 Collection
接口的 addAll()
方法,直觉上我们会认为该方法的签名如下:
[code lang=”java”]// Java
interface Collection<E> … {
void addAll(Collection<E> items);
}[/code]
然而使用上面的声明,会导致无法进行如下(完全安全的)简单操作:
[code lang=”java”]// Java
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from); // !!! Would not compile with the naive declaration of addAll:
// Collection
}[/code]
【注】String 是 Object 的子类,把 String 集合添加到 Object 集合是安全的,但由于泛型是不可变的,Collection<String>
不是 Collection<Object>
的子类,故上面的代码无法编译。
(我们在 Java 中吸取了惨痛了教训,见 Effective Java 第25条:列表优先于数组(Item 25: Prefer lists to arrays))
所以 addAll()
实际使用的签名如下:
[code lang=”java”]// Java
interface Collection<E> … {
void addAll(Collection<? extends E> items);
}[/code]
这里的通配符类型参数 ? extends E
表示该方法接受 E
的子类的集合作为参数,而不仅限于 E
本身的集合。这意味着我们可以安全地从集合中读取类型 E
的元素(集合中的所有元素都是 E
的子类的实例),但不能对该集合进行写入,因为不知道哪些对象符合“E
的未知子类”的要求。以不能写入为代价,我们得到了希望的效果: Collection<String>
表现为了 Collection<? extends Object>
的子类。用专业术语来讲,使用 extends
限定(上边界)的通配符使得类型是协变的(Covariant)。
理解上面工作原理的关键其实很简单,如果只能从集合中取出元素,把 String
的集合中的元素以 Object
的类型读取出来是没有问题的。反过来说,如果只能往集合中放入元素,把 String
的实例放进 Object
的集合也是可以的。在 Java 中,List<? super String>
可以作为 List<Object>
的超类(【注】? super String
表示 String
的任意超类, Object
只是其中一种)。
上面的后一种情况称为逆变性(Contravariance)。对于 List<? super String>
,只可以调用它以 String
作为参数的方法(例如可以调用 add(String)
、set(int, String)
),但如果调用它返回 List<T>
中的 T
的方法,得到的会是 Obejct
而不是 String
。
Joshua Bloch 把仅能从中读取的对象称为生产者(Producer),仅能向其中写入的对象称为消费者(Consumer),他建议,“为了保证最大的灵活性,在生产者或消费者的输入参数上使用泛型”,并提出了如下的助记方式:
PECS 表示生产者 Extend,消费者 Super。(PECS stands for Producer-Extends, Consumer-Super.)
注意:如果使用了生产者对象,如 List<? extends Foo>
,就不能调用能修改该对象的 add()
或 set()
,但这并不意味着该对象是不可变的(Immutable),例如依然可以调用 clear()
来清空整个列表,因为 clear()
没有参数。通配符(或者其他形式的变型)只能确保类型安全(Type Safety),不可变性(Immutability)则完全是另一码事。
Declaration-site variance
假设我们有一个泛型接口 Source<T>
,它没有任何以 T
作为参数的方法,只有一个返回 T
的方法:
[code lang=”java”]// Java
interface Source<T> {
T nextT();
}[/code]
这样,把 Source<String>
的实例的引用保存到 Source<Object>
类型的变量是完全安全的,因为 Source<T>
不具有消费者方法。但 Java 并不知道这一点,所以会禁止如下的用法:
[code lang=”java”]/// Java
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!! Not allowed in Java
// …
}[/code]
为了解决这一问题,我们必须把 objects
的类型声明为 Source<? extends Object>
,但这并没有什么实际意义,因为我们依然可以像之前一样调用 objects
上的所有方法。在这种场景下,像 Source<? extends Object>
这种更复杂的声明并没有带来什么实际价值,但编译器并不知道这一点。
在 Kotlin 中,可以通过声明处变型(Declaration-Site Variance)向编译器说明上面提到的情况:我们可以对类型参数 T
进行标注,确保它仅能用于 Source<T>
成员的返回(生产)类型,而从不被消费。为了达到这样的效果,我们只需使用 out
修饰符:
[code lang=”kotlin”]abstract class Source<out T> {
abstract fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // This is OK, since T is an out-parameter
// …
}[/code]
一般的规则是:如果类 C
的类型参数 T
被声明为 out
,则 T
仅能出现在 C
的成员的输出(Out)位置,由此 C<Base>
可以安全地作为 C<Derived>
的超类。
使用专业术语来说,类 C
对于参数 T
是协变的(Covariant),或者说 T
是一个协变的类型参数。可以把 C
想象为 T
的一个生产者,而不是 T
的消费者。
out
修饰符称为变型注解(Variance Annotation),又由于它用在类型参数的声明处,故称之为声明处变型(Declaration-Site Variance)。这与 Java 使用通配符来实现类型协变的使用处变型(Use-Site Variance)相反。
除了 out
,Kotlin 还提供了与之互补的标注 in
,in
使得类型参数成为逆变的(Contravariant):仅能被消费,不能被生产。Comparable
是一个典型的逆变类:
[code lang=”kotlin”]abstract class Comparable<in T> {
abstract fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 has type Double, which is a subtype of Number
// Thus, we can assign x to a variable of type Comparable<Double>
val y: Comparable<Double> = x // OK!
}[/code]
我们相信 in
和 out
非常直白(它们已经成功地在 C# 中使用了很久),并不需要特别记忆如上面 PECS 的口诀,甚至可以换个更好的表述:
存在性转换:消费者 in
,生产者 out
。(The Existential Transformation: Consumer in, Producer out! :-))
Type projections
Use-site variance: Type projections
把类型参数 T
声明名为 out
可以很轻松地避免使用处的子类化问题,但有的类并不能仅限于返回 T
,Array
就是一个很好的例子:
[code lang=”kotlin”]class Array<T>(val size: Int) {
fun get(index: Int): T { /* … */ }
fun set(index: Int, value: T) { /* … */ }
}[/code]
由于 Array
既是生产者,又是消费者,所以 T
既不能是协变的,也不能是逆变的。这就带来了不灵活的地方,如对于下面的函数:
[code lang=”kotlin”]fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}[/code]
这个函数用于把一个数组的元素复制到另个数组,在实际使用的时候:
[code lang=”kotlin”]val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { “” }
copy(ints, any) // Error: expects (Array<Any>, Array<Any>)[/code]
这里就遇到了和开头类似的问题,Array<T>
对于 T
是不可变的,因此 Array<Int>
不是 Array<Any>
的子类。这还是因为 copy()
可能干坏事,比如说,它可能会尝试往 from
里写入一个 String
,而如果我们传入的 from
是 Int
型的数组,则会在运行时抛出 ClassCastException
。
【注】在 copy()
中没有限定 from
只能作为生产者。如果上面的 copy(ints, any)
合法,且在 copy()
中把 from
作为消费者,就可能出现把任意对象写入 Array<Int>
的情况,导致运行时出现 ClassCastException
,丧失了运行时的类型安全。
我们需要确保 copy()
不会干坏事,即禁止在 copy()
中对 from
进行写入,可以使用如下的方式:
[code lang=”kotlin”]fun copy(from: Array<out Any>, to: Array<Any>) {
// …
}[/code]
这里出现的就是类型投射(Type Projection),我们表明 from
不仅是一个数组,而且是一个受限(投射的)的数组:我们只能调用其返回类型参数 T
的方法,这里我们只能调用 get()
。这是 Kotlin 中的使用处变型(Use-Site Variance),对应 Java 的 Array<? extends Object>
,但稍微简单一些。
也可以使用 in
来投射类型:
[code lang=”kotlin”]fun fill(dest: Array<in String>, value: String) {
// …
}[/code]
Array<in String>
对应 Java 的 Array<? super String>
,如可以向 fill()
传递 CharSequence
数组或 Object
数组。
Star-projections
有时候并不知道类型参数的信息,但依旧想要确保类型安全。安全地方法是定义一个泛型类型的投射,使得该泛型的任意具体实例都是该投射的子类。
为此,Kotlin 提供了星号投射(Star-Projection )的语法:
- 对于
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 的原始类型(Raw Type),但它是安全的。
Generic functions
函数也可以像类一样具有类型参数,类型参数放在函数名前:
[code lang=”kotlin”]fun <T> singletonList(item: T): List<T> {
// …
}
fun
// …
}[/code]
调用泛型函数时,要在调用处的函数名之后指定类型参数:
[code lang=”kotlin”]val l = singletonList<Int>(1)[/code]
Generic constraints
对于一个给定类型参数,可以使用泛型约束来对可以替代它的类型的集合进行限制。
Upper bounds
最常用的约束是指定上边界(Upper Bound),对应 Java 的 extends
:
[code lang=”kotlin”]fun <T : Comparable<T>> sort(list: List<T>) {
// …
}[/code]
冒号后面的类型是上边界,表示只有 Comparable<T>
的子类可以替代 T
,如:
[code lang=”kotlin”]sort(listOf(1, 2, 3)) // OK. Int is a subtype of Comparable
sort(listOf(HashMap
未指定上边界时,默认的上边界是 Any?
,尖括号内只能声明一个上边界,如果同一个类型需要多个上边界,需要使用单独的 where
子句:
[code lang=”kotlin”]fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T>
where T : Comparable,
T : Cloneable {
return list.filter { it > threshold }.map { it.clone() }
}[/code]