引言:和Java不同,
Kotlin
始终要求类型实参要么被显式地说明,要么能被编译器推导出来。例如,在Java中,可以声明List类型的变量,而不需要说明它可以包含哪类事物。而Kotlin
从一开始就有泛型,所以它**不支持
没有类型参数的泛型类**(即原生态类型),类型实参必须定义
。
一、泛型函数和属性
泛型函数:编写一个使用列表的函数,要求在任何列表(通用的列表)上使用,而不是某个具体类型的元素的列表,这个函数即为泛型函数。
泛型函数有它自己的类型形参。这些形参在每次函数调用时都必须替换成具体的类型实参。
大部分使用集合的库函数都是泛型的。来看看图1中的slice函数。这个函数返回一个只包含在指定下标区间内的元素。
接受者和返回类型用到了函数的类型形参T
,它们的类型都是List<T>
。在一个具体的列表上调用这个函数时,可以显式地指定类型实参。但大部分情况下无需声明,因为编译器会推导出类型。
- 调用泛型函数
fun main(args: Array<String>) {
genericFunctionTest()
}
fun genericFunctionTest() {
val letters = ('a' .. 'z').toList()
// 显式地指定类型实参
println(letters.slice<Char>(0 .. 2))
// 编译器推导出这里的T是Char
println(letters.slice(10 .. 14))
}
// 输出结果
[a, b, c]
[k, l, m, n]
- 声明泛型的扩展属性
// 这个泛型扩展属性能在任何种类元素的列表上调用
val <T> List<T>.penultimate: T
get() = this[size - 2]
// 在这次调用汇总,类型参数T被推导成Int
>>> println(listOf(1, 2, 3, 4).penultimate)
3
不能声明泛型非扩展属性 普通(即非扩展)属性不能拥有类型参数,不能在一个类的属性中存储多个不同类型的值。
二、声明泛型类
和Java一样,Kotlin
通过在类名称后加上一对尖括号,并把类型参数放在尖括号内来声明泛型类及泛型接口。一旦声明之后,就可以在类的主体内像其他类型一样使用类型参数。
// List接口定义了类型参数T
interface List<T> {
// 在接口或类的内部,T可以当作普通类型使用
operator fun get(index: Int): T
// ...
}
如果自定义的类继承了泛型类(或者实现了泛型接口),就得为基础类型的泛型形参提供一个类型实参。它可以是具体类型或者另一个类型形参:
// 这个类实现了List,提供了具体类型实参:String
class StringList: List<String> {
// 注意T如何被String代替
override fun get(index: Int): String = ... }
// 现在ArrayList的泛型类型形参T就是List的类型实参
class ArrayList: List<T> {
override fun get(index: Int): T = ... }
StringList
类被声明成只能包含String
元素,所以它使用String
作为基本类型的类型实参。
ArrayList
类定义了它自己的类型参数T并把它指定为父类的类型实参。
三、类型参数约束
类型参数约束可以限制作为(泛型)类和(泛型)函数的类型实参的类型。
以计算列表元素之和的函数为例。它可以用在List和List上,但不可以用在List这样的列表上。可以定义一个类型参数约束,说明sum
的类型形参必须是数字,来表达这个限制。
上界约束:在泛型类型具体的初始化中,其对应的类型实参就必须是这个具体类型或者它的子类型。
定义:把冒号放在类型参数名称 之后
,作为类型形参上界的类型紧随**其后
**,如下:
// Java
<T extends Number> T sum(List<T> list)
// Kotlin
fun <T: Number> List<T>.sum(): T
一旦指定了类型形参T的上界,就可以把类型T的值当作它的上界(类型)的值使用。例如:可以调用定义在上界类的方法:
// 指定Number为类型形参的上界
fun <T: Number> oneHalf(value: T): Double {
// 调用Number类中的方法
return value.toDouble() / 2
}
fun oneHalfTest() {
println(oneHalf(100))
}
- 为一个类型参数指定多个约束
fun <T> ensureTrailingPeriod(seq: T)
// 类型参数约束的列表
where T: CharSequence, T: Appendable {
// 调用为CharSequence接口定义的扩展函数
if(!seq.endsWith('.')) {
// 调用Appendable接口的方法
seq.append('.')
}
}
fun ensureTrailingPeriodTest() {
val helloWorld = StringBuilder("Hello World")
ensureTrailingPeriod(helloWorld)
println(helloWorld)
}
// 输出结果
Hello World.
这种情况下,可以说明作为类型实参的类型必须实现**CharSequence
和Apppendable
两个接口**。这意味着该类型的值可以使用访问数据(endsWith
)和修改数据(append
)两个操作。
四、让类型形参非空
若声明的是泛型类或者泛型函数,任何类型实参,包括那些可空的类型实参,都可以替换他的形参类型。
事实上,没有指定上界的类型形参将会使用**Any?
这个默认的上界**。
class Processor<T> {
fun process(value: T) {
value?.hashCode()
}
}
process 函数中,参数value
是可空的,尽管T
并没有使用问号标记。
若想保证替换类型形参始终是非空类型,可以通过指定一个约束来实现。若除了可空性之外没有任何限制,可以使用**Any
代替默认的Any?
作为上界**:
// 指定非“空”上界
class ProcessorNew<T: Any> {
fun process(value: T) {
// 类型T的值现在是非“空”的
value.hashCode()
}
}
约束<T: Any>
确保了类型T
永远都是非空类型!