Kotlin:泛型的基本用法

我不是罗大锤 2021年11月23日 215次浏览

泛型主要有两种定义方式:一种是定义泛型类,另一种是定义泛型方法,使用的语法结构都是。当然括号内的T并不是固定要求的,事实上可以使用任意英文字母或单词都可以,但是通常情况下,T是一种约定俗成的泛型写法。

一、定义泛型类

class MyClass<T> {
    fun method(param: T): T{
        return param
    }
}
// 此时的MyClass就是一个泛型类
// MyClass中的方法允许使用T类型的参数和返回值
fun main() {
    val myClass = MyClass<Int>()
    val result = myClass.method(123)
}

二、定义泛型方法

class MyClass {
    fun <T> method(param: T): T{
        return param
    }
}

fun main() {
    val myClass = MyClass()
    val result = myClass.method<Int>(123)
}

三、设置泛型上界

泛型默认上界是Any?,如果想让泛型的类型不可为空,可以手动设置泛型上届为Any。

class MyClass {
    // 这个泛型方法只能接收和返回Number类型(Int、Float、Double等)
    fun <T : Number> method(param: T): T {
        return param
    }
}

四、利用泛型实现类似apply函数

fun <T> T.build(block: T.() -> Unit): T {
    block()
    return this
}

五、对泛型进行实化

1.泛型实化原理和简单实现

在JDK1.5之前,Java是没有泛型功能的,那个时候诸如List之类的数据结构可以存储任意类型的数据,取出数据的时候也需要手动向下转型才行,这不仅麻烦,而且很危险。比如说我们在同一个List中存储了字符串和整形这两种类型,但是在取出数据的时候却无法区分具体的数据类型,如果手动将它们强制转换成同一种类型的话,那么就会抛出类型转换异常。

于是在JDK1.5中,Java终于引入了泛型功能。这不仅让诸如List之类的数据结构变得简单好用,也让我们的代码变得更加安全。

但是实际上,Java的泛型功能是通过类型擦除机制来实现的。什么意思呢?就是说泛型对于类型的约束只在编译时期存在,运行的时候仍然会按照JDK1.5之前的机制来运行,JVM是识别不出来我们在代码中指定的泛型类型的。例如我们创建了一个List集合,虽然在编译时期只能向集合中添加字符串类型的元素,但是在运行时期JVM并不能知道它本来只打算包含哪种类型的元素,只能识别出来它是个List。

所有JVM的语言,它们的泛型功能都是通过类型擦除机制来实现的,包括Kotlin,这种机制使得我们可不能使用a is T或者T::class.java这样的语法,因为T的实际类型在运行的时候已经被擦除了。

然而不同的是,Kotlin提供了内联函数的概念,内联函数中的代码会在编译的时候自动被替换到调用它的地方,这样的话也就不存在什么泛型擦除的问题了,因为代码在编译之后会直接使用实际的类型来替换内联函数中的泛型声明。工作原理如下图:
Kotlin泛型的基本用法_1.jpg

可以看到,bar()是一个带有泛型类型的内联函数,foo()函数调用了bar()函数,在代码编译之后,bar()函数中的代码将可以获得泛型的实际类型。

泛型实化例子代码如下:

// 函数必须是内联函数
// 在声明泛型的地方必须加上reified关键字来表示该泛型要实化
inline fun <reified T> getGenericType() {
    
}

我们准备实现一个获取泛型实际类型的功能,代码如下:

// 只有一行代码,实现了在Java中不可能实现的功能
inline fun <reified T> getGenericType() = T::class.java

现在我们可以使用如下代码对getGenericType()函数进行测试:

fun main() {
    val result1 = getGenericType<String>()
    val result2 = getGenericType<Int>()
    println("result1 is $result1") // result1 is String
    println("result2 is $result2") // result2 is Int
}

2.泛型实化的应用

泛型实化功能允许我们在泛型函数中获得泛型的实际类型,这也使得类似a is T、T::class.java这样的语法成为可能,从而灵魂运用这一特性将可以实现一些不可思议的语法结构。

