End

Kotlin 朱涛-10 泛型 型变 逆变 in 协变 out 星投影

本文地址


目录

10 | 泛型:逆变or协变,傻傻分不清?

泛型是在代码架构的层面进行的一种抽象,从而达到代码逻辑尽可能复用的目的。

泛型基础

类声明

open class Animal
class Cat : Animal()
class Dog : Animal()

在【类名】后增加泛型

类名 后面加上 <T>,可以为增加泛型支持。

class List1<T>          // T 代表泛型的形参
class List2<T : Animal> // 指定泛型的上界:泛型实参必须 is a Animal
class List3<T : Cat>    // 指定泛型的上界:泛型实参必须 is a Cat

fun main() {
    List1<Cat>()    // Cat 代表泛型的实参,实参的意思是:一个具体的类型
    List1<Dog>()
    List1<String>()

    List2<Cat>()
    List2<Dog>()
    List2<String>() // 报错,提示 Expected: Animal, Found: String

    List3<Cat>()
    List3<Dog>()    // 报错,提示 Expected: Cat, Found: Dog
    List3<String>() // 报错,提示 Expected: Cat, Found: String
}

在【fun】后增加泛型

在关键字 fun 后面加上 <T>,可以为函数增加泛型支持。

fun <T> foo1(tv: T) = println("Any")             // T 代表泛型的形参
fun <T : Animal> foo2(tv: T) = println("Animal") // 指定泛型的上界:泛型实参必须 is a Animal
fun <T : Cat> foo3(tv: T) = println("Cat")       // 指定泛型的上界:泛型实参必须 is a Cat

fun main() {
    foo1(Cat())
    foo1(Dog())
    foo1("")

    foo2(Cat())
    foo2(Dog())
    foo2("")    // 报错,提示 Required: Animal, Found: String

    foo3(Cat())
    foo3(Dog()) // 报错,提示 Required: Cat, Found: Dog
    foo3("")    // 报错,提示 Required: Cat, Found: String
}

泛型的不变性问题

如果 Cat、Dog 是 Animal 的子类,那么编译器会认为:

  • 泛型类 Home<Cat> 与泛型类 Home<Animal> 不是同一类型,且不存在继承关系
  • 泛型集合 List<Cat> 与泛型集合 List<Animal> 不是同一类型,且不存在继承关系

这就是 泛型的不变性问题

泛型类的不变性

class Home<T>

fun foo1(home: Home<Animal>) = println("animal")
fun foo2(home: Home<Cat>) = println("cat")

fun main() {
    val animal: Home<Animal> = Home<Animal>()
    val cat: Home<Cat> = Home<Cat>()
    foo1(animal) // animal
    foo2(cat)    // cat

    foo1(cat)   // 报错,提示 Required: Home<Animal>, Found: Home<Cat>
    foo2(animal) // 报错,提示 Required: Home<Cat>, Found: Home<Animal>
}

泛型集合的不变性

fun getCat(list: MutableList<Cat>): Cat = list[0]          // get Cat
fun addAnimal(list: MutableList<Animal>) = list.add(Dog()) // add Dog

fun main() {
    val cats: MutableList<Cat> = mutableListOf(Cat())
    val animals: MutableList<Animal> = mutableListOf(Dog())
    val animals2: MutableList<Animal> = mutableListOf(Cat())

    getCat(cats)
    addAnimal(animals)

    getCat(animals)  // 报错,如果可以传入 animals,那么在 get Cat 时就会出错!
    getCat(animals2) // 报错,因为没办法确认 animals2 中都是 Cat
    addAnimal(cats)  // 报错,如果可以传入 cats,那么在 add Dog 时就会出错!
}

泛型的型变 Variance

泛型的型变,就是为了解决泛型的不变性问题。

型变分为两种:逆变和协变。

