Kotlin进阶知识(十一)——变型:泛型和子类型化

221 阅读6分钟

变型:描述拥有相同基础类型和不同类型实参的(泛型)类型之间是如何关联的,例如,List和List之间如何关联。

一、为什么存在变型:给函数传递实参

子类型:任何时候如果需要的是类型A的值,都能够使用类型B的值(当作A的值),类型B就称为类型A的子类型

超类型:是子类型的反义词。如果A是B的子类型,那么B就是A的超类型。

  • 检查一个类型是否是另一个的子类型
fun test1(i: Int) {
    // 编译通过,因为Int是Number的子类型
    val n: Number = i

    fun f(s: String) { println(s) }
    // 不能编译,因为Int 不是String的子类型
    f(i)
}

一个非空类型是它的可空版本的子类型,但它们都对应着同一个类。你始终能在可空类型的变量中存储非空类型的值,但反过来却不行(null不是非空类型的变量可以接受的值)。

二、协变:保留子类型化关系

一个协变类是一个泛型类(以Producer为例),对这种类来说,下面的描述是成立的:如果A是B的子类型,那么Producer就是Producer的子类型。我们说子类型被保留了。

在Kotlin中,要声明类在某个类型参数上是可以协变的,在该类型参数的名称前加上out关键字即可:

// 类被声明成在T上协变
interface Producer<out T> {
    fun produce(): T
}
  • 定义一个不变型的类似集合的类
open class Animal {
    fun feed() { ... }
}

// 类型参数没有声明成协变的
class Herd<T: Animal> {
    val size: Int get() = ...

    operator fun get(i: Int): T { ... }
}
  • **使用一个不变型的类似集合的类
// Cat是一个Animal
class Cat: Animal() {
    fun cleanLitter() { ... }
}

fun takeCareOfCats(cats: Herd<Cat>) {
    for(i in 0 until cats.size) {
        cats[i].cleanLitter()
        // 错误:推导的类型是Herd<Cat>,但期望的却是Herd<Animal>
        // feedAll(cats)
    }
}

如果尝试把猫群传给feedAll()函数,在编译期会得到类型不匹配的错误。因为Herd类中的类型参数T没有任何变型修饰符,猫群不是畜群的子类。

因而可以使用协变来解决。

  • 使用一个协变的类似集合的类
// 类型参数T现在是协变的
class Herd<out T: Animal> {
    ...
}

不能把任何类都变成协变的:这样不安全。让类在某个类型参数变成协变,限制了该类中对该参数使用的可能性。

图1:函数参数的类型叫做in位置,而函数返回类型叫作out位置

图1中,类的类型参数out关键字要求所有使用T的方法只能把T放在out位置而不能放在in位置。这个关键字约束了使用T的可能性,这保证了对应子类型关系的安全性

class Herd<out T: Animal> {
    val size: Int get() = ...
    
    // 把T作为返回类型使用
    operator fun get(i: Int): T { ... }
}

这里一个out位置,可以安全地把类声明成协变的。

重申一下,类型参数T上的关键字out有两层含义:

  • 子类型化被保留(Producer是Producer的子类型)
  • T只能用在out位置

类型参数不光可以直接当作参数类型或者返回类型使用,还可以当作另一个类型类型实参

注意构造方法参数既不在in位置,也不在out位置

位置规则只覆盖了类外部可见的(publicprotectedinternalAPI私有方法的参数既不在in位置也不在out位置

变型规则只会防止外部使用者对类的误用但不会对类自己的实现起作用:

class Herd<out T: Animal>(private var leadAnimal: T, varage animals: T) { ... }

三、逆变:反转子类型化关系

逆变的概念可以被看成协变的镜像:对一个逆变类来说,它的子类型化关系与用作类型实参的类的子类型化关系是相反的。

interface Comparator<in T> {
    // 在“in”位置使用T
    fun compare(e1: T, e2: T): Int { ... }
}

这个接口方法只是消费类型为T的值。这说明T只在in位置使用,因此它的声明之前用了in关键字。

in关键字的含义是:对应类型的值是传递进来给这个类的方法的,并且被这些方法消费。

表1 协变的、逆变的和不变型的表

协变逆变不变型
ProducerConsumerMutableList
类的子类型化保留了:Producer是Producer的子类型子类型化反转了:Consumer 是 Consumer的子类型没有子类型化
T只能在out位置T只能在in位置T可以在任何位置

四、使用点变型:在类型出现的地方指定变型

**声明点变型:**在类声明时就能够制定变型修饰符是很方便的,因为这些修饰符会应用到所有类被使用的地方。

Java中,每一次使用带类型参数的类型的时候,还可以制定这个类型参数是否可以用它的子类型或者超类型替换,这叫作使用点变型

  • 带out投影类型参数的数据拷贝函数
// 可以给类型的用法加上“out”关键字:没有使用那些T用在“in”位置的方法
fun <T> copyDataByOut(source: MutableList<out T>,
                  destination: MutableList<T>) {
    for(item in source) {
        destination.add(item)
    }
}
  • 带in投影类型参数的数据拷贝函数
// 允许目标元素的类型是来源元素类型的超类型
fun <T> copyDataByIn(source: MutableList<T>,
                     destination: MutableList<in T>) {
    for(item in source) {
        destination.add(item)
    }
}

注意: Kotlin使用点类型直接对应**Java限界通配符**。Kotlin中的MutableList<out T>和Java中的MutableList<? extends T>是一个意思。in投影的MutableList<in T>对应到Java的MutableList<? super T>

五、星号投影:使用 * 代替类型参数

星号投影语法,表明不知道关于泛型实参的任何信息

星号投影的语义:

  • 需要注意的是**MutableList<*>MutableList<Any?>** 不一样(这里非常重要的是MutableList<T>在T上是不可变的)。
  • MutableList<*>这种列表包含的是任何类型的元素
  • MutableList<*>是包含某种特定类型元素的列表

使用星号投影的语法:不需要使用任何在签名中引用类型参数的方法,或者只是读取数据不关心它的具体类型

// 每一种列表都是可能的实参
fun printFirst(list: List<*>) {
    // isNotEmpty()没有使用泛型类型参数
    if(list.isNotEmpty())
        // first()现在返回的是Any?,但是这里足够了
        println(list.first())
}

fun printFirstTest() {
    println(printFirst(listOf("Svetlana", "Dmitry")))
}

在使用点类型的情况下,有一个替代方案——引入一个泛型类型参数:

// 再一次,每一种列表都是可能的实参
fun <T> printFirstByT(list: List<T>) {
    if(list.isNotEmpty()) {
        // first()现在返回的实T的值
        println(list.first())
    }
}

星号投影的语法很简洁,但只能用在对泛型类型实参的确切值不感兴趣的地方:只是使用生产值的方法,而且不关心那些值的类型