2.1.简化startActivity功能

inline fun <reified T> startActivity(context: Context) {
    val intent = Intent(context, T::class.java)
    context.startActivity(intent)
}

// 使用泛型实化的函数来启动Activity
startActivity<TestActivity>(context)

2.2.简化startActivity功能——带参数

inline fun <reified T> startActivity(context: Context, 
        block: Intent.() -> Unit) {
    val intent = Intent(context, T::class.java)
    intent.block()
    context.startActivity(intet)
}

// 使用泛型实化的函数来带参启动Activity
startActivity<TestActivity>(context) {
    putExtra("param1", "data")
    putExtra("param2", 123)
}

六、泛型的协变和逆变

泛型的协变和逆变功能不太常用,但是Kotlin的内置API中使用了很多协变和逆变的特性,因此想要对这个语言有更加深刻的了解,这部分内容还是有必要学习一下。

在开始学习协变和逆变之前,我们要先了解一个约定,一个泛型类或者接口中的方法,它的参数列表是接收数据的地方,因此称它为in位置,返回值是输出数据的地方,称为out位置,如图:
Kotlin泛型的基本用法_2.png

有了这个约定前提,首先定义如下3个类:

open class Person(val name: String, val age: Int)
class Student(name: String, age: Int) : Person(name, age)
class Teacher(name: String, age: Int) : Person(name, age)

1.泛型的协变

先提一个问题,如果某个方法接收一个Person类型参数,而我们传入Student实例,很显然这是合法的,因为Student是Person的子类。

但如果某个方法接收一个List类型参数,而我们传入List实例。这种做法在Java中是不允许的,因为List不能成为List的子类,否则将可能存在类型转换的完全隐患。至于为什么会出现类型转换的安全隐患,下面举一个具体的例子:

// 泛型类,调用set()和get()方法可以给data字段赋值或读取
class SimpleData<T> {
    private var data: T? = null
    
    fun set(t: T?) {
        data = t
    }
    
    fun get(): T? {
        return data
    }
}

fun main() {
    val student = Student("Tom", 19)
    val data = SimpleData<Student>()
    data.set(student)
    // 假设向接收SimpleData<Person>的方法传入SimpleData<Student>合法
    handleSimpleData(data) // 代码会报错,这里假设可以通过编译
    // 这里实际会获取到包含Teacher的实例,类型转换异常
    val studentData = data.get()
}

fun handleSimpleData(data: SimpleData<Person>) {
    val teacher = Teacher("Jack", 35)
    // Teacher也是Person子类,给data设置Teache
    data.set(teacher)
}

所以为了杜绝val studentData = data.get()处可能出现类型转换异常的隐患,Java是不允许使用这种方式来传递参数的,换句话说,即使Student是Person的子类,但是SimpleData并不是SimpleData的子类。
不过可以发现刚才的主要原因是因为在handleSimpleData()方法中向SimpleData设置了一个Teacher实例,如果SimpleData在泛型T上是只读的话,就没有类型转换的安全隐患了,那么这个时候SimpleData可不可以成为SimpleData的子类呢?
这里就可以引出泛型协变的定义了,假如定义了一个MyClass的泛型类,其中A是B的子类型,同时MyClass又是MyClass的子类型,那么我们就可以称MyClass在T这个泛型上是协变的。

修改SimpleData类代码,使T只能出现在out位上,不能出现在in位置上:

// 在泛型T声明前加上out关键字
// 在其泛型类型的数据上只读
// 所有方法都不能接收T类型参数
// 此时不会再有类型转换的安全隐患,SimpleData在泛型T上是协变的
class SimpleData<out T>(val data: T?) {
    fun get(): T? {
        return data
    }
}

fun main() {
    val student = Student("Tom", 19)
    val data = SimpleData<Student>(student)
    handleMyData(data)
    // 此时没有任何安全隐患,编译通过
    // 同时现在SimpleData<Student>是SimpleData<Person>子类
    val studentData = data.get()
}

fun handleMyData(data: SimpleData<Person>) {
    val personData = data.get()
}

