Kotlin语法基础篇十一:深入浅出泛型

前言

在上一篇文章中我们详细的介绍了Kotlin中的数据类、密封类、枚举类。本篇文章我们将讲解Kotlin中比较重要的一个知识点泛型,在我们所熟知的一些编程语言像Java、C、Swift等都引入了泛型。它们在用法上都是大同小异。下面我们就来详细的介绍泛型在Kotlin中的使用。

1.泛型基础

在Java编程思想中有这么一句话:"当你希望代码能够跨多个类工作时,使用泛型才有所帮助。"我觉得这句话很形象的描述了泛型的使用场景,其实在前面的一些文章内容中我们也时常涉及到泛型的使用,只是没有详细的去介绍它。

在一般的编程模式下,我们需要给任何一个变量指明一个具体的类型。而泛型允许我们在不指定具体类型的情况下进行编程。对于泛型的声明我们使用<T>语法,声明一个泛型方法,需要将<T>放在方法名之前。声明一个泛型接口或者是类,需要将<T>放在对应的接口名或类名后。然而大写字母T是一种常规写法,我们也可以使用别的大写字母来代替。如果你想让声明的泛型有实际意义,也可以采用多个大写字母组合的方式,如<VM>、<VP>。

2.泛型函数

定义一个泛型函数其实并不复杂,我们只需要将类型参数放在函数名之前即可。如下代码示例:

Kotlin 复制代码
private fun <T> getData(data: T) : T {
    return data
}

我们定义了一个泛型函数getData(),当我们在调用泛型函数getData()的时候,需要将参数类型紧跟在函数名后,并用<>括起来:

Kotlin 复制代码
fun main() {
    val result = getData<String>("study work hard")
    println("result = $result")
}

// 输出
result = study work hard

Kotlin编译器强大的类型推导机制一样适用于泛型,当我们可以从参数中推断出其类型,在调用处我们也可以省略类型参数:

Kotlin 复制代码
fun main() {
    val result = getData("study work hard")
}

3.泛型约束

泛型约束在Java中也有明确的定义,我们可以给声明的泛型指定一个边界。<T extends BaseFragment>类似这样的Java代码我们应该都很熟悉。而在Kotlin中我们依然使用冒号来指定,如下代码示例,我们定义一个带有边界的泛型函数getInfo():

Kotlin 复制代码
open class Person

class MiddlePerson : Person()

class YoungPerson : Person()

private fun <T: Person> getInfo(person: T) : T {
    return person
}

如果没有声明,默认的上界是 Any?。在尖括号中只能指定一个上界,如果同一类型参数需要多个上界,我们需要一个单独的 where-子句,多个上界使用逗号隔开。如下代码示例:

Kotlin 复制代码
private fun <T> getInfo(person: T) : T  where T : Person, T : Serializable {
    return person
}

4.泛型函数的运用

了解了泛型函数的使用,我们再来看标准库Standard.kt文件中的函数就比较简单了。在函数的文章中,我们介绍了扩展函数,使用ClassName.funName()的语法结构。而泛化的类型同样支持扩展函数,T.funName()就代表我们给一个泛化的类型添加了扩展函数,其好处就是我们可以使用任意的对象去访问该扩展函数。如下代码示例,我们给泛化的类型添加扩展函数call():

Kotlin 复制代码
fun <T> T.call(block: () -> Unit) : T {
    block()
    return this
}

高阶函数和Lambda表达式的文章中,我们介绍了带有上下文作用域的Lambda表达式。当我们给声明的函数类型加上接收者时,在给该函数类型初始化的Lambda表达式中就会拥有该接收者的上下文作用域。而给一个函数类型添加接收者,我们只需要在函数类型声明的时候在( )前加上ClassName. 即可。

Kotlin 复制代码
block: ClassName.() -> Unit

同样的我们也可以给一个函数类型添加一个泛化的接收者,在该函数类型初始化的Lambda表达式中就会拥有其实际调用类型的上下文作用域。

Kotlin 复制代码
block: T.() -> Unit

掌握了上面两个知识点我们再来分析一下apply()函数:

  • 1.我们可以使用任意的对象去访问它
  • 2.在apply()函数调用的Lambda表达式中拥有调用者的上下文作用域
  • 3.apply()函数返回其调用者本身

由上面分析的apply函数,我们不难得出以下结论:

  • 1.apply()函数是一个泛化类型的扩展函数
  • 2.apply()函数中拥有一个函数类型的参数,并且该函数类型拥有一个接收者,该接收者就是泛化类型
  • 3.apply()函数的返回值是其泛化类型本身

这样我们就可以很容易的写出apply()函数的源码:

Kotlin 复制代码
inline fun <T> T.apply(block: T.() -> Unit) : T {
    block()
    return this
}

5.泛型类

想要声明一个泛型类其实很简单,而难的是我们如何在实际开发中灵活的去运用它。下面我们就先来看一个在实际开发过程中比较常用的泛型类,网络请求返回的数据Bean:

Kotlin 复制代码
class DataBean<T> {
    val code = 0

    val message = "success"

    private var data: T? = null

    fun setData(data: T) { this.data = data }

    fun getData() : T? { return data }
}

我们在类上声明的类型参数,可以作为类成员变量来使用,也可以将其作为方法的参数或者返回值。在调用的地方,我们将大写字母T替换成具体的类型即可,如下代码示例:

Kotlin 复制代码
fun main() {
    val appVersionInfo = AppVersionInfo("00", "1.0")
    val data = DataBean<AppVersionInfo>()
    data.setData(appVersionInfo)
}

