扩展函数

104 阅读3分钟

为什么要使用扩展?

在 Java 中,我们习惯于把通用代码封装到工具类中,诸如 StringUtils、ViewUtils 等,例如:

StringUtils.java
public static void firstChar(String str) {
    ...
}

在使用时,我们就需要调用StringUtils.firstChar(str)。然而,这种传统的调用方式不够简单直接,表意性也不够强,会让调用方忽略 String 和 firstChar() 间的强联系。另外,调用方也希望省略 StringUtils 类名,让 firstChar() 看起来更像是 String 内部的一个属性和方法,像这样:"str".firstChar()

要实现这种方式,在 Java 中就需要修改或继承 String 类,然而 String 是 JDK 中的 final 类,不能修改或继承。

这个时候可以使用 Kotlin 扩展来解决这个问题,我们可以把 firstChar 定义为 String 的扩展函数:

StringUtils.kt
定义 String 的扩展函数

fun String.firstChar() {
    ...
}

此时,在使用时可以采用"str".firstChar()的方式。在这里我们扩展了 String 类,却没有修改或继承 String。

总结:扩展是 Kotlin 中的一种特性,可以 在不修改类 / 不继承类的情况下,向一个类添加新函数或者新属性,更符合开闭原则。

开闭原则(OCP,Open Closed Principle)

开闭原则是面向对象软件设计的原则之一,即:对扩展开放,而对修改是封闭的。

扩展函数 & 扩展属性

声明扩展

声明扩展非常简单,只需要在声明时增加「类或者接口名」。这个类的名称称为 接收者类型(receiver type),调用这个扩展的对象称为 接收者对象。大多数情况下,扩展会声明为「顶级成员」,例如:

Utils.kt
声明扩展函数:
fun <T : Any?> MutableList<T>.exchange(fromIndex: Int, toIndex: Int) {
    val temp = this[fromIndex]
    this[fromIndex] = this[toIndex]
    this[toIndex] = temp
}

声明扩展属性:
val MutableList<Int>.sumIsEven
    get() = this.sum() % 2 == 0

在使用时,就可以直接像使用普通成员函数 / 属性一样:

xxx.kt
val list = mutableListOf(1,2,3)

使用扩展函数:
list.exchange(1,2)

使用扩展属性:
val isEven = list.sumIsEven

提示: MutableList 是接收者类型,list 是接收者对象。

在扩展函数内部,你可以像 「成员函数」 那样使用this来引用接受者对象,当然有时也可以省略,例如:

声明扩展属性:
val MutableList<Int>.sumIsEven
    get() = this.sum() % 2 == 0 // 省略了 this.sum() 中的 this

可空接收者

第 2.1 节 中使用了「非空的接收者类型」来定义扩展(MutableList 没有关键词?),当使用「可空变量」调用扩展时,会报编译时错误。例如:

val list:MutableList<Int>? = null
list.sumIsEven // Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type MutableList<Int>?

根据提示,我们知道可以 使用「可空的接收者类型」来定义扩展,同时还要在内部使用null == this来对接收者对象进行判空。例如:

可空接收者类型的扩展函数
fun <T : Any?> MutableList<T>?.exchange(fromIndex: Int, toIndex: Int) {
    if (null == this) return
    val temp = this[fromIndex]
    this[fromIndex] = this[toIndex]
    this[toIndex] = temp
}

可空接收者类型的扩展属性
val MutableList<Int>?.sumIsEven: Boolean
    get() = if (null == this)
        false
    else
        this.sum() % 2 == 0

在 Java 中调用

扩展的本质:扩展函数是定义在类外部的静态函数,函数的第一个参数是接收者类型的对象。这意味着调用扩展时不会创建适配对象或者任何运行时的额外消耗。

在 Java 中,我们只需要像调用普通静态方法那样调用扩展即可。例如:

xxx.java
ArrayList<Integer> list = new ArrayList<>(3);

使用扩展函数:
UtilsKt.exchange(list, 1, 2);

使用扩展属性:
boolean isEven = UtilsKt.getSumIsEven(list);

扩展的作用域

当你定义了一个扩展之后,它不会自动在整个项目内生效。在其它包路径下,需要使用improt导入。例如:

import Utils.exchange
或
import Utils.*

当你在不同包中定义了 「重名扩展」,并且需要在同一个文件中去使用它们,那么你需要使用as关键字重新命名。例如:

import Utils.exchange as swap

使用时:
list.swap(0,1)