2.@UnsafeVariance注解忽视安全隐患

下面看一下Kotlin中List简化版的代码:

public interface List<out E> : Collection<E> {
    override val size: Int
    override fun isEmpty(): Boolean
    // 原则上协变后泛型E只能出现在out位置上
    // 不过contains()目的很明确,它并不会修改当前集合的内容
    // 因此这种操作实质上又是安全的,为了让编译器理解这里是安全的
    // 可以在泛型前加上@UnsaveVariance注解
    override fun contains(element: @UnsaveVariance E): Boolean
    override fun iterator(): Iterator<E>
    public operator fun get(index: Int): E
}

切记**@UnsaveVariance不能滥用**,否则导致运行时出现了类型转换异常,Kotlin对此是不负责的。

3.泛型的逆变

从定义上来看,逆变与协变完全相反,假如定义了一个MyClass的泛型类,其中A是B的子类型,同时MyClass又是MyClass的子类型,那么我们就可以称MyClass在T这个泛型上是逆变的。协变和逆变的区别如图所示:

Kotlin泛型的基本用法_3.png

从直观的角度上来说,逆变的规则好像挺奇怪的,原本A是B的子类型,怎么MyClass能反过来成为MyClass的子类型?下面通过一个具体的例子来学习一下:

// 定义一个Transformer接口,用于执行一些转换操作
// transform()方法接收T类型并返回String类型
interface Transformer<T> {
    fun transform(t: T): String
}

// 实现Transformer接口
fun main() {
    val trans = object : Transformer<Person> {
        override fun transform(t: Person): String {
            return "${t.name} ${t.age}"
        }
    }
    // Transformer<Person>并不是Transformer<Student>子类
    handleTransformer(trans) // 这行代码会报错
}

fun handleTransformer(trans: Transformer<Student>) {
    val student = Student("Tom", 19)
    val result = trans.transform(student)
}

这段代码从安全的角度来分析是没有任何问题的,因为Student是Person的子类,使用Transformer的匿名类实现将Student对象转换成一个字符串也是绝对安全的,并不存在类型转换的安全隐患。但是实际上,在调用handleTransformer()方法时却会提示语法错误,**原因也很简单,Transformer并不是Transformer的子类型。**这个时候逆变就可以排上用场了,它就是专门用于处理这种情况的。修改Transformer接口中的代码:

// 在T声明前加上in关键字,意味着T只能出现在in位置上
// 同时也意味着Transformer在泛型T上是逆变的
interface Transformer<in T> {
    fun transform(t: T): String
}

只要做了这样一点的修改,刚才的代码就可以编译并且正常运行了,因为此时Transformer已经成为了Transformer的子类型。

逆变的用法大概就是这样了,可是想一想为什么逆变的时候泛型T不能出现在out位置上?为了解释这个问题,我们先假设逆变是允许让泛型T出现在out位置上:

interface Transformer<in T> {
    // 让transform()方法接收name和age两个参数
    // 由于逆变不允许T出现在out上,所以现在要加上注解
    fun transform(name: String, age: Int): @UnsafeVariance T
}

fun main() {
    val trans = object : Transformer<Person> {
        override fun transform(name: String, age: Int): Person {
            // 期望返回Person,实际返回Teacher
            // 类型转换异常
            return Teacher(name, age)
        }
    }
    handleTransformer(trans)
}

fun handleTransformer(trans: Transformer<String>) {
    val result = trans.transform("Tom", 19)
}

上述代码就是一个典型的违反逆变规则而造成类型转换异常的例子,在Transformer的匿名类实现中,我们使用transform()方法中传入的name和age构建了一个Teacher对象,并把这个对象直接返回。由于transform()方法的返回值要求是一个Person对象,而Teacher是Person子类,因此这种写法肯定是合法的。

但是在handleTransformer()方法中,调用了Transformer的transform()方法,并传入了name和age两个参数,期望得到Student对象的返回,可实际上返回的确实Teacher对象,因此这里必然会造成类型转换异常。