Kotlin的泛型:协变与逆变

Kotlin 的协变与逆变统称为 Kotlin 的变型。变型是指泛型的基础类型与它的参数类型是如何关联的。

对于普通类型来说,我们可以使用子类代替父类,因为子类包含了父类的全部内容。但是对于泛型来说,如果泛型的基础类型相同,其中一个参数类型是另外一个参数类型的子类,泛型类也不存在这种继承关系,无法直接替换使用。要解除这些限制,就需要用到协变与逆变。

变型

变型的存在是为了解决函数的泛型参数传递问题。

下面的代码中,printContents 接收 List 的参数,然后把 list 中的每个元素拼接成 String 打印出来。

fun printContents(list: List<Any>) {
    println(list.joinToString())
}

fun main() {
    val list = listOf("abc", "def")
    // abc, def
    printContents(list)
}

printContents 虽然接收的 List,但是传递给它 List 也能正常工作。也就是说 String 是 Any 的子类,List 也可以类似的作为 List 的子类传参。

下面的代码使用 MutableList 替换了 List,代码编译不过。

fun printContents(list: MutableList<Any>) {
    println(list.joinToString())
}

fun main() {
    val list = mutableListOf("abc", "def")
    printContents(list)
}

编译器提示如下:

Type mismatch.
Required:
MutableList<Any>
Found:
MutableList<String>

参数类型不匹配,期望是 MutableList 类型,实际传入的是 MutableList 类型。

可以看出 MutableList 的表现和 List 不一样,编译器没有对 List 的参数报错。

List

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

MutableList

public interface MutableList<E> : List<E>, MutableCollection<E> {

对比 List 和 MutableList 的定义,可以看出 List 在类型参数 E 之前多了 out 关键字。因此编译器在处理 List 传参时才没有报错。

out 关键字也就是协变,指泛型类和它的参数类型一起变化。比如 String 可以代替 Any 传参,那么 List 可以代替 List 传参。

再看另一个例子:
下面的代码在 it.length 这一行无法编译。

addAnswer 函数向 Any 列表增加一个整数。如果 addAnswer 传入的实参是 String 列表,那么在遍历列表时,会获取每个元素的长度,但是最后一个元素是整型,它没有长度,会导致运行时错误。

fun addAnswer(list: MutableList<Any>) {
    list.add(42)
}

fun main() {
    val strings: MutableList<Any> = mutableListOf("abc", "defg")
    addAnswer(strings)
    println(strings.maxByOrNull {
        it.length
    })
}

子类型

类与类型

我们通常使用 class 表示类,使用 type 表示类型。如果是普通的非泛型类,类和类型是相同的,比如 String 类表示的是 String 类型。

但是对于 String 类来说,它还可能存在可空类型 String?,也就是 String 类对应两种类型:非空类型 String 和可空类型 String?。

对泛型来说,情况更加复杂。List 是一个类,但是它不是一个类型,它对应多个类型:List, List<String?>, List<List> 等等。

子类型与超类型

为了描述类型之间的关系,需要引入子类型 subType 的概念。

如果类型 B 可以用在任何需要类型 A 的地方,那么把类型 B 叫做类型 A 的子类型。

比如 Int 是 Number 的子类型,Int 是 Int 的子类型。但是 Int 不是 String 的子类型,因为 Int 不能替代 String 使用。

反过来,我们称类型 A 是类型 B 的超类型。超类型和子类型完全相反。如果类型 B 是类型 A 的子类型,那么类型 A 是类型 B 的超类型。

例如 Int 是 Number 的子类,而且是子类型,因此可以将 Int 赋值给 Number。

但是 Int 不是 String 的子类,也不是子类型,所以 i 不能作为 f 函数的参数,编译器会报错。

fun test(i : Int) {
    // compile ok
    val n : Number = i

    fun f(s: String) {
        println(s)
    }
    // compile error
//    f(i)
}

对于可空类型和非空类型,它们都是相同的类,但是非空类型是可空类型的子类型。因此可以把 s 赋值给 t。

相反,String? 是 String 的超类型,因此 t 赋值给 r 会编译报错。

fun test2() {
    val s: String = "abc"
    // compile ok
    val t: String? = s

    // compile error
//    val r: String = t
}

对泛型来说,比如 MutableList,MutableList 既不是 MutableList 的子类型,也不是它的超类型。这种场景被称为在该类型参数上是“不变型”的。

协变:保留子类型

协变是指:
如果 A 是 B 的子类型,那么 Producer 是 Producer 的子类型。

Kotlin 使用 out 关键字表示泛型类在类型参数是协变的。

以下代码表示 Producer 在 T 上是协变的。

interface Producer<out T> {
    fun produce(): T
}

以下代码编译不过:

open class Animal {
    fun feed() {
    }
}

class Herd<T : Animal> {
    private val _animals: List<T> = listOf()
    val size: Int
        get() = _animals.size

