扩展函数与属性:给别人的类“开外挂”
说实话,我第一次接触 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 的“语法糖”,但用好了能大幅提升代码可读性。伴生对象替代了 Java 的 static,顶层函数则让工具类变得多余。记住一点:扩展不是真正的修改类,而是“看起来像”。理解这一点,你就不会踩坑了。