扩展概述

以 JDK 内置的集合 ArrayList 为例,如果我们想给其添加一个能力:交互两个元素 swap(index1, index2)。我们应该怎么做?常见的有如下方式:

  • 工具类+静态方法,以 ArrayList 和要交换的两个 index 为入参,操作 ArrayList 交换元素;
  • 继承 ArrayList 新写一个类,在类里面创建 swap() 方法,前提是基类是可以继承的,比如 java.lang.String 就不行;
  • 使用组合的方式,写一个 ArrayListWrapper,内置一个 ArrayList,对外提供 swap() 方法

其实,这几种方式都存在劣势。不管是工具类还是继承的方式还是装饰者模式的组合类,都是新建了一个类,很可能有同事以前已经创建了这个类了。但是如果你不知道新类的名字是啥,就很尴尬了。或者懒得问干脆自己新建一个,一个新的“轮子”诞生了。

如果我们在 AndroidStudio 里面输入 ArrayList 就能自动提示 swap 这个函数就好了,就跟原本就有这个方法一样。唉~~Kotlin 就提供了这种能力,这就是扩展。

扩展函数

继续以 swap() 方法为例。我们在 Kotlin 中使用如下格式完成,先定义 swap() 方法:

fun <T> MutableList<T>?.swap(i1: Int, i2: Int) {
    if (this == null) {
        return
    }
    val tmp = this[i1]
    this[i1] = this[i2]
    this[i2] = tmp
}

不失一般性的,如果我们想向某个已经存在的类 ReceiverClassType 中“添加”一个函数 func,那么我们可以写成:

ReceiverClassType.func()

如果允许接收空值,则可以写成:

ReceiverClassType?.func()

知道了定义格式,如何使用扩展函数呢?这样:

val list = mutableListOf(1, 2, 3)
list.swap(0, 2)

扩展函数在安卓开发中也有用武之地,比如我们要给原生控件 ImageView 扩展支持加载网络图片的功能,有了扩展之后我们可以这么做:

import android.widget.ImageView
import com.bumptech.glide.Glide

fun ImageView.loadImage(url: String) {
    Glide.with(this).load(url).into(this)
}

再比如,我们要给原生控件 TextView 添加富文本的能力,后端网络接口返回给前端一个 JSON 数组,数组里面的每个元素代表文本中的一段子串的样式:

fun TextView.setRichText(jsonArray: String) {
    val builder: SpannableStringBuilder = parse(jsonArray)
    setText(builder)
}

简单了解用法之后,一大串问题就来了:

  • 扩展函数的实现原理是什么?
  • 扩展函数放在什么位置呢?
  • 可以有可见性修饰符吗?
  • 可以作为类的成员方法吗?如果可以,支持重载、重写、多态吗?
  • 如果扩展函数与成员函数冲突了,谁的优先级更高呢?
  • 如何从 Java 代码里面调用扩展函数呢?

实现原理

继续以上面的 swap() 方法为例,我们看下其反编译出来的 Java 代码:

public final class SwapKt {
   public static final void swap(@Nullable List $this$swap, int index1, int index2) {
      if ($this$swap != null) {
         Object tmp = $this$swap.get(index1);
         $this$swap.set(index1, $this$swap.get(index2));
         $this$swap.set(index2, tmp);
      }
   }
}

其实就是一个工具类+static方法,并没有真的向目标类添加新的方法。
如果我们想在 Java 代码中引用扩展函数,我们需要使用 SwapKt.swap() 的形式:

import example.kotlin.pkg.SwapKt;
import java.util.ArrayList;

ArrayList<Integer> list = new ArrayList<>();
list.add(0);
list.add(1);
SwapKt.swap(list, 0 ,1);

放置位置

我们知道,kotlin 的普通函数是可以放置在单个文件里面(也就是 top-level),也可以作为成员函数放置在类体内,扩展函数也不例外。
我们能以 top-level 的形式放在单个文件 Swap.kt 中:

package example.kotlin.pkg

fun <T> MutableList<T>?.swap(index1: Int, index2: Int) {
    // ...
}

扩展函数也可以作为成员函数,即将类 A 的扩展函数在类 B 中定义,我们称 A 为接收类型(Receiver Type),称 B 为分发类型(Dispatch Type)。比如下面示例代码中的 Host 就是接收类型,而 Connection 是分发类型:

