泛型类型参数
泛型允许你定义带类型形参的类型。当这种类型的实例被创建出来的时候,类型形参被替换成称为类型实参的具体类型。
使用方式和 Java 一样:List<String>,Map<K, V>。
同样,Kotlin 也可以自动推导类型:
val authors = listOf("Dmitry", "Svetlana")
和 Java 不同的是,Kotlin 始终要求类型实参要么被显示的说明,要么能被编译期推导出来。
泛型函数和属性
反省函数有自己的类型形参,这些类型形参在每次函数调用时都必须替换成具体的类型实参。
fun <T> List<T>.slice(indices: IntRange): List<T>
调用时可以显示的指定类型实参,也可以自动推导:
val letters = ('a' .. 'z').toList()
println(letters.slice<Char>(0 .. 2))
println(letters.slice(10 .. 13))
声明泛型类
Kotlin 声明泛型类的方法与 Java 一样:
interface List<T>{
operator fun get(index: Int): T
}
如果继承了泛型类,就得为基础类型的泛型形参提供一个类型实参。
class StringList: List<String>{
override fun get(index: Int): String = //...
}
class ArrayList<T>: List<T>{
override fun get(index: Int): T = //...
}
类型参数约束
类型参数约束可以限制作为(泛型)类和(泛型)函数的类型实参的类型。
使用冒号放在类型参数名称之后,作为类型形参上界的类型紧随其后,等同于 Java 中的 extends 关键字的作用:
fun <T: Number> T sum(List<T> list)
让类型形参非空
没指定上界的类型形参将会使用 Any? 作为默认上界。
如果想保证替换类型形参的始终是非空类型,可以通过指定一个约束来实现。如果出了可空性没有任何限制,可以使用 Any。
class Processor<T: Any>{
fun process(value: T){
value.hashCode()
}
}
运行时的泛型:擦除和实化类型参数
运行时的泛型:类型检查和装换
和 Java 一样,Kotlin 的泛型在运行时也被擦除了。
因为类型实参没有被保存下来,所以不能检查他们:
if(value is List<String>){...}
Error: Cannot check for instance of erased type: List<String>
那么如果想检查一个值是否是列表,而不是 set 或者其他对象,可以使用特殊的星号投影语法来做检查:
if(value is List<*>){...}
声明带实化类型参数的函数
虽然泛型在运行时会被擦除,但可以使用内联函数避免这种限制,内联函数的类型形参能够被实化。
inline 函数除了可以提高性能,内联代码之外,另一种场景就是类型参数可以被实化。
如果把函数声明成 inline 并且用 reified 标记类型参数,就可以实化该类型参数。
inline fun <reified T> inA(value: Any) = value is T
使用实化类型参数代替类引用
另一种实化类型参数的常见使用场景是为接收 java.lang.Class 类型参数的 API 构建适配器。
例如:
inline fun <reified T : Activity> Context.startActivity() {
val intent = Intent(this, T::class.java)
startActivity(intent)
}
变型:泛型和子类型化
变型的概念描述了拥有想用基础类型和不同类型实参(泛型)类型之间是如何关联的;例如,List<String> 和 List<Any> 之间如何关联。
为什么存在变型:给函数传递实参
fun addAnswer(list: MutableList<Any>){
list.add(42)
}
>>val strings = mutableListOf("abc", "bac")
>>addAnswer(strings)
Error: Type mismatch: inferred type is MutableList<String> but MutableList<Any> was expected
上述例子展示了 MutableList<Any> 与 MutableList<String> 之间转换存在的问题。
类、类型和子类型
如果需要的是 A 类型的值,你都能够使用类型 B 的值(当做 A 的值),类型 B 就成为类型 A 的子类型。
例如:Int 是 Number 的子类型,但不是 String 的子类型。
一个非空类型是它的可空版本的子类型。
一个泛型类,例如 MutableList ——如果对于任意两种类型 A 个 B,MutableList<A> 既不是MutableList<B> 的子类型也不是它的超类型,他就被称为在该类型参数上是不变型的。
而对于另外一些类,例如 List,如果 A 是 B 的子类型,那么 List<A> 就是 List<B> 的子类型。这样的类或者接口被称为协变的。
协变:保留子类型化关系
一个协变类是一个泛型类(我们以 Producer<T> 为例,对这种类来说,下面的描述是成立的:如果 A 是 B 的子类型,那么 Producer<A> 就是 Producer<B> 的子类型。我们说,子类型化被保留了。
Kotlin 中,要声明类再某个类型参数上是可以协变的,在该类型参数的名称前加上 out 关键字即可。
interface Producer<out T>{
fun producer(): T
}
将一个类的类型参数标记为协变的,在该类型实参没有精确匹配到函数中定义的类型形参时,可以让该类的值作为这些函数的实参传递,也可以作为这些函数的返回值。
你不能把任何类都变成协变的:这样不安全。让类在某个类型参数变为协变,限制了该类中对该类型参数使用的可能性。要保证类型安全,他只能用在所谓的 out 位置,意味着这个类只能生产类型 T 的值而不能消费它们。
在类成员的声明中类型参数的使用可以分为 in 位置和 out 位置。如果函数把 T 当成返回类型,我们说它在 out 位置。这种情况下,该函数生产类型为 T 的值。如果 T 用作函数参数的类型,他就在 in 位置。这样的函数消费类型为 T 的值。
参数类型 T 上的关键字 out 有两层含义:
- 子类型化会被保留(Producer<Cat> 是 Producer<Animal>)的子类型
- T 只能用在 out 位置
类型形参不光可以直接当做参数类型或者返回类型使用,还可以当做另一个类型的类型实参:
逆变:反转子类型化关系
逆变的概念可以看成是协变的镜像:第一个逆变来说,它的子类型化关系与作用理性实参的类的子类型化关系是相反的。
interface Comparator<in T>{
fun compare(e1: T, e2: T): Int
}
这个接口只是消费了类型为 T 的值。
一个为特定类型的值定义的比较器显然可以比较该类型任意子类型的值。
val anyComparator = Comparator<Any>{
e1, e2 -> e1.hashCode() - e2.hashCode()
}
val strings = listOf("a", "b", "c")
strings.sortedWith(anyComparator)
sortedWith 函数期望一个 Comparator<String> ,传递给它一个能比较更一般的类型的比较器是安全的。如果你要在特定类型的对象上执行比较,可以使用能处理该类型或者它的超类型的比较器。这说明 Comparator<Any> 是 Comparator<String> 的子类型,其中 Any 是 String 的超类型。不同类型之间的子类型关系和这些类型的比较器之间的子类型化关系截然相反。
一个在类型参数上逆变的类是这样的一个泛型类(我们以 Consumer<T> 为例),对这种类来说,下面的描述是成立的:如果 B 是 A 的子类型,那么 Consumer<A> 就是 Comsumer<B> 的子类型,类型参数 A 和 B 交换了位置,所以我们说子类型化被反转了。
in 关键字的意思是,对应类型的值是传递进来给这个类的方法的,并且被这些方法消费。和协变的情况类似,约束类型参数的使用将导致特定的子类型化关系。在类型参数 T 上的 in 关键字意味着子类型化被反转了。
一个类可以在一个类型参数上协变,同时在另一个类型参数上逆变:
interface Function1<in P, out R>{
operator fun invoke(p: P): R
}
使用点变形:在类型出现的地方制定变型
在 Java 中每一次使用带类型参数的类型的时候,还可以指定这个类型参数是否可以用它的子类型或者超类型替换。这叫做使用点变形。
Kotlin 也支持使用点变型,允许在类型参数出现的具体位置指定变型,即使在类型声明时它不能被声明称协变的或者逆变的。
fun <T:R, R> copyData(source: MutableList<T>,
destination: MutableList<R>){
for(item in source){
destination.add(item)
}
}
>>val ints = mutableListOf(1,2, 3)
>>val anyItems = mutableListOf<Any>()
>>copyData(ints, anyItems)
但是 Kotlin 提供了一种更优雅的表达方式。当函数的实现调用了那些类型参数只出现在 out 位置(或只出现在 in 位置)的方法时,可以充分利用这一点,在函数定义中给特定用途的类型参数加上变型修饰符。
fun <T> copyData(source: MutableList<out T>,
destination: MutableList<T>){
for(item in source){
destination.add(item)
}
}
可以为类型声明中类型参数任意的用法指定变型修饰符,这些用法包括:形参类型、局部变量类型、函数返回类型,等等。这里发生的一切被称作为类型投影:我们说 source 不是一个常规的 MutableList,而是一个投影(受限)的 MutableList。
星号投影:使用 * 代替类型参数
星号投影语法可以用来表名你不知道关于泛型实参的任何信息。例如,一个包含未知类型的元素的列表用这种语法表示为 List<*>。
MutableList<*> 和 MutableList<Any?> 不一样,MutableList<Any?> 这种列表包含的是任意类型的元素,而 MutableList<*> 是包含某种特定类型元素的列表。
MutableList<*> 投影成了 MutableList<out Any?> :当你没有任何元素类型信息的时候,读取 Any? 类型的元素仍然是安全的,但是向列表中写入元素是不安全的。谈到 Java 通配符,Kotlin 的 AnyType<*> 对应于 Java 的 MyType<?>.
参考文献
[1]Kotlin 实战(Kotlin in Action).北京:电子工业出版社,2017.
如果觉得还不错的话,欢迎关注我的个人公众号,我会不定期发一些干货文章~
网友评论