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