Kotlin Generics

Consumer in, Producer out!

在使用Java语言过程中,经常会用到泛型这一技术,尤其是针对数据结构的开发。泛型技术为处理多类型参数的场景提供了极大便利。这里介绍下Kotlin语言下的泛型技术。

Declaration-site Variance

out

当我们在Kotlin中使用 out 关键字来标示一个类型参数时:

1
2
3
interface Source<out T> {
fun nextT(): T
}

这其实就是在告诉Kotlin编译器,所有Source的实例只会“产生”(返回)T类型的内容而不需要“消费”(传入)T类型的参数。
out 关键字做的事情跟Java泛型中的上界通配符 <? extends T>非常相近,都是通过限制写入来确保类型安全
即便方法返回的是T类型的子类实例,方法的调用者拿到时已经被转换成了T类型。(编译器允许这么做,因为有着明确的类型转换)

in

in 关键字做了相反的事情: 虽然允许写入,但是不允许从中获取元素,从而确保类型安全: ( Kotlin版 <? super T>

1
2
3
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}

这告诉编译器,所有Comparable的实例只会接收T类型的入参,但不会返回任何T类型的元素。
所有T类型的子类入参在编译时都会上转型成类型。

为什么要使用Declaration-site Variance?
本质上泛型为开发者提供了一种使用同一种模式(类)来操作一系列类型集合的可能。但是因为在编译期无法确定类型,在运行期强行使用可能会遭受类型转换出错带来的后果(比方转换类型之后找不到方法的引用)。
因此作为享用泛型的便利和避免类型转换出错的一种tradeoff(Kotlin in & out, Java通配符 ? ),规定了开发者在coding期编写源码时必须遵循的规范。只有遵循了这一套规范才能完全避免在运行期使用泛型时,类型转换错误而导致程序崩溃的风险

在声明类型时使用泛型可以帮助我们声明一个泛型类型,可以使用这一个泛型类型去声明实例,这也是为什么被称为 Declaration-site。但如果我们想要直接使用其他人设计好的没有使用 Declaration-site Variance的泛型类呢,比如说Array?
这个时候便需要Kotlin 泛型的另外一种用法:Use-site Variance

Use-site Variance

同样的,在 Use-site Variance 场景中,依旧使用 in 和 out 关键字来声明类型参数,只不过这一次声明的是 方法入参类型的类型参数

1
2
3
fun copy(from: Array<out Any>, to: Array<Any>) {
// copy from 'from' to 'to'
}

函数经过这样的声明之后,可以很方便对 T继承链上的类型使用 (Int继承自Any),如:

1
2
3
val ints: Array<Int> = arrayOf(1, 2, 3)
val a = Array<Any>(3) { "" }
copy(ints, a) // Int is a subclass of Any

这样的用法如果是在不使用 out 来声明入参类型的类型参数时是会报出ClassCastException错误的,因为那时候只能接收copy(from : Array< Any >, to: Array< Any >)的方法调用。

in 也是一样,只不过适用的情况是往里写入元素。

Star-projections

Star-projections 只能作用在方法入参上

当使用 * 标识符声明一个类型时,

1
如Foo<*>

其实是在告诉编译器: 当从Foo中获取返回的元素时应用

1
Foo<out T>

当往Foo中添加元素时,应用

1
Foo<in Nothing>

转换成Java通配符就是

1
Foo<? super Nothing, ? extends Any>

再将NothingAny根据其意义替换掉:就是Foo<?, ?>,因此,Star-projections也被看作是Kotlin版的类型通配符 ?。
来个例子看看,C.kt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class C {
fun c() {
val map: A<String, Int> = A()
test(map)
}
}

//用 Star-projections 来声明函数
fun test(from: A<*, *>) {
print(from)
}

//Declaration-site 声明泛型类型
class A<in T, out U> {
private var value: T? = null

fun set(t: T) {
value = t
}
}

反编译查看经过Kotlin编译器编译之后的二进制文件,可以看到Star-projections 全部被转换成了能够匹配全部类型的通配符

1
2
3
4
5
6
7
8
9
10
11
12
Compiled from "C.kt"
public final class com.maxtropy.viewtest.CKt {
public static final void test(com.maxtropy.viewtest.A<?, ?>);
Code:
0: aload_0
1: ldc #9 // String from
3: invokestatic #15 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
6: getstatic #21 // Field java/lang/System.out:Ljava/io/PrintStream;
9: aload_0
10: invokevirtual #27 // Method java/io/PrintStream.print:(Ljava/lang/Object;)V
13: return
}