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(类型是 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>) = Unit.also { list.add(Dog()) } // add Dog

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

    val cat: Cat = getCat(cats)
    addAnimal(animals)

    getCat(animals) // 报错,如果可以传入 animals,由于 animals 中可能有 Dog,那么 get 的就不一定是 Cat!
    addAnimal(cats) // 报错,如果可以传入 cats,由于 cats 集合只能 add Cat,那么在 add Dog 时就会出错!
}

泛型的型变 Variance

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

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

总结

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

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

  • 类似 <? super Animal><? extends Animal> 这种属于型变
  • 类似 <T super Animal><T extends 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>
}

声明处型变

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

包括两种类型:(声明处)逆变(声明处)协变

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

声明处逆变 in -- 印尼

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

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

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

foo2(animal)     // 编译通过,可以认为 Home<Cat> 是 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(cat)         // 编译通过,可以认为 Home<Animal> 是 Home<Cat> 的父类

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

使用处型变

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

包括两种类型:(使用处)逆变(使用处)协变

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

使用处逆变 in -- 印尼

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

当把方法 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>) = Unit.also { list.add(Dog()) } // add Dog

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

    val cat: Cat = getCat(cats)
    addAnimal(animals)

    getCat(animals) // 报错,如果可以传入 animals,由于 animals 中可能有 Dog,那么 get 的就不一定是 Cat!
    addAnimal(cats) // 报错,如果可以传入 cats,由于 cats 集合只能 add Cat,那么在 add Dog 时就会出错!
}

使用处逆变

当把方法 getCat 的实参由 MutableList<Cat> 改为 MutableList<in Cat> 后,当要求传入 MutableList<Cat> 时,也可以传入 MutableList<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  白乾涛  阅读(6567)  评论(0编辑  收藏  举报