文章目录
- 声明泛型函数
- 声明泛型类
- 型变
- 型变的概念
- 不变
- 协变
- 逆变
- 小结
- 声明处型变
- 使用处型变
- 星投影
- 总结
声明泛型函数
泛型允许你定义带类型形参的类型。当这种类型的实例被创建出来的时候,类型形参被替换成称为类型实参的具体类型。Kotlin中泛型的使用与声明和Java很相似,我们声明一个泛型函数,可以像下面这么写:
fun <T> sayHello(item : T) { //普通函数
println(" ${item.toString()} say hello")
}
fun <T> T.sayHi() { //扩展函数
println("${toString()} say hi")
}
类型参数T的声明在关键字 fun 和函数名称之间。使用泛型函数时,我们可以显式地指定类型实参,编译器可以推导出来时也可以省略:
val xiaoMing = "xiaoming"
xiaoMing.sayHi<String>()
sayHello(xiaoMing)
声明泛型类
Kotlin 通过在类名称后加上一对尖括号 ,井把类型参数放在尖括号内来声明泛型类及泛型接口。一旦声明之后,就可以在类的主体内像其他类型一样使用类型参数:
interface Speakable<T> {
fun sayHello(item: T)
}
class Person<T>(var name: T) :Speakable<T>{
override fun sayHello(item: T) {
println("$name say hello to $item")
}
}
val xiaoming = Person("xiaoming")
xiaoming.sayHello("xiaogang") //输出:xiaoming say hello to xiaogang
使用泛型类时如果编译器可以推断出类型参数那么可以省略类型实参。实现泛型接口时需要为它的形参提供一个类型实参(在上面的例子中,是Person用自己的类型形参作为Speakable接口的实参)。
#类型参数约束
类型参数约束可以限制作为(泛型)类和(泛型)函数的类型实参的类型。如果把一个类型指定为泛型类型形参的上界约束,在泛型类型具体的初始化中,其对应的类型实参就必须是这个具体类型或者它的子类型。泛型约束的语法如下:
fun <T:Number> half(item :T):Double {
return item.toDouble() / 2
}
println(half(3)) //1.5
一旦指定了类型形参 T 的上界,你就可以把类型 T 的值当作它的上界(类型)的值使用。例如,可以调用定义在上界类中的方法。而当没有指定类型参数的上界时,它的上界默认为Any?。
型变
型变的概念描述了拥有相同基础类型和不同类型实参的(泛型)类型之间是如何关联的:例如, List<String>和 List<Any>之间如何关联。为什么我们需要型变呢?因为型变的存在,很大的提高了泛型API的灵活性。
型变的概念
为了讨论类型之间的关系,需要熟悉子类型这个术语。任何时候如果需要的是类型 A 的值,你都能够使用类型 B 的值(当作 A 的值),类型 B 就称为类型 A 的子类型。术语超类型是子类型的反义词。如果 A 是 B 的子类型,那么 B 就是 A 的超类型。知道一个类是否是另一个类的子类型是十分重要的,因为编译器在每一次给变量赋值或者给函数传递实参的时候都要做一项检查,只有值的类型是变量类型的子类型时,才允许变量存储该值。因此我们可以将子类型作为超类型来使用,这就是型变能提高API灵活性的原因。
简单的情况下,子类型和子类本质上意味着一样的事物。例如,Int 类是 Number 的子类,因此 Int 类型是 Number 类型的子类型。如果一个类实现了一个接口,它的类型就是该接口类型的子类型:String 是 CharSequence 的子类型 。
那么对于类型参数分别为子类型和超类型的两个泛型类型,比如 List<Int> 和 List<Number> 来说,情况是怎么样的呢?显然,它们只有三种可能的关系:
- List<Int> 是 List<Number> 的子类型,这种情况我们称之为协变。
- List<Int> 是 List<Number> 的超类型。这种情况我们称之为逆变。
- List<Int> 和 List<Number> 没有关系。这种情况我们称之为不变。
不变
首先来看最简单的不变情况:List<Int> 和 List<Number>是两个不相关的类,这不会隐藏什么危险,但是我们来看下面一个情形:
open class Animal {
var weight: Int = 0
fun feed() {}
}
class Cat : Animal() {
fun catchMouse() {}
}
fun feedAll(animal: List<Animal>) {
for (i in 0 until animal.size) {
animal[i].feed()
}
}
val cats = listOf(Cat(),Cat())
// feedAll(cats) //error
当我们将List <Cat> 作为参数使用feeaAll()时,将会报错(假设 List是不变的),因为List<Animal> 和 List<Cat>没有关系(当然我们可以使用强制类型转换,但这种方式有很大的风险),于是我们只能再声明一个feedAll(cats: List<Cat>),以及为更多的可能出现的Dog、Hamster、Tortoise…等声明方法,这显然是种很浪费的情况。
协变
如果List是协变的,那么这种难题就迎刃而解了,既然 List<Cat> 是 List<Animal> 的子类型,那么参数类型为 List<Animal> 的地方完全可以使用 List<Cat>。但是这个会有什么风险呢?看下面这种情形:
class Dog : Animal()
fun walk(animal: List<Animal>) {
animal.add(Dog())
}
walk(cats)
for (cat in cats) {
cat.catchMouse() // error: dog can't catch mouse
}
我们想带着一群猫出去散步,我们有一个带着一群动物的出去散步的方法,猫是动物,于是通过协变,我们成功带着这群猫去散步了。但是在散步的路上,一只狗混了进来(假设List有add方法),狗当然也是动物,于是狗安然无恙地跟着我们散完步回家了,回到家后这群猫就要去抓老鼠,这时候这只狗就混不下去了,因为它并没有抓老鼠的方法。于是程序就出错了。
我们可以看出,问题出在我们让狗混进了猫群,换言之,当我们把一个子类类参的集合通过协变转换成父类类参的集合之后,有将其他子类放入集合的危险。而只要避免这种情况:我们只使用集合,而不向集合中添加元素,从而不改变这个集合中拥有的元素类型,那么这种行为就是安全的。
逆变
最后是逆变,初看这个概念可能会有点奇怪,因为类型参数的子类型关系被反转了。但是逆变也是很有用武之地的。来看下面这种情况:我们有两只猫,我们想知道哪只猫更重,我们当然还记得曹冲称象的故事:
class Elephant: Animal()
fun compare(elephant1: Elephant,elephant2: Elephant):Int {
return boat(elephant1) - boat(elephant2)
}
fun boat(elephant: Elephant): Int {
return depthOfWater()
}
虽然曹冲称的是象,但把象换成猫情况也是一样的:于是我们找了一艘船,分别把猫放上去,再比较船吃水的深度,谁轻谁重就一目了然了。事实上,对于任何动物,这个方法都是OK的:
interface Comparator<T> {
fun compare(e1: T,e2: T):Int
}
class BoatComparator: Comparator<Animal> {
override fun compare(e1: Animal, e2: Animal): Int {
return boat(e1) - boat(e2)
}
private fun boat(e:Animal):Int {
return depthOfWater()
}
}
这样看起来很好,当我们想用这个方法来找到一群猫中那只最重的猫的时候,我们理所当然的会想到使用这个办法:
fun whoIsHeaviestCat(list: List<Cat>, comparator: Comparator<Cat>): Int{
var index = 0
for (i in 1..list.size) {
if (comparator.compare(list[index],list[i]) >0) {
index = i
}
}
return index
}
val list = listOf(Cat(),Cat(),Cat())
whoIsHeaviestCat(list, BoatComparator()) //error
但是当我们使用BoatComparator的时候,方法报错了,因为方法参数是Comparator<Cat>,而我们使用的是Comparator<Animal>,虽然Comparator<Animal>显然也可以用来比较猫。怎么办呢,如果我们再声明一个BoatComparator: Comparator<Cat>,这又是之前协变时的问题:太浪费了。于是这时候逆变就派上用场了:如果Comparator<T>是逆变的,那么Comparator<Animal>就是 Comparator<Cat> 的子类型,那么 whoIsHeaviest(list,BoatComparator()) 就不会报错了。
同样的,逆变又可能导致什么问题呢?来想象下面这种情况:出于某种想法,我们想修改一下Comparator的API:把compare函数的返回值改为e1、e2中较大的那一个,显然这时返回类型就是T:
interface Comparator<T> {
fun compare(e1: T,e2: T):T
}
于是BoatComparator的返回值也需要改变成Animal,到此为止还没什么问题,但是在whoIsHeaviestCat中我们再调用compare时,得到的返回值就也变成了Animal——类型信息在逆变中失去了,这个时候我们当然知道它是一只Cat,可以使用强制类型转换回来,但前面也说了,这不是一种好的代码风格。 于是我们知道了,逆变的风险在于丢失类型信息,而如果我们保证没有在逆变的地方将这个值保存或者传递,那么就没有变量持有这个值,也就没有丢失类型信息的问题了。
小结
综上所述,我们为了接口的灵活和安全,应该有限制的使用形变,我们应该对协变的对象只读取不写入,而对逆变的对象只写入不读取:第一种对象被称为生产者,第二种对象被称为消费者。
在函数或者方法声明中类型参数的使用可以分为 in 位置和 out 位置。考虑这样一个类,它声明了一个类型参数 T 并包含了一个使用 T 的函数。如果函数是把 T 当成返回类型,我们说它在 out 位置。这种情况下,该函数生产类型为 T 的值 。 如果 T 用作函数参数的类型,它就在 in 位置。
所以对于生产者,它只应该被读取,所以只能在用于 out 的位置;而对于消费者,它只应该被写入,所以只能在 in 的位置。这也就是Kotlin声明泛型的规则。
声明处型变
在 Kotlin 中,我们可以使用 in 或 out 修饰符标注泛型类的类型参数来确保它仅从类成员中消费或者生产:当一个类 C 的类型参数 T 被声明为 out 时,它就只能出现在 C 的成员的 out 位置,但回报是 C 可以安全地作为 C的超类。即类 C 是在参数 T 上是协变的,或者说 T 是一个协变的类型参数。 也就是说 C 是 T 的生产者,而不是 T 的消费者:
interface Source<out T> {
fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // 这个没问题,因为 T 是一个 out-参数
// ……
}
当一个类 C 的类型参数 T 被声明为 in 时,它使得一个类型参数逆变,它就只能出现在 C 的成员的 in 位置,只可以被消费而不可以被生产:
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
// 因此,我们可以将 x 赋给类型为 Comparable <Double> 的变量
val y: Comparable<Double> = x // OK!
}
使用处型变
有些类既不是协变也不是逆变的,因为它同时生产和消费指定为它们类型参数的类型的值 。 但是对于这个类型的变量来说,在某个特定函数中只被当成生产者和消费者其中一种角色使用。对于这种情况, Kotlin 提供了 的解决方案就是使用处型变:当函数的实现调用了那些类型参数只出现在 out 位置(或只出现在 in位置)的方法时,可以在函数定义中给特定用途的类型参数加上变型修饰符,从而在这个函数内,这个类型参数的逆变或协变是安全的:
fun <T> copyData(source: MutableList<T>,
destination: MutableList<in T>) {
for (item in source) {
destination.add(item)
}
}
val ints: MutableList<Int> = mutableListOf(1, 2, 3)
val any: MutableList<Any> = mutableListOf()
copyData(ints,any)
可以为类型声明中类型参数任意的用法指定变型修饰符,这些用法包括:形参类型、局部变量类型、函数返回类型等等。这种方式也被叫做类型投影,因为在这个函数内,修饰符修饰的函数参数的使用被限制了。
星投影
如果你不知道关于泛型实参的任何信息,你可以使用星号表示为C<*>。
* 和 Any? 是不同的,比如 MutableList <Any?>这种列表包含的是任意类型的元素。而另一方面,MutableList <*>是包含某种特定类型元素的列表,但是你不知道是哪个类型 。因为不知道*是哪个类型,你不能向列表中写入任何东西,因为你写入的任何值都可能会违反调用代码的期望。但是从列表中读取元素是可行的,因为列表会返回所有 Kotlin 类型的超类型 Any? 。
编译器会把 MutableList <*>当成 out 投影的类型,在上面这个例子中,MutableList <*>投影成了 MutableList<out Any?>:当你没有任何元素类型信息的时候,读取 Any?类型的元素仍然是安全的,但是向列表中写入元素是不安全的 。所以星号投影的语法很简沽,但只能用在对泛型类型实参的确切值不感兴趣的地方:只是使用生产值的方法,而且不关心那些值的类型。
总结
本文介绍了Kotlin中泛型的声明和使用方式,并简单介绍了变型的概念和在Kotlin的实现。总的来说,Kotlin中的泛型和Java中的大同小异:使用处型变的 out T 相当于 Java 中的 ? extends T,in T 相当于 Java 中的 ? super T;声明处型变是使用处型变的简写方式;星投影 MyType<*> 对应于 Java 的 MyType<?>。