关于Kotlin的型变

Posted by roger on September 3, 2019

关于Kotlin的型变

1.Java对泛型的限制

Java中所有的类都是不变型的

在Java中,List<String>并不是List<Object>的子类型。

如果List<String>并是List<Object>的子类型, 那么就是意味着将一个List<Object>类型的对象指向List<String>不会报错,那么我们就可以对这个List<String>对象添加一些Object对象,比如:

// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!!即将来临的问题的原因就在这里。Java 禁止这样!
objs.add(1); // 这里我们把一个整数放入一个字符串列表
String s = strs.get(0); // !!! ClassCastException:无法将整数转换为字符串

这就是在Java中禁止List<String>是List<Object>子类型的原因

然而我们将无法做到一下简单的事情(这是完全安全):

// Java
void copyAll(Collection<Object> to, Collection<String> from) {
  to.addAll(from);
  // !!!对于这种简单声明的 addAll 将不能编译:
  // Collection<String> 不是 Collection<Object> 的子类型
}

如果不引入通配符类型参数?,那么必须手动的为每一个类型写一个addAll方法

比如:addAll(Collection<A>),addAll(Collection<B>),addAll(Collection<C>),等等

// Java
interface Collection<E> …… {
  void addAll(Collection<String> items);
}

然而Collection的addAll()的实际签名是以下这样:

// Java
interface Collection<E> …… {
  void addAll(Collection<? extends E> items);
}

通配符类型参数? extends E 表示此方法接受 E 或者 E 的 一些子类型对象的集合,而不只是 E自身。 这意味着我们可以安全地从其中(该集合中的元素是 E 的子类的实例)读取 E,但不能写入

将一个集合作为参数传递给一个有着? extends E 参数的方法,就说明在该方法中,该集合只会被读取,不会被写入

相反的,将一个集合作为参数传递给一个有着? super E 参数的方法,就说明在该方法中,该集合只会被写入,不会被读取

2.Kotlin相对Java的变化

与Java不同,Kotlin并没有直接使用通配符,而是使用了声明处型变

Joshua Bloch 称那些你只能从中读取的对象为生产者,并称那些你只能写入的对象为消费者。他建议:“为了灵活性最大化,在表示生产者或消费者的输入参数上使用通配符类型”,并提出了以下助记符:

PECS 代表生产者-Extens,消费者-Super(Producer-Extends, Consumer-Super)。

Out修饰符:生产者

当一个类Person的类型参数T被声明为out时,它就只能出现在Person的成员的输出位置,通常也就是返回位置,可以说Person在参数T上是协变的,Person是T的生产者,而不是T的消费者

这和Java中? extends E 是一致的,凡是用Out修饰符修饰的T只能被读取,无法被消费

In修饰符

当一个类的类型参数被声明为In时,类型参数逆变, 它就只能写入,无法被读取,充当一个消费者的角色

比如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!
}

3.类型投影

将类型参数声明为out非常方便,而且能够避免使用出子类型化的麻烦,但有些类实际上不能限制只返回T,有些时候在一个类中T既有充当生产者的方法,又有充当消费者的方法。

一个很好的例子是Array:

class Array<T>(val size: Int) {
    fun get(index: Int): T { …… }
    fun set(index: Int, value: T) { …… }
}

该类在T上既不能是协变的也不能是逆变的。

那么下面的函数:

fun copy(from: Array<Any>, to: Array<Any>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}

该函数将一个数组复制到另一个数组。

val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" } 
copy(ints, any)
//   ^ 其类型为 Array<Int> 但此处期望 Array<Any>

由于Array<T>在T上是不型变的,因此Array<Int>Array<Any>都不是另一个的子类型。因为copy()方法可能会做一些不正确的事情,比如会向from写入一些不被允许的类。因此,只要确保copy()from参数只会被读取,而to参数只会被写入

我们可以:

fun copy(from: Array<out Any>, to: Array<Any>) { …… }

这里发生的事情称为类型投影from是一个受限制的数组,我们只能调用返回类型为类型参数T的方法。这就意味着我们只能读取,这就是我们的使用处型变的用法,并且是对应于 Java 的 Array<? extends Object>、 但使用更简单些的方式。