Kotlin 扩展函数实战:告别 Utils 工具类的优雅姿势
你有没有翻过自己半年前写的 Kotlin 代码?我最近就翻了一次,发现了一个 StringUtils.kt 文件,里面躺着一堆这样的函数:
object StringUtils {
fun isEmail(s: String): Boolean {
return s.matches(Regex("^[\\w.-]+@[\\w.-]+\\.\\w+$"))
}
fun isEmpty(s: String?): Boolean {
return s == null || s.isBlank()
}
fun truncate(s: String, maxLen: Int): String {
return if (s.length > maxLen) s.take(maxLen) + "..." else s
}
}
// 调用时
if (StringUtils.isEmail(input)) { ... }
val name = StringUtils.truncate(user.name, 20)
每次调用都要先找到对应的 XxxUtils 对象,再调方法——这浓浓的 Java 味儿,让我意识到:我写了两年 Kotlin,居然一直在写 Java。
扩展函数:让对象自己"长"出新能力
Kotlin 最让人上瘾的特性之一就是 扩展函数(Extension Function)。它允许你在不修改源码、不继承的情况下,给任何类添加新方法。
把上面的工具类改造一下:
// 给 String 添加扩展函数
fun String.isEmail(): Boolean =
matches(Regex("^[\\w.-]+@[\\w.-]+\\.\\w+$"))
fun String.truncate(maxLen: Int): String =
if (length > maxLen) take(maxLen) + "..." else this
// 可空接收者:专门处理可能为 null 的情况
fun String?.isEmptyOrBlank(): Boolean =
this == null || isBlank()
调用时的变化是颠覆性的:
// 旧:StringUtils.isEmail(input)
if (input.isEmail()) { ... }
// 旧:StringUtils.truncate(user.name, 20)
val displayName = user.name.truncate(20)
// 旧:StringUtils.isEmpty(user.name)
if (user.name.isEmptyOrBlank()) { ... }
代码读起来就像这些方法本来就属于 String 类一样自然。
高阶函数:把"怎么做"交给调用者
扩展函数解决了"在哪里放工具方法"的问题,而 高阶函数(Higher-Order Function) 解决的是"如何让工具方法更灵活"的问题。
来看一个实际场景:你有一个 List<Int>,想找出所有满足某个条件的元素之和。
// 不够灵活:条件写死了
fun sumOfEvens(numbers: List<Int>): Int =
numbers.filter { it % 2 == 0 }.sum()
fun sumOfPositives(numbers: List<Int>): Int =
numbers.filter { it > 0 }.sum()
每换一种条件就要写一个新函数。用高阶函数,把"条件"作为参数传进去:
// 通用版本:调用者决定过滤规则
fun List<Int>.sumWhere(predicate: (Int) -> Boolean): Int =
filter(predicate).sum()
// 调用
val evenSum = numbers.sumWhere { it % 2 == 0 }
val positiveSum = numbers.sumWhere { it > 0 }
val bigSum = numbers.sumWhere { it > 100 }
这里 (Int) -> Boolean 就是一个 函数类型,表示"接收一个 Int,返回一个 Boolean"。Kotlin 把函数当作一等公民——可以赋值给变量、作为参数传递、作为返回值。
实战:写一个配置解析器
把扩展函数和高阶函数组合起来,解决一个真实问题:解析 KV 配置字符串。
// 原始数据
val rawConfig = """
host=localhost
port=8080
debug=true
timeout=30
""".trimIndent()
// 第一步:给 String 加扩展,解析成 Map
fun String.parseConfig(): Map<String, String> =
lineSequence()
.filter { it.contains('=') }
.associate { line ->
val (key, value) = line.split('=', limit = 2)
key.trim() to value.trim()
}
// 第二步:给 Map<String, String> 加扩展,带类型转换
inline fun <reified T> Map<String, String>.getOr(key: String, default: T): T {
val raw = this[key] ?: return default
return when (T::class) {
Int::class -> raw.toIntOrNull() as? T ?: default
Long::class -> raw.toLongOrNull() as? T ?: default
Boolean::class -> raw.toBooleanStrictOrNull() as? T ?: default
String::class -> raw as T
else -> default
}
}
// 使用
val config = rawConfig.parseConfig()
val host: String = config.getOr("host", "0.0.0.0") // "localhost"
val port: Int = config.getOr("port", 3000) // 8080
val debug: Boolean = config.getOr("debug", false) // true
val missing: String = config.getOr("token", "none") // "none"
这段代码有两个值得注意的点:
inline+reified:因为泛型在运行时会被擦除,普通泛型函数无法判断T的具体类型。用inline标记后,可以用reified保留类型信息,在函数体内做when (T::class)判断。- 链式调用:
rawConfig.parseConfig().getOr("port", 3000)读起来就像英语句子一样流畅。
扩展函数的本质:编译器的小把戏
你可能好奇:我明明没有修改 String 的源码,为什么可以直接 "hello".isEmail()?
答案是 静态解析。编译器在编译时,会把扩展函数调用转换成普通函数调用:
// 你写的
"hello".isEmail()
// 编译器实际生成的(伪代码)
StringUtilsKt.isEmail("hello")
扩展函数并不真的"注入"到目标类里,它只是一个语法糖。这也意味着两件事:
- 扩展函数不能访问类的 private 成员——因为它本质上是外部函数
- 成员函数优先级高于扩展函数——如果类本身有同名方法,扩展函数会被"遮蔽"
fun String.length(): Int = 42 // 永远不会被调用
println("hello".length) // 输出 5,调用的是 String 自带的 length
几个实用技巧
1. 给 View 加扩展,替代 findViewById 的样板代码(Android 场景)
fun View.visible() { visibility = View.VISIBLE }
fun View.gone() { visibility = View.GONE }
fun View.invisible() { visibility = View.INVISIBLE }
// 使用
textView.visible()
loadingSpinner.gone()
2. 用扩展函数实现 DSL 风格的代码
fun <T> MutableList<T>.add(vararg items: T) {
items.forEach { add(it) }
}
val list = mutableListOf<String>().apply {
add("Alice", "Bob", "Charlie")
}
3. 可空类型的扩展:优雅处理 null
// 接收者是 String?,意味着可以在 null 上调用
fun String?.orDefault(default: String = ""): String = this ?: default
val name: String? = null
println(name.orDefault("Anonymous")) // "Anonymous"
小结
扩展函数和高阶函数是 Kotlin "惯用写法"的核心支柱。记住三个原则:
| 原则 | 说明 |
|---|---|
| 优先用扩展,而非工具类 | str.isEmail() 比 StringUtils.isEmail(str) 可读性好得多 |
| 把变化的部分抽成 Lambda 参数 | 不要为每种条件写一个函数,传一个 predicate 进去 |
| 扩展函数是静态解析的 | 它不会真的修改类,也不能访问 private 成员 |
今日小挑战:试着给 List<T> 写一个扩展函数 groupByFirst(),按照每个元素第一个字符分组(假设元素是 String)。提示:你会用到标准库的 groupBy 函数。写完之后,再试试用高阶函数让它支持"按任意规则分组"——这时候你的函数签名应该长什么样?