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 - 注意:函数传入参数的时候,并不一定就意味着写入
- 某些情况下,
val或private 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
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/5491800.html

浙公网安备 33010602011771号