Kotlin 泛型:基本使用

2023-02-24 22:58:45 浏览数 (1)

泛型在 Kotin 的日常使用中运用很广泛:

  1. 当我们使用 List、Array 等类型时,我们会使用到泛型类;
  2. 当我们使用 apply、let 等函数时,我们会使用到泛型函数。

在 Kotlin 中声明和使用泛型类、泛型函数的基本概念和 Java 相似,有 Java 泛型概念的情况下,不用详细解释或者做进一步了解,也能够很容易地上手使用泛型。

但使用泛型仅仅是第一步,要想将泛型应用好,仍然需要做进一步深入的学习。

本篇是 Kotlin 泛型的基础介绍,进阶内容可点击链接查看。

  • Kotlin 泛型:基本使用
  • Kotlin 泛型:类型参数约束

系列持续更新中,欢迎关注订阅。

为什么需要泛型

假如我们想实现自定义的列表类型,用于存放数值、字符串或其他具体的类型。我们可以这么写:

代码语言:text复制
// 数值列表
interface NumberList {
    fun set(index: Int, obj: Number?)
    fun get(index: Int): Number?
}

// 字符串列表
interface StringList {
    fun set(index: Int, obj: String?)
    fun get(index: Int): String?
}

// Car 列表
class Car 
interface CarList {
    fun set(index: Int, obj: Car?)
    fun get(index: Int): Car?
}

如果没有泛型,我们只能针对每种具体的类型,分别定义对应的列表,这种方式只能针对有限的具体类型进行实现、不同具体类型的列表实际上具有相似的实现,这些代码只能在不同列表间拷贝重复,无法复用,难以维护。

有的同学会用这样的方法来解决上面的问题:

代码语言:txt复制
interface AnyList {
    fun set(index: Int, obj: Any?)
    fun get(index: Int): Any?
}

这个方法虽然能解决上述问题,但它带来了其他的问题。

首先,列表中存放的数据类型信息消失了,从函数签名上,我们只知道能得到一个实例,但这个实例具体是什么类型就无从得知,作为列表的使用者,面对一个未知的接口,开发体验别提有多糟糕了。

其次,Kotlin 是静态类型语言,静态类型语言的优势是能够在编译时帮我们提前进行类型检查,保证类型的正确性,避免潜在的类型错误。而上面这个例子,由于任何类型都是 Any 类型的子类,在进行类型检查时,Kotlin 无法帮我们检查出不合理的调用,我们完全可以往一个 String 列表里放入一个 Number 实例,从而让使用者从一个 Car 列表中得到猫猫狗狗,这都是完全有可能的。这种看似灵活的万能列表,实际上是随时会爆炸的炸弹,严重降低了工程质量(请注意倒车,请注意倒车。

什么是泛型

泛型提供了一种方法,允许我们定义带「类型参数」的泛型类/泛型函数,在创建泛型类的实例、调用泛型函数时,「类型参数」将替换成具体的「类型实参」。

上面的例子用泛型定义将会很方便简洁,同时,类型信息得到了保留,编译器也能正常进行类型检查:

代码语言:text复制
interface List<T> {
    fun set(index: Int, obj: T?)
    fun get(index: Int): T?
}

val stringList: List<String> = // ... 省略
stringList.set(0, "a string") // OK
stringList.get(0)?.charAt(0) // OK
stringList.set(0, 1) // 编译出错,类型不匹配
stringList.get(0) - 1 // 编译出错,类型不匹配

class Car
val carList: List<Car> = // ... 省略
carList.set(0, Car()) // OK
carList.get(0) is Car? // Always true
carList.set(0, 1) // 编译出错,类型不匹配
carList.get(0) is Int? // 编译出错,类型不匹配

泛型机制允许我们在编码的时候,使用占位符作为类型(即「类型参数」代替实际使用时的类型(即「类型实参」)。

如何区别上述两个概念?

当我们在「定义」泛型类、泛型函数时,我们使用的是「类型参数」;

当我们在「使用」泛型类、泛型函数时,我们使用的是「类型实参」。

「类型参数」是占位符,就像变量一样,可以任意取名,一般使用单个大写字母(T、U、V)、全大写单词(DATA、TOKEN)、或首字母大写的单词(Data、Token);

「类型实参」是具体的类型,只能传入已存在的具体类型,如 IntStringAny 或者其他自定义的具体类型。

定义泛型类、泛型函数的方式如下:

代码语言:text复制
// --- 泛型函数 ---
fun <P> run(param: P) // 仅用于函数参数,定义在泛型类、泛型接口中
fun <R> run(): R // 仅用于函数返回值,定义在泛型类、泛型接口中
fun <P, R> invoke1(param: P): R // 用于函数参数和返回值,定义在泛型类、泛型接口中
fun <T> filter(predicate: (T) -> Boolean) // 用于高阶函数

// --- 泛型类 ---
class Box<T> { // 泛型类
    private var instance: T? // 用于属性
    // 类中的泛型函数
    fun get(): T? { // 用于方法,下同
        return instance
    }
    fun set(instance: T?) {
        this.instance = instance
    }
}
interface List<T> { // 泛型接口
    fun set(index: Int, obj: T) // 用于方法,下同
    fun get(index: Int): T?
}

使用泛型类、泛型函数:

代码语言:txt复制
// 使用泛型函数
filter<String> { it: String -> false }

// 使用泛型类
val stringBox = Box<String>()

// 使用泛型接口
class Car
class CarList : List<Car> {
    // 实现接口中的泛型函数
    override fun set(index: Int, obj: Car) {
        // todo
    }
    override fun get(intdex: Int): Car? {
        // todo
    }
}
val carList = CarList()
carList.set(0, Car())
carList.get(0) is Car? // Always true

了解到这里,就掌握了基本的泛型使用方式:

  1. 用「类型参数」作为占位符,定义泛型类、泛型函数
  2. 使用泛型类、泛型函数时,需要传递具体类型作为「类型实参」。

下一篇文章,将介绍 Kotlin 泛型的进阶知识:类型参数约束

0 人点赞