欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Kotlin 泛型中的 in 和 out

程序员文章站 2024-03-14 19:37:40
...

协变

在 Java 的泛型系统中. 泛型默认是不支持协变(covariant). 也就是说在 Java 中. 即使 A 是 B 的子类, List<A> 也不是 List<B> 的子类.
协变在某些情况下会造成一些问题, 比如在 Java 中, 数组是支持协变的. 下面的代码可以正常通过编译, 只有在运行时才会报错.

// snippet 1
// 数组是协变的, String[] 是 Object 的子类
Object [] objects = new String[10]
objects[1] = 1

虽然协变会引起一些不能在编译时就发现的明显错误, 但是在 Java 一直到 Java 5 才引入了泛型. 如果数组不支持协变, 很多方法就没有办法实现了. 比如 Arrays.sort(Ojbect[] array)这个方法. 不知持协变的情况下只能对每一个不同对象都实现一个方法.
在有了泛型的支持后, 数组协变这个特性就变成了一个彻彻底底的设计缺陷. 但是为了保持向下的兼容, Java 还是一直保留这个设计.
Kotlin 并没有类似 Java 的兼容包袱, 所以 Kotlin 中的 Array 是不支持协变的.下面的代码不能通过编译.

// snippet 2
val objects: Array<Object> = Array<String>(1) { "" } // Error

协变的作用

在一些情况下, 协变依然很有用. 比如我们实现一个List 的 copy 的函数如下:

// snippet 3
// 从 src 复制元素到 
static <T> void copy(List<T> dest, List<T> src)

当需要将一个类型为 List<Integer> 的 src 复制到 List<Number> dest 我们可能会写出如下的代码:

// snippet 4
List<Number> dest = ... // 初始化 dest
List<Integer> src = ...// 初始化 src
copy(dest, src)

但是 List<Integer> 并不是 List<Number> 的子类. 所以上面的代码并不能通过编译.
Java 提供了有限通配符类型 (bounded wildcard type) 来处理这种情况.上面的代码可以被改写成下面的形式:

// snippet 5
static <T> void copy(List<T> dest, List<? extend T> src)

这样改写后, src 的类型就变成 T 的某一个子类的列表. 也就是说 src 是支持协变的.

逆变

逆变(contravariant) 跟协变相反. 如果 List 支持逆变, 且 A 是 B 的子类, 那么 List<B> 是 List<A> 是子类.
逆变在函数中比较常见, 比如在 Kotlin 中 (Any) -> () 是 (String) -> () 的子类. Java 也通过 wildcard type 的方式支持了逆变. 声明方式如下:

List<? super Number> a;

变量 a 可以接受 List<Integer>, List<Double> 等类型.

PECS 法则

在函数的接口里使用通配符类型可以提高接口的灵活度. 但是参数究竟要使用 super 还是 extend 呢. Effective Java 中给出了一个 PECS 法则.

PSCS stands for producers-extends, consumer-super

也就是说, 如果一个参数是起的是生产者的作用, 那应该用 extend, 如果起的是消费者的作用, 就应该使用 super. 一个最好的例子就是 JDK 里 Collections 中的 copy 方法. 函数定义如下:

public static <T> void copy(List<? super T> dest, List<? extend T> src)

该函数里 src 提供需要复制的内容, 是一个生产者. 所以使用了 extend. dest 接受复制过来的元素, 是一个消费者. 所以使用的是 super. 判读一个参数是生产者还是消费者需要一定的编码的经验. 下面是我的两条经验:

  • 如果是参数是一个容器类型, 比如 List. 那么如果你在函数中是只是读取容器内容, 那么该参数就是一个生产者. 反正, 如果只是往容器里添加和删除, 而没有读取那么这个参数就是消费者.
  • 如果参数是一个 function Object, 可以将其改成 Lambda 函数. 正常在 Lambda 里当作参数的类型参数要求是逆变的, 返回值是协变的 比如 Comparator<T> 改写成 Lambda 形式应该是 (T, T) ->Int, 这里的 T 是只出现在参数中, 应该是逆变的. 当函数需要 Comparator 作为参数时通常使用会使用 super. 比如 Collections 中 sort 函数的定义为:
public static <T> void sort(List<T> list, Comparator<? super T> c)

Kotlin 中的 in 和 out

Kotlin 中可以声明泛型类型是协变还是逆变的. out 修饰类型参数是协变的, in 修饰的类型参数支持逆变.
比如 Collections 的 copy 方法的可以定义为:

public <T: Any> fun copy(dest: List<in T>, src: List<out T>)

除了在参数中使用外. 还可以直接在类型中使用. 比如 Kotlin 中的 List 是不可变的. 所以 List 定义里直接声明为协变的.

public interface List<out E>: Collection<E>

in 和 out 两个关键字应该是取自: Consumer in, Producer out!

参考资料:

Kotlin Generics: https://kotlinlang.org/docs/reference/generics.html