跳到主要内容

Kotlin 扩展函数实战:告别 Utils 工具类的优雅姿势

· 阅读需 5 分钟
重庆 Kotlin User Group

你有没有翻过自己半年前写的 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"

这段代码有两个值得注意的点:

  1. inline + reified:因为泛型在运行时会被擦除,普通泛型函数无法判断 T 的具体类型。用 inline 标记后,可以用 reified 保留类型信息,在函数体内做 when (T::class) 判断。
  2. 链式调用: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 函数。写完之后,再试试用高阶函数让它支持"按任意规则分组"——这时候你的函数签名应该长什么样?