9.泛型
实化类型参数允许你在运行时的内联函数调用中引用作为类型实参的具体类型(对普通类和函数来说,这样行不通,因为类型实参在运行时会被擦除)。
声明点类型可以说明一个带类型参数的泛型类型,是否是另一个泛型类型的子类型或超类型,它们的基础类型相同但类型参数不同。例如,它能调节是否可以把List<Int>
类型的参数传递给期望List<Any>
的函数。使用点变型在具体使用一个泛型类型时做同样的事,达到和Java通配符一样的效果。
和Java不同,kotlin从一开始就有泛型,所以它不支持原生态类型,类型实参必须定义。kotlin始终要求类型实参要么被显式地说明,要么能被编译器推导出来。
普通(即非扩展)属性不能拥有类型参数,不能在一个类的属性中存储多个不同类型的值,隐藏生命泛型非扩展函数没有任何意义。
类型参数约束可以限制作为泛型类和泛型函数的类型实参的类型。如果你把一个类型指定为泛型类型形参的上界约束,在泛型类型具体的初始化中,其对应的类型实参就必须是这个具体类型或者它的子类型。
<T extends Number> T sum(List<T> list){ ... }
fun <T : Number> List<T>.sum() : T
在一个类型参数上指定多个约束
fun <T> ensureTrailingPeriod(seq : T) where T : CharSequence, T : Appendable {
if(!seq.endsWith('.')) { // 调用为CharSequence接口定义的扩展函数
seq.append('.') //调用Appendable接口的方法
}
}
如果你声明的是泛型类或者泛型函数,任何类型实参,包括那些可空的类型实参,都可以替换他的类型形参。事实上,没有指定上界的类型形参将会使用Any?
这个默认的上界。
JVM上的泛型一般是通过类型擦除实现的,就是说泛型类实例的类型实参在运行时是不保留的。可以声明一个inline
函数,使其类型实参不被擦除(或者,按照kotlin术语,称作实化)。
擦除泛型类型信息是有好处的,应用程序使用的内存总量较小,因为要保存在内存中的类型信息更少。
if(value is List<*>) { ... }
可以认为它就是拥有未知类型实参的泛型类型(或者类比于Java的List<?>
)
在as
和as?
转换中仍然可以使用一般的泛型类型。但是如果该类有正确的基础类型单类型实参时错误的,转换也不会失败,因为在运行时转换发生的时候类型实参是未知的。因此,这样的转换会导致编译器发出"unchecked cast"的警告。
kotlin编译器是足够智能的,在编译器它已经知道相应的类型信息时,is
检查是允许的。
内联函数的类型形参能够被实化,意味着你可以在运行时引用实际的类型参数。
>>> inline fun <reified T> isA(value : Any) = value is T
>>> println(isA<String>("abc"))
true
>>> println(isA<Int>("a"))
false
一个实化类型参数能发挥作用的最简单的例子就是标准库函数filterIsInstance
。类型实参在运行时是已知的,函数filterIsInstance
使用它来检查列表中的值是不是指定为该类型实参的类的实例。
/**
* Returns a list containing all elements that are instances of specified type parameter R.
*/
public inline fun <reified R> Iterable<*>.filterIsInstance(): List<@kotlin.internal.NoInfer R> {
return filterIsInstanceTo(ArrayList<R>())
}
编译器把实现内联函数的字节码插入每一次调用发生的地方。每次调用带实化类型参数的函数时,编译器都知道这次特定调用中用作类型实参的确切类型。因此,编译器可以生成引用作为类型实参的具体类的字节码。因为生成的字节码引用了具体类,而不是类型参数,它不会被运行时发生的类型参数擦除影响。
注意,带reified
类型参数的inline
函数不能在Java代码中调用。普通的内联函数可以像常规函数那样在Java中调用——它们可以被调用而不能被内联。带实化类型参数的函数需要额外的处理,来吧类型实参的值替换到字节码中,所以它们必须永远是内联的。这样它们不可能用Java那样普通的方式调用。
inline fun <reified T> loadService() {
return ServiceLoader.load(T::class.java)
}
可以按下面的方式使用实化类型参数:
- 用在类型检查和类型转换中(
is
,!is
,as
,as?
) - 使用kotlin反射api,(
::class
) - 获取相应的
java.lang.Class
(::class.java
) - 作为调用其他函数的类型实参
不能做下面的事情
- 创建指定为类型参数的类的实例
- 调用类型参数类的伴生对象的方法
- 调用带实化类型参数函数的时候使用非实化类型形参作为类型实参
- 把类、属性或者非内联函数的类型参数标记为
reified
把一个字符串列表传给期望Any
对象列表的函数是否安全,如果函数添加或者替换了列表中的元素就是不安全的,因为这样会产生类型不一致的可能性。否则它就是安全的。
任何时候如果需要的是类型A的值,你都能够使用类型B的值当做A的值,类型B就称为类型A的子类型。
如果A是B的子类型,那么B就是A的超类型。
只有值的类型是变量类型的子类型时,才允许变量存储该值。
非空类型A是可空的A?的子类型。
一个泛型类,例如MutableList
,如果对于任意两种类型A和B,MutableList<A>
既不是MutableList<B>
的子类型也不是它的超类型,它就被称为在该类型参数上是不变型的。Java中所有的类都是不变型的。如果A是B的子类型,那么List<A>
就是List<B>
的子类型,这样的类或者接口被称为协变的。
在kotlin中,要声明类在某个类型参数上是可以协变的,在该类型参数的名称前加上out
关键字即可:
interface Producer<out T> {
fun produce() : T
}
你不能把任何类都变成协变的:这样不安全,让类在某个类型参数变为协变,限制了该类中对该类型参数使用的可能性。要保证类型安全,它只能用在所谓的out
位置,意味着这个类只能生产类型T的值而不能消费他们。
在类成员的声明中类型参数的使用可以分为in
位置和out
位置。考虑这样一个类,它声明了一个类型参数T并包含了一个使用T的函数。如果函数是把T当成返回类型,我们说它在out
位置。这种情况下,该函数生产类型为T的值。如果T用作函数参数的类型,它就在in
位置。这样的函数消费类型为T的值。
函数参数的类型叫做in
位置,函数返回类型叫做out
位置。
类型参数T上的关键字out
有两层含义:
- 子类型化会被保留(
Producer<Cat>
是Producer<Animal>
的子类型) - T只能用在
out
位置
构造方法的参数既不在in
位置,也不在out
位置。即使类型参数声明成了out
,仍然可以在构造方法参数的生命中使用它:
class Herd<out T: Animal>(vararg animals: T) { ... }
如果把类的实例当成一个更泛化的类型的实例使用,变型会防止该实例被误用:不能调用存在潜在危险的方法。构造方法不是那种在实例创建之后还能调用的方法,因此它不会有潜在的危险。
位置规则只覆盖了类外部可见的(public
,protected
和internal
)API。私有方法的参数既不在in
位置也不在out
位置。变型规则只会防止外部使用者对类的误用单不会对类自己的实现起作用:
class Herd<out T : Animal>(private var leadAnimal: T, vararg animals : T)
逆变的概念可以被看成是协变的镜像:对一个逆变类来说,它的子类型化关系与用作类型实参的类的子类型化关系是相反的。
一个在类型参数上逆变的类是这样的一个泛型类,对这种类来说,下面的描述是成立的:如果B是A的子类型,那么Consumer<A>
就是Consumer<B>
的子类型。类型参数A和B交换了位置,所以我们说子类型化被反转了。
in
关键字的意思是,对应类型的值是传递进来给这个类的方法的,并且被这些方法消费的。和协变的情况类似,约束类型参数的使用将导致特定的子类型化关系。在类型参数T上的in
关键字意味着子类型化被反转了,而且T只能用在in
位置。
协变 | 逆变 | 不变型 |
---|---|---|
Producer |
Consumer |
MutableList |
类的子类型化保留了:Producer<Cat> 是Producer<Animal> 的子类型 |
子类型化翻转了:Consumer<Animal> 是Consumer<Cat> 的子类型 |
没有子类型化 |
T只能在out 位置 |
T只能在in 位置 |
T可以在任何位置 |
一个类可以在一个类型参数上协变,同事在另外一个类型参数上逆变。
public interface Function1<in P1, out R> : kotlin.Function<R> {
public abstract operator fun invoke(p1: P1): R
}
在类声明的时候就能够指定变形修饰符是很方便的,因为这些修饰符会应用到所有类被使用的地方,这被称作声明点变型。如果你熟悉Java的通配符类型(? extends 和 ? super),你会意识到Java用完全不同的方式处理变型。在Java中,每一次使用带类型参数的类型的时候,还可以指定这个类型参数是否可以用它的子类型或者超类型替换。这叫做使用点变型。
声明点变型带来了更简洁的代码,因为只用指定一次变型修饰符,所有这个类的使用者都不用再考虑这些了。Java中,库作者不得不一直使用通配符:Function<? super T, ? extends R>
来创建按照用户期望运行的API。
fun <T> copyData(source : MutableList<out T>,destination : MutableList<T>) {
for(item in source) { destination.add(item) }
}
kotlin也只是使用点变型,允许在类型参数出现的具体位置指定变型,即使在类型声明时他不能被声明成协变或逆变的。
source
不是一个常规的MutableList
,而是一个投影受限的MutableList
,只能调用返回类型是泛型类型参数的那些方法,只在out
位置使用它的方法。
Kotlin的使用点变型直接对应Java的限界通配符。Kotlin中的MutableList<out T>
和Java中的MutableList<? extends T>
是一个意思。in
投影的MutableList<in T>
对应到Java的MutableList<? super T>
。
使用*
代替类型参数,这种情况下泛型类型使用所有可能的类型实参,都是可以接受的。
MutableList<*>
和MutableList<Any?>
不一样,MutableList<T>
在T上是不变型的。MutableList<*>
投影成了MutableList<out Any?>
,当你没有任何元素类型信息时,读取Any?
类型的元素仍然是安全的,但是向列表中写入元素是不安全的。Kotlin的MyType<*>
对应于Java的MyType<?>
。对像Consumer<in T>
这样的逆变类型参数来说,星号投影等价于<in Nothing>
。
当类型实参的信息并不重要的时候,可以使用星号投影的语法:不需要使用任何在签名中引用类型参数的方法,或者只是读取数据而不关心它的具体类型。星号投影的语法很简洁,但只能用在对泛型类型实参的确切值不感兴趣的地方:只是使用生产值的方法,而且不关心那些值的类型。