Kotlin-扩展函数与扩展属性

Kotlin 的扩展(Extension),主要分为两种语法:第一个是扩展函数,第二个是扩展属性。从语法上看,扩展看起来就像是我们从类的外部为它扩展了新的成员。

这在实际编程当中是非常有用的功能。我们可以来想象一个场景:我们想修改 JDK 当中的 String,想在它的基础上增加一个方法 lastElement() 来获取末尾元素,如果使用 Java,我们是无法通过常规手段实现的,因为我们没办法修改 JDK 的源代码。任何第三方提供的 SDK,我们都无权修改。

不过,借助 Kotlin 的扩展函数,我们就完全可以在语义层面,来为第三方 SDK 的类扩展新的成员方法和成员属性。不管是为 JDKString 增加新的成员方法,还是为 Android SDKView 增加新成员属性,我们都可以实现。


扩展函数

扩展函数,就是从类的外部扩展出来的一个函数,这个函数看起来就像是类的成员函数一样。这里,我们就以 JDK 当中的 String 为例,来看看如何通过 Kotlin 的扩展特性,为它新增一个 lastElement() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Ext.kt

/*
① ② ③ ④
↓ ↓ ↓ ↓ */
fun String.lastElement(): Char? {
// ⑤
// ↓
if (this.isEmpty()) {
return null
}

return this[length - 1]
}

// 使用扩展函数
fun main() {
val msg = "Hello Wolrd"

// lastElement就像String的成员方法一样可以直接调用
val last = msg.lastElement() // last = d
}

我们先是定义了一个 String 的扩展函数 lastElement() ,然后在 main 函数当中调用了这个函数。并且,这个扩展函数是直接定义在 Kotlin 文件里的,而不是定义在某个类当中的。这种扩展函数,我们称之为“顶层扩展”,这么叫它是因为它并没有嵌套在任何的类当中,它自身就在最外层。

现在,我们依次来看看上面的五处注释:

  1. 注释①,fun 关键字

    代表我们要定义一个函数。也就是说,不管是定义普通 Kotlin 函数,还是定义扩展函数,我们都需要 fun 关键字。

  2. 注释②,“String.

    代表我们的扩展函数是为 String 这个类定义的。在 Kotlin 当中,它有一个名字,叫做接收者(Receiver),也就是扩展函数的接收方。

  3. 注释③,lastElement()

    是我们定义的扩展函数的名称。

  4. 注释④,“Char?

    代表扩展函数的返回值是可能为空的 Char 类型。

  5. 注释⑤,“this.

    代表“具体的 String 对象”,当我们调用 msg.lastElement() 的时候,this 就代表了 msg

需要注意的是,在整个扩展函数的方法体当中,this 都是可以省略的。这一点,KotlinJava 是一样的,this 代表当前作用域,它可写可不写。即例如以下的代码:

1
2
3
4
5
6
fun String.lastElement() : Char? = if (isEmpty()) null else get(length - 1)

fun main() {
val str = "Hello World!"
println(str.lastElement())
}

实际上,Kotlin 的扩展是允许我们为“可为空的类型”进行扩展的。比如说:

1
2
3
4
5
6
7
// 不为空的接收者类型
// ↓
fun String.lastElement(): Char? {}

// 可为空的接收者类型
// ↓
fun String?.lastElement(): Char? {}

接受者可空的扩展在遇到空对象调用时不会抛异常,并且允许可空对象调用。


扩展属性

在学习了 Kotlin 的扩展函数以后,扩展属性就很好理解了。

扩展函数,是在类的外部为它定义一个新的成员方法;而扩展属性,则是在类的外部为它定义一个新的成员属性

那么,在研究了扩展的实现原理后,我们知道,我们从外部定义的成员方法和属性,都只是语法层面的,并没有实际修改那个类的源代码。

还是以 lastElement 为例,在之前的案例当中,我们是通过扩展函数来实现的,这次我们以扩展属性的方式来实现。扩展函数的定义对比普通函数,其实就只是多了一个“接收者类型”。类似的,扩展属性,也就是在普通属性定义的时候多加一个“接收者类型”即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 接收者类型
// ↓
val String.lastElement: Char?
get() = if (isEmpty()) {
null
} else {
get(length - 1)
}

/* 使用 this 关键字也可以写成:
val String.lastElement : Char?
get() = if (isEmpty()) null else this[length - 1]
*/

fun main() {
val msg = "Hello Wolrd"

// lastElement就像String的成员属性一样可以直接调用
val last = msg.lastElement // last = d
}

在这段的代码中,我们为 String 类型扩展了一个新的成员属性“lastElement”。然后在 main 函数当中,我们直接通过“msg.lastElement”方式使用了这个扩展属性,就好像它是一个成员一样。


扩展与其反编译代码

为了看得更加清晰,我们用一张图来描述它们之间的关系。

上面的两个箭头,说明了扩展函数与扩展属性,它们最终会被 Kotlin 编译器转换成静态方法;下面两个箭头,说明了扩展函数和扩展属性的调用代码,最终会被 Kotlin 编译器转换成静态方法的调用。