所谓的型变,对应到 Java 中,是指有 ? 的泛型:

  • <? super Animal><? extends Animal> 这种有 ? 的,属于型变
  • <T super Animal><T extends Animal> 这种没有 ? 的,不属于型变

总结

  • 逆变 in:泛型 T 最终会以函数参数的形式,被传入函数里面,这往往是一种写入行为
    • 通常作为参数传入
    • 类似 Java 中的 <? super T>
    • 可以写入不可以读取(只能以 Any? 读取)
  • 协变 out:泛型 T 最终会以函数返回值的形式,被传出函数外面,这往往是一种读取行为
    • 通常作为返回值传出
    • 类似 Java 中的 <? extends T>
    • 可以读取,不可以写入(只能写入 Nothing)

总结:

  • Consumer in, Producer out :消费者使用 in,生产者使用 out
  • 传入用 in,传出用 out
  • 泛型作为参数用 in,泛型作为返回值用 out
  • 注意:函数传入参数的时候,并不一定就意味着写入
  • 某些情况下,valprivate var,可以用 out,因为其也满足 可以读取不可以写入 的特性
  • 正常情况下,同时作为参数和返回值的泛型参数,无法直接使用 in 或者 out 来修饰泛型
  • 特殊场景下,同时作为参数和返回值的泛型参数,可以用 @UnsafeVariance 解决型变冲突

不使用型变时

class Home<T>

fun foo1(home: Home<Animal>) = println("animal")
fun foo2(home: Home<Cat>) = println("cat")

fun main() {
    val animal: Home<Animal> = Home<>()
    val cat: Home<Cat> = Home<>()
    foo1(animal) // animal
    foo2(cat)    // cat

    foo1(cat)   // 报错,提示 Required: Home<Animal>, Found: Home<Cat>
    foo2(animal) // 报错,提示 Required: Home<Cat>, Found: Home<Animal>
}

声明处型变

声明处型变,就是修改泛型参数声明处的代码,即在泛型形参前加 in/out

Java 中没有声明处型变,只有使用处型变

声明处逆变 in -- 印尼苏

记忆口诀:印尼(in 逆),即 in 代表 逆变 -- 父子关系反转 -- 反转代表 super

当把 Home 类的声明由 Home<T> 改为 Home<in T> 后,当要求传入 Home<Cat> 时,也可以传入 Home<Animal>

class Home<in T> // 声明处逆变。Java 中没有声明处型变,因为不能在这里出现 <? super T>

foo1(Home<Cat>())    // 报错,提示 Required: Home<Animal>, Found: Home<Cat>
foo2(Home<Animal>()) // 编译通过

此时,可以认为 Home<Cat>Home<Animal> 的父类,这种父子关系颠倒的现象,就叫做 泛型的逆变

声明处协变 out

当把 Home 类的声明由 Home<T> 改为 Home<out T> 后,当要求传入 Home<Animal> 时,也可以传入 Home<Cat>

class Home<out T> // 声明处协变。Java 中也没有声明处型变,因为不能在这里出现 <? extends T>

foo1(Home<Cat>())    // 编译通过
foo2(Home<Animal>()) // 报错,提示 Required: Home<Cat>, Found: Home<Animal>

此时,仍可认为 Home<Animal>Home<Cat> 的父类,这种父子关系一致的现象,就叫做 泛型的协变

使用处型变

使用处型变,就是修改泛型参数使用处的代码,即在泛型实参声明前加 in/out

Java 中也有使用处型变,例如 List<? extends Number> list = new ArrayList<>();

使用处逆变 in -- 印尼苏

记忆口诀:印尼(in 逆),即 in 代表 逆变 -- 父子关系反转 -- 反转代表 super

当把方法 foo2 的实参由 Home<Cat> 改为 Home<in Cat> 后,当要求传入 Home<Cat> 时,也可以传入 Home<Animal>

fun foo2(home: Home<in Cat>) = println("cat") // 使用处逆变

