扩展函数与属性:给别人的类加点料

说实话,我第一次接触 Kotlin 的扩展函数时,心里是有点抗拒的。当时我在想:「这玩意儿不就是语法糖吗?有啥用?」直到我在一个项目里被 Java 的工具类折磨得够呛——每次写 StringUtils.isEmpty(str) 这种代码,我都觉得哪里不对劲。后来我彻底倒向了扩展函数,嗯,真香。

扩展函数和扩展属性,说白了就是让你能给别人的类(甚至是 JDK 里的类)添加新方法、新属性,而不需要继承它,也不需要修改它的源码。这听起来有点「魔法」,但 Kotlin 在编译层面帮我们搞定了。

1. 扩展函数:最常用的扩展

先看一个最简单的例子。我想给 String 加一个方法,判断它是不是一个有效的手机号(简单判断长度和开头)。

fun String.isValidPhone(): Boolean {
    return this.startsWith("1") && this.length == 11
}

// 使用
val phone = "13800138000"
println(phone.isValidPhone()) // true

看到没?String 类本身没有 isValidPhone() 方法,但我通过扩展函数给它加上了。调用的时候就像调用它自己的方法一样自然。

这里有个关键点:扩展函数并不是真的修改了 String 类。它只是在编译时,把 this 作为第一个参数传进去。说白了,它就是一个静态方法,只不过 Kotlin 帮你隐藏了细节。

核心语法fun 接收者类型.函数名(参数列表): 返回类型 { 函数体 }

在函数体内部,你可以通过 this 访问接收者对象的成员。当然,this 也可以省略。

我在项目中遇到过这样一个场景:我们需要频繁地把 List<User> 转换成 Map<Long, User>(以用户 ID 为 key)。每次写循环太烦了,于是我写了个扩展函数:

fun <T, K> List<T>.toMapBy(keySelector: (T) -> K): Map<K, T> {
    val map = LinkedHashMap<K, T>()
    for (element in this) {
        map[keySelector(element)] = element
    }
    return map
}

// 使用
val userMap = userList.toMapBy { it.id }

这个扩展函数是泛型的,任何 List 都能用。你想想看,这比写一个 ListUtil.toMap(list, keySelector) 要优雅多少?

2. 扩展属性:给类加个「假」属性

扩展属性跟扩展函数类似,只不过它看起来像个属性。注意,它不能有 field 字段,因为它不是真的往类里塞数据。它本质上就是一对 getter/setter。

val String.isEmail: Boolean
    get() = this.contains("@") && this.contains(".")

// 使用
println("abc@example.com".isEmail) // true

你可能会问:「这跟写个 fun String.isEmail(): Boolean 有啥区别?」

我个人习惯是:如果这个操作是「判断型」的(返回 Boolean),或者「计算型」的(不依赖外部状态),我会用扩展属性。这样调用的时候不用加括号,读起来更像自然语言。

小技巧:扩展属性只能声明为 valvar,但 var 需要同时提供 getter 和 setter。不过因为不能有幕后字段,所以 var 的 setter 通常用来做副作用操作,比如写日志、更新缓存等。

3. 伴生对象扩展:给静态方法加点料

Kotlin 没有 static 关键字,静态成员都放在伴生对象里。那能不能给伴生对象加扩展?当然可以。

class MyClass {
    companion object {
        const val TAG = "MyClass"
    }
}

// 给伴生对象扩展方法
fun MyClass.Companion.createDefault(): MyClass {
    println("Creating default instance")
    return MyClass()
}

// 调用时就像调用伴生对象的静态方法
val instance = MyClass.createDefault()

这个特性在写工厂方法时特别有用。我曾经在一个 SDK 项目里,给某个第三方库的伴生对象扩展了一个 createWithDefaultConfig() 方法,这样调用方不用关心配置细节,直接一行代码就能拿到实例。

注意:如果伴生对象没有名字(即没有写 companion object Named {}),扩展时用 ClassName.Companion 来引用。如果伴生对象有名字,比如 companion object Factory {},那就用 ClassName.Factory

4. 扩展的作用域与解析:到底调用哪个?

这是扩展函数最容易被坑的地方。我刚开始用的时候也踩过坑,所以这里我详细说说。

第一,扩展函数是静态解析的。什么意思?它不参与多态。看代码:

open class Animal
class Dog : Animal()

fun Animal.speak() = "Animal sound"
fun Dog.speak() = "Woof!"

fun main() {
    val animal: Animal = Dog()
    println(animal.speak()) // 输出 "Animal sound",不是 "Woof!"
}

为什么会这样?因为扩展函数是在编译时根据变量的声明类型决定的,而不是运行时类型。说白了,animal 被声明为 Animal,所以调用的是 Animal.speak()。哪怕它实际指向一个 Dog 对象,也不会调用 Dog.speak()

第二,扩展函数的可见性受导入控制。你可以在一个文件里定义扩展,在另一个文件里使用它。但必须 import 进来。如果你不想暴露给外部,可以声明为 privateinternal

// 文件: StringExtensions.kt
package com.example.extensions

fun String.reverse(): String {
    return this.reversed()
}

// 文件: Main.kt
import com.example.extensions.reverse

fun main() {
    println("hello".reverse()) // "olleh"
}

第三,成员函数优先级高于扩展函数。如果类本身有一个同名同签名的函数,扩展函数永远不会被调用。这是 Kotlin 的设计原则:不破坏原有类的行为。

class MyList {
    fun size(): Int = 10
}

fun MyList.size(): Int = 20 // 这个扩展永远不会被调用

fun main() {
    val list = MyList()
    println(list.size()) // 10,不是 20
}

我曾经在代码 review 时看到有人给 List 扩展了一个 add 方法,结果死活不生效。嗯,就是因为 List 本身已经有 add 了。

知识体系总览

下面这张图总结了扩展函数与属性的核心知识点,我把它画成了流程图,方便你对照理解。

扩展函数与属性知识体系 扩展机制 扩展函数 扩展属性 伴生对象扩展 作用域与解析规则 静态解析 导入控制可见性 成员优先于扩展 不可覆盖成员 本质:编译时静态方法,不修改原类,不参与多态

总结一下

扩展函数和属性是 Kotlin 里我最喜欢的特性之一。它让代码更简洁、更可读,而且不会破坏原有类的封装。但记住几个要点:

  • 静态解析:扩展函数不参与多态,调用哪个版本由编译时类型决定。
  • 不能覆盖成员:如果类已经有同名函数,扩展函数会被忽略。
  • 可见性可控:通过 privateinternal 和导入机制控制。
  • 伴生对象也能扩展:适合写工厂方法或静态工具。

我个人建议,在项目里可以建一个 extensions 包,把所有扩展函数按功能分类放进去。比如 StringExtensions.ktListExtensions.kt。这样既好找,也好维护。

好了,扩展函数这块就聊到这儿。记住一句话:扩展是给别人的类加功能,但别滥用。如果一个类需要太多扩展,可能说明设计上该重构了。


公众号:蓝海资料掘金营,微信deep3321