Kotlin Reference: Generics

  Kotlin 的类可以具有类型参数,就像 Java 一样:

[code lang=”kotlin”]class Box(t: T) {
var value = t
}[/code]

  一般来说,要创建这种类的实例,需要提供类型参数:

[code lang=”kotlin”]val box: Box = Box(1)[/code]

  但如果类型参数可以被推断出来,例如通过构造器参数或其他途径,则可以省略类型参数:

[code lang=”kotlin”]val box = Box(1) // 1 has type Int, so the compiler figures out that we are talking about Box[/code]

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&ltString&gt strs = new ArrayList&ltString&gt();
List&ltObject&gt 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&ltE&gt … {
void addAll(Collection&ltE&gt items);
}[/code]

然而使用上面的声明,会导致无法进行如下(完全安全的)简单操作:

[code lang=”java”]// Java
void copyAll(Collection&ltObject&gt to, Collection&ltString&gt from) {
to.addAll(from); // !!! Would not compile with the naive declaration of addAll:
// Collection is not a subtype of Collection&ltObject&gt
}[/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&ltE&gt … {
void addAll(Collection&lt? extends E&gt 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&ltT&gt {
T nextT();
}[/code]

这样,把 Source<String> 的实例的引用保存到 Source<Object> 类型的变量是完全安全的,因为 Source<T> 不具有消费者方法。但 Java 并不知道这一点,所以会禁止如下的用法:

[code lang=”java”]/// Java
void demo(Source&ltString&gt strs) {
Source&ltObject&gt 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&ltout T&gt {
abstract fun nextT(): T
}

fun demo(strs: Source&ltString&gt) {
val objects: Source&ltAny&gt = 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 还提供了与之互补的标注 inin 使得类型参数成为逆变的(Contravariant):仅能被消费,不能被生产。Comparable 是一个典型的逆变类:

[code lang=”kotlin”]abstract class Comparable&ltin T&gt {
abstract fun compareTo(other: T): Int
}

fun demo(x: Comparable&ltNumber&gt) {
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&ltDouble&gt
val y: Comparable&ltDouble&gt = x // OK!
}[/code]

  我们相信 inout 非常直白(它们已经成功地在 C# 中使用了很久),并不需要特别记忆如上面 PECS 的口诀,甚至可以换个更好的表述:

存在性转换:消费者 in,生产者 out。(The Existential Transformation: Consumer in, Producer out! :-))

Type projections

Use-site variance: Type projections

  把类型参数 T 声明名为 out 可以很轻松地避免使用处的子类化问题,但有的类并不能仅限于返回 TArray 就是一个很好的例子:

[code lang=”kotlin”]class Array&ltT&gt(val size: Int) {
fun get(index: Int): T { /* … */ }
fun set(index: Int, value: T) { /* … */ }
}[/code]

  由于 Array 既是生产者,又是消费者,所以 T 既不能是协变的,也不能是逆变的。这就带来了不灵活的地方,如对于下面的函数:

[code lang=”kotlin”]fun copy(from: Array&ltAny&gt, to: Array&ltAny&gt) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}[/code]

  这个函数用于把一个数组的元素复制到另个数组,在实际使用的时候:

[code lang=”kotlin”]val ints: Array&ltInt&gt = arrayOf(1, 2, 3)
val any = Array&ltAny&gt(3) { “” }
copy(ints, any) // Error: expects (Array&ltAny&gt, Array&ltAny&gt)[/code]

  这里就遇到了和开头类似的问题,Array<T> 对于 T不可变的,因此 Array<Int> 不是 Array<Any> 的子类。这还是因为 copy() 可能干坏事,比如说,它可能会尝试往 from 里写入一个 String,而如果我们传入的 fromInt 型的数组,则会在运行时抛出 ClassCastException

【注】在 copy() 中没有限定 from 只能作为生产者。如果上面的 copy(ints, any) 合法,且在 copy() 中把 from 作为消费者,就可能出现把任意对象写入 Array<Int> 的情况,导致运行时出现 ClassCastException,丧失了运行时的类型安全。

  我们需要确保 copy() 不会干坏事,即禁止在 copy() 中对 from 进行写入,可以使用如下的方式:

[code lang=”kotlin”]fun copy(from: Array&ltout Any&gt, to: Array&ltAny&gt) {
// …
}[/code]

这里出现的就是类型投射(Type Projection),我们表明 from 不仅是一个数组,而且是一个受限(投射的)的数组:我们只能调用其返回类型参数 T 的方法,这里我们只能调用 get()。这是 Kotlin 中的使用处变型(Use-Site Variance),对应 Java 的 Array<? extends Object>,但稍微简单一些。

  也可以使用 in 来投射类型:

[code lang=”kotlin”]fun fill(dest: Array&ltin String&gt, 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 &ltT&gt singletonList(item: T): List&ltT&gt {
// …
}

fun T.basicToString() : String { // extension function
// …
}[/code]

  调用泛型函数时,要在调用处的函数名之后指定类型参数:

[code lang=”kotlin”]val l = singletonList&ltInt&gt(1)[/code]

Generic constraints

  对于一个给定类型参数,可以使用泛型约束来对可以替代它的类型的集合进行限制。

Upper bounds

  最常用的约束是指定上边界(Upper Bound),对应 Java 的 extends

[code lang=”kotlin”]fun &ltT : Comparable&ltT&gt&gt sort(list: List&ltT&gt) {
// …
}[/code]

  冒号后面的类型是上边界,表示只有 Comparable<T> 的子类可以替代 T,如:

[code lang=”kotlin”]sort(listOf(1, 2, 3)) // OK. Int is a subtype of Comparable
sort(listOf(HashMap is not a subtype of Comparable>[/code]

  未指定上边界时,默认的上边界是 Any?,尖括号内只能声明一个上边界,如果同一个类型需要多个上边界,需要使用单独的 where 子句:

[code lang=”kotlin”]fun &ltT&gt cloneWhenGreater(list: List&ltT&gt, threshold: T): List&ltT&gt
where T : Comparable,
T : Cloneable {
return list.filter { it &gt threshold }.map { it.clone() }
}[/code]