    operator fun get(i: Int): T = _animals[i]
}

fun feedAll(animals: Herd<Animal>) {
    for (i in 0 until animals.size) {
        animals[i].feed()
    }
}

class Cat: Animal() {
    fun cleanLitter() {
    }    
}

fun takeCareOfCats(cats: Herd<Cat>) {
    for (i in 0 until cats.size) {
        cats[i].cleanLitter()
        // feedAll 函数的参数是 Herd<Animal>,但是传入了 Herd<Cat> 所以编译器报错
        // 正常来说,喂养所有的猫是合理的,因为猫是动物的子类型。
        // 但是编译器不认为 Herd<Cat> 是 Herd<Animal> 的子类型
        feedAll(cats)
    }
}

编译器不认为 Herd 是 Herd 的子类型。

因为 Herd 只有 get,没有允许添加或者修改操作。所以可以把它变成协变类。

class Herd<out T : Animal> {
    private val _animals: List<T> = listOf()
    val size: Int
        get() = _animals.size

    operator fun get(i: Int): T = _animals[i]
}

加上 out 关键字之后不报错了。因为 out 告诉编译器,Herd 是协变的,所以 Herd 是 Herd 的子类型。可以作为参数调用。

需要注意,不是所有类都可以变成协变的。

只有当这个类只能生产类型 T 的值而不能消费它们时,才能变成协变的。

T 的值只能作为函数返回值时,才能变成协变的。这也是为什么协变关键词叫做 out,表明它只能作为生产者对外输出。

以下代码中,t : T 的位置叫做 in 位置,表示它是函数参数,是消费者。: T 的位置叫做 out 位置,表示它是函数返回值,是生产者。

interface Transformer<T> {
    fun transform(t: T): T
}

一句话总结:生产者 out,消费者 in。Producer out Consumer in。类似 Java 的 PECS,Producer Extend Consumer Super。

out 关键字表明了所有使用 T 的地方只能是把 T 放在函数返回值 out 位置,不能放在函数参数 in 位置。因此 out 有了第二层含义,约束了 T 只在 out 位置。

class Herd<out T : Animal> {
    private val _animals: List<T> = listOf()
    val size: Int
        get() = _animals.size

    operator fun get(i: Int): T = _animals[i]
}

out 有两层含义:

  • 子类型化会被保留
  • T 只能用在 out 位置

可以这么理解:子类型化保留是 out 关键字带来的权利。T 只能用在 out 位置是 out 关键字带来的义务。要满足子类型化必须先保证 T 只能用在 out 位置。

再来看 List 和 MutableList:

List 在 E 上是协变的,E 用在函数返回值 out 位置。

public interface List<out E> : Collection<E> {
    public operator fun get(index: Int): E
}

需要注意的是 List 的有些方法也把 E 作为函数参数使用,但是它用 @UnsafeVariance 注解排除了编译器影响。

MutableList 不是协变的,它可读可写。E 既在 in 位置 又在 out 位置。

public interface MutableList<E> : List<E>, MutableCollection<E> {
    override fun add(element: E): Boolean
    public operator fun set(index: Int, element: E): E
}

还有一点需要注意:位置规则 in 或者 out 只对类外部可见的 api,比如 public、protected、internal 起作用,对内部不起作用,比如 private。

以下代码编译通过,因为虽然 var 是可读可写的,但是它是 private 的,所以不受位置影响。如果去掉 private 则报错。

class Herd2<out T : Animal>(private var leadAnimal: T, vararg animals: T) {

}

逆变:反转子类型

逆变与协变相反。如果 A 是 B 的子类型, 那么 Consumer 是 Consumer 的子类型。

逆变的参数类型需要用在函数的参数位置。也就是 in 位置,使用 in 关键字表示。

Comparator 比较器是一个逆变的例子,它在入参 e1、e2 是逆变的。

interface Comparator<in T> {
    fun compare(e1: T, e2: T): Int
}

sortedWith 函数期望传入一个 Comparator,但是实际传入 Comparator 也是可以的。

因为 Comparator 在 T 上是逆变的。String 是 Any 的子类型,那么 Comparator 是 Comparator 的子类型。

val strings: List<String> = listOf("a", "c", "b")

fun sortString() {
    val anyComparator = Comparator<Any> { e1, e2 ->
        println("$e1 hashcode=${e1.hashCode()}")
        println("$e2 hashcode=${e2.hashCode()}")
        e1.hashCode() - e2.hashCode()
    }
    // sortedWith 需要 Comparator<String> 作为参数,我们传入了 Comparator<Any>。
    // 因为 Comparator 是逆变的,Comparator<Any> 是 Comparator<String> 的子类型,
    // 所以可以使用泛型父类代替泛型子类传递参数。
    val sortedWith = strings.sortedWith(anyComparator)

    println(sortedWith)
    println(strings)
}

另外一个逆变的例子是函数类型。它同时使用了逆变和协变。

Function1 在参数 P 是逆变的,在返回值 R 是协变的。通常会写成 § -> R 的箭头表示。

// 箭头操作符的单参数函数形式 Function1
// 通常我们会写成 (P) -> R,这种表示法隐含了 P 是逆变的,R 是协变的。
interface Function1<in P, out R> {
    operator fun invoke(p: P): R
}

enumerateCats 是高阶函数,它接收 (Cat) -> Number 类型的函数,但是实际传递 Animal::getIndex 也是可以的。

getIndex 的类型是 (Animal) -> Int ,在参数上是逆变的,在返回值上是协变的。

open class Animal {

}

class Cat : Animal() {

}

// Cat 是参数,f 在 Cat 上是逆变的。Number 是返回值,f 在 Number 是协变的。
fun enumerateCats(f: (Cat) -> Number) {

}

fun Animal.getIndex(): Int = 0

fun testEnumerateCats() {
    enumerateCats(Animal::getIndex)
}

使用点变型

之前的例子都是在类声明的之后指定变型修饰符,这种做法称为声明点变型。除此之外还有使用点变型。

Java 泛型都是使用点变型。

下面是 Stream 类的 map 方法。在使用 Function 作为参数时,使用通配符 ? super T 表示参数逆变,? extends R 表示返回值协变。

public interface Stream<T> extends BaseStream<T, Stream<T>> {    
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
}

MutableList 是不变型的泛型。

// 不变型的复制方法
fun <T> copyData(source: MutableList<T>, destination: MutableList<T>) {
    for (item in source) {
        destination.add(item)
    }
}

为了让 source 和 destination 支持不同的类型,可以使用泛型上界。

fun <T : R, R> copyData2(source: MutableList<T>, destination: MutableList<R>) {
    for (item in source) {
        destination.add(item)
    }
}

fun testCopyData2() {
    val ints = mutableListOf(1, 2, 3)
    val anyItems = mutableListOf<Any>()

    copyData2(ints, anyItems)
    println(anyItems)
}

还可以给参数 source 的类型加上 out 关键字,说明它支持协变,也就是可以传入 T 的子类型 A,只会用在返回值位置。

fun <T> copyData3(source: MutableList<out T>, destination: MutableList<T>) {
    for (item in source) {
        destination.add(item)
    }
}

除了给函数形参加上变型修饰符,还可以用在局部变量、函数返回类型等。

这种方式叫做“类型投影”。以上面为例,source 不再是一个常规的 MutableList,而是一个受限(投影)的MutableList。加上 out 后,它的泛型类型参数只能是 T 的子类型。

out 投影后,类型的含义会发生变化,它不再能调用参数类型作为函数参数的那些方法,也就是不能用在 in 位置。

add 方法将 Number 作为函数参数,是 in 位置。但是 list 是 out 投影,所以 add 方法不能使用,编译器报错。

fun testCopyData3() {
    val list : MutableList<out Number> = mutableListOf(1, 2, 3)
    // compile error, The integer literal does not conform to the expected type Nothing
//    list.add(42)
}

相反的,如果把 destination 加上 in,变为 in 投影,那么传入的参数都可以是 T 的超类型。

fun <T> copyData4(source: MutableList<T>, destination: MutableList<in T>) {
    for (item in source) {
        destination.add(item)
    }
}

fun testCopyData4() {
    val ints = mutableListOf(1, 2, 3)
    val anyItems = mutableListOf<Any>()

    copyData4(ints, anyItems)
    println(anyItems)
}

星号投影

星号投影用来表明不知道关于泛型实参的任何信息。

下面的代码中 unknownElements 是 MutableList<*> 星投影类型,它有可能是 Any?也有可能是 Char。

val list: MutableList<Any?> = mutableListOf('a', 1, "qwe")

val chars = mutableListOf('a', 'b', 'c')

val unknownElements: MutableList<*> = if (Random().nextBoolean()) list else chars

fun testStart() {
    // compile error.
    // The integer literal does not conform to the expected type Nothing
//    unknownElements.add(42)
    println(unknownElements.first())
}

fun main() {
    testStart()
}

如果想 add 42 会报错,因为它被投影为 MutableList<out Any?>,只能用在返回值位置,不能作为参数添加。虽然它的类型未知,但是读取 Any? 是安全的,而向未知类型的列表写入一个具体类型的元素是不安全的。

Kotlin 的 MyType<*> 对应 Java 的 MyType<?>。

下面的例子从 list 打印第一个元素:

fun printFirst(list: List<*>) {
    if (list.isNotEmpty()) {
        println(list.first())
    }
}

fun <T> printFirst2(list: List<T>) {
    if (list.isNotEmpty()) {
        println(list.first())
    }
}

相比第二种使用泛型参数 T 的情况,星号投影更加简洁。

星号投影用在那些只是使用生产值的方法,而且不关心那些值的类型。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值