Kotlin Generics
Consumer in, Producer out!
在使用Java语言过程中,经常会用到泛型这一技术,尤其是针对数据结构的开发。泛型技术为处理多类型参数的场景提供了极大便利。这里介绍下Kotlin语言下的泛型技术。
Declaration-site Variance
out
当我们在Kotlin中使用 out 关键字来标示一个类型参数时:
1 | interface Source<out T> { |
这其实就是在告诉Kotlin编译器,所有Source的实例只会“产生”(返回)T类型的内容而不需要“消费”(传入)T类型的参数。
out 关键字做的事情跟Java泛型中的上界通配符 <? extends T>非常相近,都是通过限制写入来确保类型安全。
即便方法返回的是T类型的子类实例,方法的调用者拿到时已经被转换成了T类型。(编译器允许这么做,因为有着明确的类型转换)
in
in 关键字做了相反的事情: 虽然允许写入,但是不允许从中获取元素,从而确保类型安全: ( Kotlin版 <? super T> )
1 | interface Comparable<in T> { |
这告诉编译器,所有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 | fun copy(from: Array<out Any>, to: Array<Any>) { |
函数经过这样的声明之后,可以很方便对 T继承链上的类型使用 (Int继承自Any),如:
1 | val ints: Array<Int> = arrayOf(1, 2, 3) |
这样的用法如果是在不使用 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> |
再将Nothing和Any根据其意义替换掉:就是Foo<?, ?>,因此,Star-projections也被看作是Kotlin版的类型通配符 ?。
来个例子看看,C.kt:
1 | class C { |
反编译查看经过Kotlin编译器编译之后的二进制文件,可以看到Star-projections 全部被转换成了能够匹配全部类型的通配符 ?
1 | Compiled from "C.kt" |