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

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

1、为何存在变型:给函数传递实参

子类型:任什么时候候若是须要的是类型A的值,都可以使用类型B的值(看成A的值),类型B就称为类型A的子类型markdown

超类型:是子类型的反义词。若是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不是非空类型的变量能够接受的值)。oop

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

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

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

// 类被声明成在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没有任何变型修饰符,猫群不是畜群的子类。code

于是可使用协变来解决。orm

  • 使用一个协变的相似集合的类
// 类型参数T如今是协变的
class Herd<out T: Animal> {
    ...
}
复制代码

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

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

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

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) { ... }
复制代码

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

逆变的概念能够被当作协变的镜像:对一个逆变类来讲,它的子类型化关系与用做类型实参的类的子类型化关系是相反的。

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

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

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

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

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

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

**声明点变型:**在类声明时就可以制定变型修饰符是很方便的,由于这些修饰符会应用到全部类被使用的地方。

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>

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

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

星号投影的语义:

  • 须要注意的是**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())
    }
}
复制代码

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

相关文章
相关标签/搜索