在Kotlin:泛型杂谈(上)中,从泛型扩展属性、非空约束、实例化类型参数三个方面简单介绍了一下Kotlin中泛型相对于Java独有的特性,接下来将继续介绍Kotlin中泛型相关的一些知识点。
- 类型和子类型
定义:如果程序需要的是类型A的值,能够使用类型B的值来当作类型A的值,那么类型B就是类型A的子类型,类型A就是类型B的超类型。
初次接触这个概念的时候以为就是类与子类的别名,那是否真的如此呢?让我们先看一个例子:
var name: String?
在上面的例子中,我们其实使用String
类构造了2种
类型:非空类型和可空类型;在此例中我们可以知道:非空类型name是可空类型name的子类型。
上述可知,类型与子类型并非完全等价于类与子类的关系。这个概念有什么作用呢?让我们继续往下看。
- 协变
open class Work {
open fun doSomething() {
println("doSomething")
}
}
class Cook: Work() {
override fun doSomething() {
println("cook")
}
}
fun doSomething(works: Array<Work>) {
works.forEach {
it.doSomething()
}
}
fun main(args: Array<String>) {
val works: Array<Cook> = arrayOf(Cook())
doSomething(works)
}
在上例中,将Array<Cook>
类型的参数传递给了doSomething
函数,但该函数期望的是Array<Work>
,由于类型不匹配导致了编译错误,那如果想要正确运行又该如何处理呢?
fun main(args: Array<String>) {
val works: Array<Cook> = arrayOf(Cook())
doSomething(works as Array<Work>)
}
上面我们通过显式类型转换达到了目的,但是这样做存在两个问题:
- 代码啰嗦。
- 无法在编译期间杜绝错误转换问题,比如:
arrayOf("Error") as Array<Work>
这里我们试图将Array<String>
转换成Array<Work>
,编译期间只会给出
Unchecked cast: Array<String> to Array<Work>
的警告,只有在运行时才会抛出异常。那是否有更好的方法处理此类问题呢?下面让我们一起了解一下Kotlin的协变:
定义:可以使用子类型替换需要父类型的位置,即当参数或返回值需要超类型时传入子类型参数或返回子类型的值。
使用:使用out
关键字来修饰泛型类型,从而将泛型类定义为协变。
根据上面的描述,我们可以将上述例子稍作修改,从而解决上面显示类型转换所带来的问题:
fun doSomething(works: Array<out Work>) {
works.forEach {
it.doSomething()
}
}
fun main(args: Array<String>) {
val works: Array<Cook> = arrayOf(Cook())
doSomething(works)
}
非常简单对不对?下面思考一个问题,是否可以把任何类型都声明为协变?要回答这个问题,让我们先看一下Kotlin中的两个关键字out
和in
:
如果一个类声明了一个类型参数T并有一个使用T的方法,如果方法将T作为返回类型,那么T就在out
位,如果将T作为参数类型,那么T就在in
位置,比如:
interface A<T> {
fun getT(): T
fun setT(t: T)
}
上面例子中,T在getT
中位于out
位置,在setT
中位于in
位置。
通过对out
和in
的解释,我们来回答是否可以把任何类型都声明为协变?
这个问题:即声明为协变
的类型,只能用在上面所说的out
位置以保证类型安全。
- 逆变
上面我们对协变进行了详细的介绍,那么逆变又是什么东西呢?看下面的例子:
fun main(args: Array<String>) {
val anyComparator = Comparator<Any> {
e1, e2 -> e1.hashCode() - e2.hashCode()
}
val names: List<String> = listOf("Tom", "Joy")
println(names.sortedWith(anyComparator))
}
上面例子中,我们声明了一个Comparator<Any>
比较器,但sortedWith
却希望一个Comparator<String>
比较器,根据上面类型与子类型的描述,这里Comparator<Any>
成了Comparator<String>
的子类型,与协变(Comparator<String>
为Comparator<Any>
的子类型)相比,它的子类型化关系发生了逆转,这就是所谓的逆变
。
让我们看一下sortedWith
的定义:
public fun <T> Iterable<T>.sortedWith(comparator: Comparator<in T>): List<T> {
//doSomething...
}
从定义中可知,只需要使用in
关键字来修饰泛型类型,就可以将泛型类定义为逆变
;并且只能用在上面所说的in
位置。
上面我们对协变
、逆变
进行了介绍,这两个特性的主要目的是提高程序安全性,将一些运行时错误(比如上面的类型转换)杜绝在编译期;但也有一些我们需要注意的细节:
- 只读属性,类型参数用在了
out
位置,可变属性用在了out
位置和in
位置。 - 位置规则只覆盖了类外部可见(public、protected、internal)API,私有方法的参数既不在
in
位置也不在out
位置;这是因为协变
、逆变
规则设计的目的是防止外部使用者对类的误用,因此不会对类自己的实现起作用。 - 构造方法的参数既不在
in
位置,也不在out
位置;即使类型参数声明为协变,仍然可以在构造方法参数的声明中使用它;这是因为协变
、逆变
规则设计的目的是防止外部使用者对类的误用,但是类的构造方法属于类实例创建后无法再次调用的方法,所以不存在误用的风险。
- 星号投影
有些时候我们不知道或不关心泛型类型实参,这个时候我们可以使用星号投影;它的语法很简单,比如List<*>
,由于星号投影并不知道实际的类型实参,因此该特性只能调用类中生产值的方法,且不需要关心值的类型。
好了,上面我们从类型和子类型、协变、逆变、星号投影对Kotlin中泛型剩下的特性进行了介绍,如有错误疏漏之处,还希望能与大家一起探讨。^ _ ^
网友评论