class Host(val hostname: String) {
    fun printHost() {
        print(hostname)
    }
}

class Connection(val host: Host, val port: Int) {
    fun printPort() {
        print(port)
    }

    fun Host.printConnectionString() {
        host.printHost()
        print(":")
        printPort()
    }

    fun connect() {
        host.printConnectionString()
    }
}

fun main() {
    Connection(Host("kotl.in"), 443).connect()
    //Host("kotl.in").printConnectionString()  // error, the extension function is unavailable outside Connection
}

既然在分发类型中定义接收类型的扩展方法,那在扩展方法中引用 this 究竟是谁呢?接收类型还是分发类型的?实际是接收类型的,如果想引用分发类型的 this,需要使用 this@DispatchType:

class Connection {
    fun Host.getConnectionString() {
        toString()         // calls Host.toString()
        this@Connection.toString()  // calls Connection.toString()
    }
}

既然扩展函数可以以成员函数的形式出现,那么就有成员函数的相应“待遇”:重写、重载、多态等,但这些“待遇”都是分发类型才有的,而接收类型是没有的,接收类型的函数都是静态引用,只跟接收类型有关,先来看个例子:

open class Shape
class Rectangle: Shape()

fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"

fun printClassName(s: Shape) { // 只由 Shape 决定,而非其子类
    println(s.getName())
}

// 输出 Shape
printClassName(Rectangle())

// 输出 Shape
val shape: Shape = Rectangle()
printClassName(shape)

// 输出 Shape
val rect: Rectangle = Rectangle()
printClassName(rect as Shape)

在来看个更复杂的:

open class Shape
class Rectangle: Shape()

open class ShapeCaller {
    open fun Shape.getName() = "Shape in ShapeCaller"
    open fun Rectangle.getName() = "Rectangle in ShapeCaller"

    fun call(s: Shape) {
        println(s.getName())
    }
}

class RectangleCaller: ShapeCaller() {
    override fun Shape.getName() = "Shape in RectangleCaller"
    override fun Rectangle.getName() = "Rectangle in RectangleCaller"
}

fun main() {
    ShapeCaller().call(Shape())         // 输出 Shape in ShapeCaller
    ShapeCaller().call(Rectangle())     // 输出 Shape in ShapeCaller
    RectangleCaller().call(Shape())     // 输出 Shape in RectangleCaller
    RectangleCaller().call(Rectangle()) // 输出 Shape in RectangleCaller
}

由于 call(s: Shape) 里面的接收类型是 Shape,所有不能是 ShapeCaller 还是 RectangleCaller 都只会调用 Shape.getName(),但是调用父类还是子类里面的 Shape.getName() 却是符合多态的,具体决定于分发类型是 ShapeCaller 还是 RectangleCaller。

如果扩展函数和成员函数签名完全一致,那么成员函数优先级更高。

扩展属性

与扩展函数类似,Kotlin 还支持扩展属性。使用方法也类似,比如我们要给 MutableList 扩展一个本不存在的属性 lastIndex:

val <T> MutableList<T>?.lastIndex: Int
    get() {
        if (this == null) {
            return -1
        }

        return size - 1
    }

注意,因为扩展属性是本不存在的,因此其没有 Backing Field,我们也无法直接对其赋值,因此扩展属性只能是 val 类型的,否则会报错:

fun play() {
    val list = mutableListOf<String>()
    print(list.lastIndex)
    list.lastIndex = 1 // 报错
  
    // 结合扩展函数 swap() 实现交换首位元素
    list.swap(0, list.lastIndex)
}

最佳实践

在实际开发中,优先使用成员函数还是扩展函数呢?答案是优先使用成员函数,只有在无法使用成员函数,比如无法修改既有类或者虽然可以修改但是想控制使用权限的时候,再使用扩展函数。
同时,结合 infix 关键字我们可以实现很酷的功能,比如:

val ago = "ago"

infix fun Int.days(tense: String) = when (tense) {
    ago -> "2021-10-04 11:39:27"
    else -> "?"
}

fun main() {
//    val _2daysAgo = 2.days(ago)
    val _2daysAgo = 2 days ago
    println(_2daysAgo)
}

谁能想到 2 days ago 就是一行代码呢?而且是可以直接正常运行的代码,简直不要太酷!

更多常用扩展属性和方法见我的 Gist:Frequently used Kotlin extensions for Android

参考资料