扩展函数与属性:给别人的类“开外挂”

说实话,我第一次接触 Kotlin 的扩展函数时,心里是有点抗拒的。我当时想:“这玩意儿不就是语法糖吗?有啥好学的?”直到我在项目中遇到一个特别头疼的问题——要给一个第三方库的 String 类加个方法,但又不能改源码。嗯,这时候扩展函数就真香了。

今天咱们就来聊聊 Kotlin 里这个“开外挂”的能力:扩展函数、扩展属性、伴生对象和顶层函数。说白了,就是让你在不修改原有类的情况下,给它加新功能。

核心思想:扩展函数和属性并不是真的修改了类的内部结构,而是通过静态解析的方式,让你能用“类名.方法名”的语法来调用。这就像给别人的房子加了个门铃,但房子本身没变。

1. 扩展函数:给类“加方法”

先看个最简单的例子。假设我想给 String 类加一个方法,用来判断字符串是不是一个有效的手机号。传统做法是写个工具类,但 Kotlin 里可以这样:

// 定义扩展函数
fun String.isValidPhone(): Boolean {
    return this.matches(Regex("^1[3-9]\\d{9}$"))
}

// 使用
fun main() {
    val phone = "13800138000"
    println(phone.isValidPhone())  // 输出: true
}

看到没?直接在 String 类上“长”出了一个新方法。这里的 this 就是调用这个方法的字符串对象。我个人习惯把扩展函数放在一个单独的 Kotlin 文件里,比如 StringExt.kt,方便管理。

小技巧:扩展函数是静态解析的,不是动态派发。什么意思呢?就是说,如果你定义了一个扩展函数,它不会像类的成员方法那样被重写。我曾经踩过这个坑——给一个接口定义了扩展函数,结果子类调用时,调的还是父类的扩展函数。

2. 扩展属性:给类“加字段”

扩展属性跟扩展函数类似,但它看起来像是一个属性。注意,它不能有 field 字段,因为本质上它只是 getter/setter 的语法糖。

// 定义扩展属性
val String.isEmail: Boolean
    get() = this.contains("@") && this.contains(".")

// 使用
fun main() {
    val email = "test@example.com"
    println(email.isEmail)  // 输出: true
}

你想想看,这多方便。不用再写一堆工具类方法了。但要注意,扩展属性不能有幕后字段(backing field),所以你不能像普通属性那样给它赋值。说白了,它就是个“伪属性”。

避坑指南:我曾经在项目里给 View 类加了一个扩展属性 isVisible,结果发现它不能保存状态。每次调用都是重新计算的。如果你需要保存状态,还是老老实实用成员变量吧。

3. 伴生对象与静态成员

Java 里有 static 关键字,Kotlin 里没有。那静态成员怎么搞?用伴生对象(companion object)。

class MyClass {
    companion object {
        const val TAG = "MyClass"
        fun create(): MyClass = MyClass()
    }
}

// 调用
fun main() {
    println(MyClass.TAG)      // 输出: MyClass
    val obj = MyClass.create() // 创建实例
}

伴生对象说白了就是类内部的一个单例对象。它的成员可以通过类名直接访问,看起来就像 Java 的静态成员。我个人习惯把工厂方法、常量定义放在伴生对象里。

但要注意,伴生对象并不是真正的静态成员。它在运行时是一个真实的对象。如果你需要真正的静态方法(比如在 Java 中调用),可以用 @JvmStatic 注解:

class MyClass {
    companion object {
        @JvmStatic
        fun staticMethod() {
            println("这是真正的静态方法")
        }
    }
}

4. 顶层函数与属性

顶层函数和属性,说白了就是写在类外面的函数和属性。它们不属于任何类,直接属于包。

// 顶层函数
fun calculateArea(width: Int, height: Int): Int {
    return width * height
}

// 顶层属性
val PI = 3.14159

// 使用
fun main() {
    println(calculateArea(10, 20))  // 输出: 200
    println(PI)                     // 输出: 3.14159
}

顶层函数在 Java 中会被编译成静态方法,类名就是文件名加上 Kt 后缀。比如文件叫 MathUtils.kt,那在 Java 中调用就是 MathUtilsKt.calculateArea(10, 20)

我的建议:顶层函数适合放一些工具方法,比如字符串处理、数学计算。但别滥用,不然文件会变得又臭又长。我一般一个文件不超过 5 个顶层函数。

知识体系总览

下面这张图帮你理清今天讲的内容:

Kotlin 扩展机制 扩展函数 扩展属性 伴生对象 静态解析,非动态派发 无幕后字段 @JvmStatic 转真静态 顶层函数与属性 编译为静态方法/字段 图:Kotlin 扩展机制知识体系

实际项目中的选择

说了这么多,到底什么时候用哪种?我列个表给你参考:

场景 推荐方式 原因
给第三方类加方法 扩展函数 不能修改源码,扩展函数最合适
给类加计算属性 扩展属性 语法简洁,像原生属性一样使用
工具方法集合 顶层函数 不需要类包装,直接调用
工厂方法/常量 伴生对象 语义清晰,类名直接访问

总结一下:扩展函数和属性是 Kotlin 的“语法糖”,但用好了能大幅提升代码可读性。伴生对象替代了 Java 的 static,顶层函数则让工具类变得多余。记住一点:扩展不是真正的修改类,而是“看起来像”。理解这一点,你就不会踩坑了。

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