与 Java 类似,Kotlin 中的类也有类型参数:
class Box<T>(t: T) {
var value = t
}
一般来说,要创建这样类的实例,我们需要提供类型参数:
val box: Box<Int> = Box<Int>(1)
但是如果类型参数可以推断出来,例如从构造函数的参数或者从其他途径,允许省略类型参数:
val box = Box(1) // 1 具有类型 Int,所以编译器知道我们说的是 Box<Int>。
什么是型变
java 中的泛型是不型变的,List<String>
并不是 List<Object>
的子类型。为了保证运行安全,才这样设计,可以看看下面的代码
List<String> strs = new ArrayList<String>();
List<Object> objs = strs;
objs.add(1);
String s = strs.get(0);//在这里我们将面临 ClassCastException:无法将整数转换为字符串
但这样我们又会遇到这样的问题。例如,考虑 Collection
接口中的 addAll()
方法。
interface Collection<E> …… {
void addAll(Collection<E> items);
}
但之后我们无法做到以下这个事情
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from);
// !!!对于这种简单声明的 addAll 将不能编译:
// Collection<String> 不是 Collection<Object> 的子类型
}
这就是为什么 addAll()
的实际签名是以下这样:
interface Collection<E> …… {
void addAll(Collection<? extends E> items);
}
熟悉 Java 肯定知道, Java 中在处理型变时使用的是通配符类型参数 extend
、super
,通配符的出现是为了保证类型安全。
Kotlin相关
声明处型变
Kotlin 中为什么不直接使用 Java 的通配符方式,而使用声明处型变的方式,我们可以看看下面这个例子:
定义一个泛型接口:
interface Source<T> {
T nextT();
}
那么,在 Source<Object>
类型的变量中存储 Source<String>
实例的引用是极为安全的——没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作:
// Java
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!!在 Java 中不允许
// ……
}
为了修正这一点,我们必须声明对象的类型为 Source<? extends Object>
,这是毫无意义的,因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。但编译器并不知道。
out (协变)
所以在 Kotlin 中,有一种方法向编译器解释这种情况。这称为声明处型变:我们可以标注 Source
的类型参数 T
为 out 来确保它仅从 Source<T>
成员中返回,并从不被消费。
interface Source<out T> {
fun nextT(): T //T 只出现在返回位置
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // 这个没问题,因为 T 是一个 out-参数
}
当一个类型参数 T
被声明为 out 时,它就只能出现在输出位置就是只能在函数的返回位置,作为回报 C<Base>
可以安全地作为 C<Derived>
的超类进行转换。
由于被声明为 out 的 T 只存在于返回位置,所以在使用时如果我们定义
Source<String>
则返回的是String
,定义的是Source<Any>
返回的则是Any
。这时我们把Source<String>
对象赋值给Source<Any>
,则Source<Any>
中真正意义上存放的是String
,但函数返回的是Any
,现在想想实际存的String
是不是拥有所有Any
的属性和方法,获取到返回的Any
在使用过程中是没有任何问题的。所以当一个类型参数
T
被声明为 out 时,只能出现在输出位置。他就有C<Base>
可以安全地作为C<Derived>
的超类的能力。
in (逆变)
in 使得一个参数 T :只可以被消费而不可以被生产。逆变类型的一个很好的例子是 Comparable
:
interface Comparable<in T> {
operator 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 使得一个参数 T 只能存在于消费位置,即方法中参数的类型。所以我们定义
Comparable<Number>
在使用这个类型参数时我们只能获取到Number
,Comparable<Double>
在使用这个类型参数时我们只能获取到Double
,我们都知道Number
是Double
父类,Double
有Number
没有的属性和方法。在使用中我们是不是可以可以说Double
拥有Number
的所有能力。所以在传入Number
参数的地方我们传个Double
肯定是没有问题的。所以我们可以用
Comparable<Double>
来接受一个Comparable<Number>
,因为在使用的时候我们当然是方法和参数更多的子类受用范围大。
类型投影
类型投影:使用处型变,在使用的地方使用 out/in 声明
看个例子:
class Array<T>(val size: Int){
fun get(index: Int): T{ /*...*/}
fun set(index: Int , value: T){/*...*/}
}
可以看出 Array 既不是协变的,也不是逆变的。如果他定义了如下的函数:
fun copy(from: Array<Any> , to: Array<Any>){
assert(from.size == to.size)
for(i in from.indices)
to[i] = from[i]
}
在编辑如下代码是时会提示错误:
fun main() {
val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" }
copy(ints, any)//编辑器报错,Type mismatch Required:Array<Any> Found: Array<Int>
}
为什么编辑器报错,我们假设上面的 main
方法是可以执行的,这个时候如果由于编写错误,我们在 copy
函数中调用了 set
方法,这将导致结果错误,明明是 copy
但却得到一个全是 0
的数组。为了阻止 copy
做坏事,我们直接声明使用处型变,约束 from
只能调用返回函数。如下定义了 out
再去调用 from[i] = 0
编辑器提示错误。
使用时的例子
fun copy(from: Array<out Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
println(to)
}
fun fill(dest: Array<in Number>, value: Number) {
}
fun main() {
val ints: Array<Int> = arrayOf(1, 2, 3)
val anys: Array<Any> = arrayOf("", "", "")
copy(ints, anys)
fill(anys, 1.0)
}
使用 copy
函数 Array<out Any>
是协变的相当于用一个 Array<Any>
来接收 Array<Int>
使用 fill
函数 Array<in Number>
是逆变的相当于用一个 Array<Number>
来接受 Array<Any>
星投影
有时你想说,你对类型参数一无所知,但仍然希望以安全的方式使用它。 这里的安全方式是定义泛型类型的这种投影,该泛型类型的每个具体实例化将是该投影的子类型。
Kotlin 为此提供了所谓的星投影语法:
- 对于
Foo <out T : TUpper>
,其中T
是一个具有上界TUpper
的协变类型参数,Foo <*>
等价于Foo <out TUpper>
。 这意味着当T
未知时,你可以安全地从Foo <*>
读取TUpper
的值。 - 对于
Foo <in T>
,其中T
是一个逆变类型参数,Foo <*>
等价于Foo <in Nothing>
。 这意味着当T
未知时,没有什么可以以安全的方式写入Foo <*>
。 - 对于
Foo <T : TUpper>
,其中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 的原始类型,但是安全。
@UnsafeVariance
当我们的使用违反了 in
和 out
的使用,但是我们自己是知道这样做事没有问题的,我们就可以使用 @UnsafeVariance
告诉编辑器,这个你不用管了。例子如下:
public interface List<out E> : Collection<E> {
...
public operator fun get(index: Int): E
public fun indexOf(element: @UnsafeVariance E): Int
...
}
这是官方的 List
类 ,泛型是 <out E>
但是在 indexOf
中作为入参使用了,但我们知道这完全不会对我们的 list
造成任何的影响,我们就可以用 @UnsafeVariance
让编辑器不要管了。
参考
[Kotlin 官网_泛型相关