data class AppVersionInfo(val code: String, val name: String)

6.泛型接口

接口也可以声明类型参数。下面我们就来看一个和实际生活比较贴近的例子:比如某车企品牌一共推出了三款车型,BUS、SUV、CAR,该车企想要生产一辆SUV的车,我们就可以将具体的车型告诉车厂的相关人员,然后他们就会去生产相关的车。如下代码示例:

Kotlin 复制代码
interface Factory<T> {
    fun create(carType: Int) : T
}

sealed class CarType

object SUV : CarType()
object BUS : CarType()
object Car : CarType()

我们定义了一个Fractory<T>的泛型接口,并在该接口内部定义了一个抽象方法create(),该方法接收具体的车型然后返回。

Kotlin 复制代码
fun main() {
    val factory = object : Factory<CarType> {
        override fun create(carType: Int): CarType {
            return when(carType) {
                0 -> SUV
                1 -> BUS
                2 -> Car
                else -> Car
            }
        }
    }
    val suv = factory.create(0)
    println("suv = ${suv.javaClass.simpleName}")
}

// 输出
suv = SUV

为了方便阅读,我们直接在main()函数中使用匿名对象的方式创建了一个factory。然后我们根据需要让工厂生产了一辆SUV。当然这个简单的案例,也是工厂模式的一种应用。在根据传入的车型生产对应的汽车时,我们做了一次向上转型。我们知道在类型转换的时候,向上转型是安全的,向下转型是不安全的。

7.类型擦除机制

我们知道所有基于JVM的语言,它们的泛型功能都是通过类型擦除来实现的。当然也包括我们所熟悉的Java和Kotlin语言。那么我们怎么来理解泛型擦除呢?我们在编译期间指定的泛型约束,在运行时是会被擦除的。比如我们在编译期间声明两个存储不同数据类型的集合List<String>和List<Int>,但是在运行期间,JVM都会把他们识别成一个List,JVM并不知道你在List中存放什么类型的数据。

8.reified的关键字

了解了泛型的擦除机制,我们再来看下Kotlin中一个比较实用的语法糖。具体化的参数类型,什么是具体化的参数类型呢?这可能不太好理解。在我们所熟知的Java泛型中好像并没有这个概念。但由于Kotlin中引入了内联函数,这让泛型可以实现具体化的类型参数成为了可能。让开发者可以使用T::class.java或者param as T这样的语法。那么我们怎样才能定义一个具体的化的类型参数呢?Kotlin为我们提供了reified的关键字。要想声明一个具体化的类型参数需要满足以下两个条件:

  • 1.具体化的类型参数只能在内联函数中声明
  • 2.在声明泛型的地方必须加上`reified`关键字来修饰

下面我们就来看一个简单的例子,比如我们想要获取某个类的类名,我们就可以这么写:

Kotlin 复制代码
fun main() {
    val className = getSimpleName<String>()
    println("className = $className")
}

inline fun <reified T> getSimpleName() : String {
    return T::class.java.simpleName
}

// 输出
className = String

上述函数中的泛型`T`就是一个具体化的参数类型,为什么可以这么说呢?这是因为Kotlin编译器在背后帮我们默默的做了很多事情。还是一样的步骤,在Android Studio中依次打开Tools -> Kotlin -> ShowKotlin Bytecode在右边的弹出窗中我们点击Decompile按钮:

还记得我们在介绍inline关键字的时候说到内联函数的一个特性吗?内联函数中的代码会在编译的时候自动被替换到调用它的地方。由上图中反编译成Java的代码我们可以看到3 处的代码被替换到了2 处,而且我们的泛型也被替换成了具体的类型String。

到这里我想你应该明白了是什么具体化的类型参数,实际上是Kolitn编译器在编译期间将我们的类型参数的使用替换成了具体的类型来使用。这样在运行期间也就不存在什么类型擦除了,我们就是使用的一个普通的类型。

总结

对于一个刚刚接触编程的开发者来说泛型可能并不是那么好理解,但是熟练的掌握了泛型,对于我们写一些架构上的代码还是很有用处的。就像我们在文章开始的地方介绍到,在《Java编程思想》的书中有这么有一句话来描述泛型:"当你希望代码能够跨多个类工作时,使用泛型才有所帮助。"

到这里,本篇文章关于泛型的介绍就结束了。下篇文章我们将讲解泛型中的协变、逆变、类型投影、星投影的相关知识,我们下期再见~

相关推荐
m0_748248774 分钟前
Go gin框架(详细版)
开发语言·golang·gin
湛海不过深蓝6 分钟前
【js】记录预览pdf文件
开发语言·javascript·pdf
还没想好12330 分钟前
R语言,剪切板双向转,临时处理数据
开发语言·r语言
一只自律的鸡1 小时前
C进阶 程序的环境和预处理
c语言·开发语言
码上好玩1 小时前
vscode python pip : 无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。()
开发语言·python·pip
思忖小下2 小时前
梳理你的思路(从OOP到架构设计)_介绍Android的Java层应用框架03
android·框架
engchina2 小时前
Python中的函数式编程模块:itertools、functools和operator
开发语言·python
Ocean☾2 小时前
C语言-详细介绍-*符号
c语言·开发语言
玉红7772 小时前
Go语言的数据结构
开发语言·后端·golang
咸甜适中2 小时前
go语言并发文件备份,自动比对自动重命名(逐行注释)
开发语言·后端·golang