foo2(animal)     // 编译通过,可以认为 Home<Cat> 是 Home<Animal> 的父类

此时,可以认为 Home<Cat>Home<Animal> 的父类,这种父子关系颠倒的现象,就叫做 泛型的逆变

使用处协变 out

当把方法 foo1 的实参由 Home<Animal> 改为 Home<out Animal> 后,当要求传入 Home<Animal> 时,也可以传入 Home<Cat>

fun foo1(home: Home<out Animal>) = println("animal") // 使用处协变

foo1(cat) // 编译通过,可以认为 Home<Animal> 是 Home<Cat> 的父类

此时,仍可认为 Home<Animal>Home<Cat> 的父类,这种父子关系一致的现象,就叫做 泛型的协变

型变案例:泛型集合

不使用型变时的代码

fun getCat(list: MutableList<Cat>): Cat = list[0]          // get Cat
fun addAnimal(list: MutableList<Animal>) = list.add(Dog()) // add Dog

fun main() {
    val cats: MutableList<Cat> = mutableListOf(Cat())
    val animals: MutableList<Animal> = mutableListOf(Dog())
    val animals2: MutableList<Animal> = mutableListOf(Cat())

    getCat(cats)
    addAnimal(animals)

    getCat(animals)  // 报错,如果可以传入 animals,那么在 get Cat 时就会出错!
    getCat(animals2) // 报错,因为没办法确认 animals2 中都是 Cat
    addAnimal(cats)  // 报错,如果可以传入 cats,那么在 add Dog 时就会出错!
}

使用处逆变

当把方法 getCat 的实参由 MutableList<Cat> 改为 MutableList<in Cat> 后,当要求传入 MutableList<Cat> 时,也可以传入 MutableList<Animal> 了。

但是,返回值就不能简单的用 list[0],因为返回值的类型其实是 Animal。

fun getCat(list: MutableList<in Cat>): Cat { // 使用处逆变
    list.add(Cat())             // 逆变可以写入 Cat
    val cat1: Cat = list[0]     // 报错,提示 Required Cat, Found Any?
    list.add(Animal())          // 报错,提示 Required Cat, Found Animal
    list.add(Dog())             // 报错,提示 Required Cat, Found Dog
    val cat: Any? = list[0]     // 编译通过。逆变时不可以读取 Cat(只能以 Any? 读取)
    return cat as? Cat ?: Cat() // 逆变时只能以 Any? 读取
}

getCat(animals) // 编译通过,可以认为 MutableList<Cat> 是 MutableList<Animal> 的父类

使用处协变

当把方法 addAnimal 的实参由 MutableList<Animal> 改为 MutableList<out Animal> 后,当要求传入 MutableList<Animal> 时,也可以传入 MutableList<Cat>

fun addAnimal(list: MutableList<out Animal>) { // 使用处协变
    val animal: Animal = list[0] // 协变可以读取 Animal
    list.add(Animal())           // 报错,提示 Required: Nothing, Found: Animal
    list.add(Dog())              // 报错,提示 Required: Nothing, Found: Dog
    list.add(throw Exception())  // 编译通过。协变时不可以写入(只能写入 Nothing)
}

addAnimal(cats) // 编译通过,可以认为 MutableList<Animal> 是 MutableList<Cat> 的父类

星投影

所谓的星投影就是,当我们不关心实参到底是什么的时候,可以用星号作为泛型的实参

星投影语法可以用来简化泛型类型的使用,特别是在我们只关心某些泛型类型参数的上界或下界时。

class Home<T>

fun getHome(type: Int): Home<*> { // 无法确定返回值中泛型的类型,就可以用星号 * 作为泛型的实参
    return when (type) {
        1 -> Home<Animal>()
        2 -> Home<Boolean>()
        else -> Home<Any?>()
    }
}

小结

2016-05-14

posted @ 2016-05-14 03:37  白乾涛  阅读(6627)  评论(0)    收藏  举报