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编程思想》的书中有这么有一句话来描述泛型:"当你希望代码能够跨多个类工作时,使用泛型才有所帮助。"

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

相关推荐
sinat_384241091 小时前
带有悬浮窗功能的Android应用
android·windows·visualstudio·kotlin
Jason-河山1 小时前
利用Java爬虫获得店铺详情:技术解析
java·开发语言·爬虫
就是有点傻2 小时前
C#中面试的常见问题005
开发语言·面试·c#·wpf
红米饭配南瓜汤2 小时前
Android显示系统(01)- 架构分析
android·音视频·媒体
一舍予3 小时前
nuxt3项目搭建相关
开发语言·javascript·vue.js·nuxt
yi诺千金3 小时前
Android Configuration相关
android
AI人H哥会Java4 小时前
【JAVA】Java基础—面向对象编程:常用API与数据结构—集合框架(List、Set、Map等)
java·开发语言
shepherd枸杞泡茶4 小时前
C# 数据结构之【队列】C#队列
开发语言·数据结构·c#
scoone4 小时前
C++中的原子操作:原子性、内存顺序、性能优化与原子变量赋值
开发语言·c++
轩情吖4 小时前
模拟实现Bash
linux·c语言·开发语言·c++·后端·bash·环境变量