转载

关于Kotlin的型变

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> 、 但使用更简单些的方式。

如果有什么写的不好的地方欢迎批评和指正。

"( ̄_, ̄ )"

原文  https://juejin.im/post/5d6dd793f265da03e83b8858
正文到此结束
Loading...