Kotlin进阶知识(九)——泛型类型参数

2,777 阅读5分钟

引言:和Java不同,Kotlin始终要求类型实参要么被显式地说明,要么能被编译器推导出来。例如,在Java中,可以声明List类型的变量,而不需要说明它可以包含哪类事物。而Kotlin从一开始就有泛型,所以它**不支持没有类型参数的泛型类**(即原生态类型),类型实参必须定义

一、泛型函数和属性

泛型函数:编写一个使用列表的函数,要求在任何列表(通用的列表)上使用,而不是某个具体类型的元素的列表,这个函数即为泛型函数。

泛型函数有它自己的类型形参。这些形参每次函数调用时都必须替换成具体类型实参

大部分使用集合的库函数都是泛型的。来看看图1中的slice函数。这个函数返回一个只包含在指定下标区间内的元素。

图1:slice泛型函数的类型形参为T

接受者和返回类型用到了函数的类型形参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.

这种情况下,可以说明作为类型实参的类型必须实现**CharSequenceApppendable两个接口**。这意味着该类型的值可以使用访问数据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永远都是非空类型