所以也就是说,Kotlin 的扩展表面上看起来是为一个类扩展了新的成员,但是本质上,它还是静态方法。而且,不管是扩展函数还是扩展属性,它本质上都会变成一个静态的方法。

那么,到底什么时候该用扩展函数,什么时候该用扩展属性呢?

其实,我们只需要看扩展在语义上更适合作为函数还是属性就够了。比如这里的 lastElement,它更适合作为一个扩展属性。这样设计的话,在语义上,lastElement 就像是 String 类当中的属性一样,它代表了字符串里的最后一个字符。


扩展的优势与局限性

扩展的优势

当我们想要从外部为一个类扩展一些方法和属性的时候,我们就可以通过扩展来实现了。Kotlin 当中,几乎所有的类都可以被扩展,包括普通类、单例类、密封类、枚举类、伴生对象,甚至还包括第三方提供的 Java 类。

唯有匿名内部类,由于它本身不存在名称,我们无法指定“接收者类型”,所以不能被扩展,当然了,它也没必要被扩展。

可以说,Kotlin 扩展的应用范围还是非常广的。它最主要的用途,就是用来取代 Java 当中的各种工具类,比如 StringUtilsDateUtils 等等。

所有 Java 工具类能做的事情,Kotlin 扩展函数都可以做,并且可以做得更好。扩展函数的优势在于,开发工具可以在编写代码的时候智能提示。

扩展的局限性

Kotlin 的扩展,由于它本质上并没有修改接收类型的源代码,所以它的行为是无法与“类成员”完全一致的。那么它对比普通的类成员,就会有以下几个限制。

第一个限制,Kotlin 扩展不是真正的类成员,因此它无法被它的子类重写。

举个例子,我们定义一个这样的 Person 类,并且分别为它扩展了一个 isAdult 属性和 walk() 方法:

1
2
3
4
5
6
7
8
9
10
11
open class Person {
var name: String = ""
var age: Int = 0
}

val Person.isAdult: Boolean
get() = age >= 18

fun Person.walk() {
println("walk")
}

由于 Person 类有 open 关键字修饰,所以我们可以继承这个 Person 类。

不过,当我们尝试去重写它的成员时,会发现 isAdultwalk() 是无法被重写的,因为它们压根就不属于 Person 这个类。

第二个限制,扩展属性无法存储状态。

就如前面代码当中的 isAdult 属性一般,它的值是由 age 这个成员属性决定的,它本身没有状态,也无法存储状态。背后的根本原因,还是因为它们都是静态方法。

第三个限制,扩展的访问作用域仅限于两个地方。

  1. 如果扩展是顶层的扩展。

    那么扩展的访问域仅限于该 Kotlin 文件当中的所有成员,以及被扩展类型的公开成员,这种方式定义的扩展是可以被全局使用的。

  2. 如果扩展是被定义在某个类当中的。

    那么该扩展的访问域仅限于该类当中的所有成员,以及被扩展类型的公开成员,这种方式定义的扩展仅能在该类当中使用。


扩展的使用场景

Kotlin 扩展的第一个典型使用场景:关注点分离。主动使用扩展,通过它来优化软件架构。

所谓关注点分离,就是将我们程序的逻辑划分成不同的部分,每一个部分,都只关注自己那部分的职责。以 Kotlin 源码中的 String 类为例,String.kt 这个类,只关注 String 的核心逻辑;而 Strings.kt 则只关注 String 的操作符逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// String.kt (一共只有 13 行代码)
public class String : Comparable<String>, CharSequence {
companion object {}

public operator fun plus(other: Any?): String

public override val length: Int

public override fun get(index: Int): Char

public override fun subSequence(startIndex: Int, endIndex: Int): CharSequence

public override fun compareTo(other: String): Int
}
1
2
3
4
5
// Strings.kt 部分代码

public fun CharSequence.trim(): CharSequence = trim(Char::isWhitespace)

public expect fun String.lowercase(): String

扩展的第二个核心使用场景:被动使用扩展,提升可读性与开发效率。

当我们无法修改外部的 SDK 时,对于重复的代码模式,我们将其以扩展的方式封装起来,提供给对应的接收者类型。


总结

  1. Kotlin 的扩展,从 语法角度 来看,分为扩展函数和扩展属性。定义扩展的方式,只是比普通函数、属性多了一个“扩展接收者”而已。
  2. 作用域 角度来看,分为顶层扩展和类内扩展。
  3. 本质 上来看,扩展函数和扩展属性,它们都是 Java 静态方法,与 Java 当中的工具类别无二致。对比 Java 工具类,扩展最大的优势就在于,IDE 可以为我们提供代码补全功能。
  4. 能力 的角度来看,Kotlin 扩展一共有三个限制,分别是:扩展无法被重写;扩展属性无法存储状态;扩展的作用域有限,无法访问私有成员。
  5. 使用场景 的角度来看,Kotlin 扩展主要有两个使用场景,分别是:关注点分离,优化代码架构;消灭模板代码,提高可读性和开发效率。


参考文章:


Kotlin-扩展函数与扩展属性
https://luoyuy.top/posts/edb87b835e81/
作者
LuoYu-Ying
发布于
2023年3月1日
许可协议