跳到主要内容

Kotlin 2.5 未使用返回值检查器实战:让编译器帮你揪出那些被静默吞掉的关键结果

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

你有没有遇到过这种场景:代码跑得好好的,测试也通过了,但生产环境就是偶尔出现数据对不上的问题?最后排查了半天,发现罪魁祸首是一个被默默忽略的函数返回值。

来看一段看似无害的代码:

fun processOrder(order: Order) {
val cart = order.cart
// 移除库存不足的商品
cart.items.filter { it.inStock }
// 应用优惠券
cart.total.applyDiscount(order.coupon)
// 保存到数据库
repository.save(cart)
}

问题在哪?filter 不会修改原列表,它返回一个新列表;applyDiscount 如果返回的是新对象而非原地修改,那折扣根本没生效。两行代码的返回值都被静默丢弃了,但编译器一言不发。

这就是 Kotlin 2.5 稳定版的未使用返回值检查器(Unused Return Value Checker)要解决的问题。

这个检查器到底是什么​

简单来说:当一个函数返回了有意义的值(非 Unit、非 Nothing),而你调用它后既没有接收返回值,也没有用于条件判断或传递给其他函数,编译器就会发出警告。

这个功能最早在 Kotlin 2.3.0 以实验性身份登场,经过两个版本的打磨,终于在 Kotlin 2.5.0 中正式稳定。

快速上手:两种启用方式​

方式一:精准打击 —— 用注解标记​

@MustUseReturnValues
fun calculateDiscount(price: Double, rate: Double): Double {
return price * rate
}

fun main() {
calculateDiscount(100.0, 0.8) // ⚠️ 警告:返回值未被使用
}

@MustUseReturnValues 可以标记在函数、类甚至整个文件上:

// 标记整个文件
@file:MustUseReturnValues
package com.example.orders

// 标记整个类
@MustUseReturnValues
class PriceCalculator {
fun computeTotal(items: List<Item>): Double = items.sumOf { it.price }
fun applyTax(total: Double): Double = total * 1.13
}

方式二:全面覆盖 —— 编译器参数​

如果你希望项目里所有函数的返回值都被检查,可以在 build.gradle.kts 中开启 full 模式:

kotlin {
compilerOptions {
// "check" 模式:只检查带 @MustUseReturnValues 注解的
// "full" 模式:检查所有非 Unit/Nothing 返回值
freeCompilerArgs.add("-Xreturn-value-checker=full")
}
}

check 模式更温和,只检查你显式标记的函数;full 模式则一视同仁,适合对代码质量要求极高的团队。

实战场景:那些容易被忽略的返回值陷阱​

场景一:集合操作的"假修改"​

@MustUseReturnValues
fun demo() {
val list = mutableListOf(1, 2, 3)

list.plus(4) // ⚠️ plus 返回新列表,原列表不变
list.filter { it > 1 } // ⚠️ filter 返回新列表
list.map { it * 2 } // ⚠️ map 返回新列表

// 正确写法
val newList = list + 4
val filtered = list.filter { it > 1 }
list.add(4) // ✅ add 返回 Boolean,但 MutableList.add 默认标记为可忽略
}

这是最常见的坑。plus、filter、map 这些函数返回新集合,不会修改原集合。如果忽略了返回值,等于白操作。

场景二:字符串处理的"无效替换"​

fun sanitize(input: String): String {
input.trim() // ⚠️ trim 返回新字符串
input.replace("<", "&lt;") // ⚠️ replace 返回新字符串
input.lowercase() // ⚠️ lowercase 返回新字符串
return input // 返回的是原始未处理的字符串!
}

// 正确写法
fun sanitizeFixed(input: String): String =
input.trim().replace("<", "&lt;").lowercase()

Kotlin 的 String 是不可变的,所有修改操作都返回新字符串。忽略返回值意味着你的处理逻辑全部失效。

场景三:Result 和 Either 的静默失败​

@MustUseReturnValues
sealed class Result<out T> {
data class Success<T>(val value: T) : Result<T>()
data class Failure(val error: Throwable) : Result<Nothing>()
}

fun fetchUser(id: Long): Result<User> = TODO()

fun processUser(id: Long) {
fetchUser(id) // ⚠️ 返回值未使用,可能是一个 Failure 被静默忽略了
}

// 正确写法
fun processUserFixed(id: Long) {
when (val result = fetchUser(id)) {
is Result.Success -> println("User: ${result.value.name}")
is Result.Failure -> println("Error: ${result.error.message}")
}
}

场景四:数据库操作的返回值​

@MustUseReturnValues
interface UserRepository {
fun deleteById(id: Long): Boolean // 返回是否删除成功
fun update(user: User): Int // 返回受影响的行数
}

fun removeUser(id: Long, repo: UserRepository) {
repo.deleteById(id) // ⚠️ 删除可能失败了,但你完全不知道
}

// 正确写法
fun removeUserSafe(id: Long, repo: UserRepository) {
val deleted = repo.deleteById(id)
if (!deleted) {
log.warn("User $id not found for deletion")
}
}

合理抑制警告:不是所有忽略都是 Bug​

有些时候,你确实不需要返回值。Kotlin 提供了两种优雅的抑制方式:

方式一:@IgnorableReturnValue —— 函数作者说"可以忽略"​

@IgnorableReturnValue
fun MutableList<T>.addIfAbsent(element: T): Boolean {
if (contains(element)) return false
return add(element)
}

fun main() {
val list = mutableListOf(1, 2, 3)
list.addIfAbsent(4) // ✅ 无警告,作者已标记为可忽略
}

方式二:val _ = ... —— 调用者说"我知道,我故意的"​

fun computeExpensiveResult(): Result = TODO()

fun main() {
computeExpensiveResult() // ⚠️ 警告

val _ = computeExpensiveResult() // ✅ 显式丢弃,无警告
}

val _ 是 Kotlin 的"未命名变量"语法,表示"我明确知道有返回值,但我不需要它"。这比直接忽略多了一层意图表达,代码审查时一目了然。

在团队中渐进式推广​

直接开启 full 模式可能会让老项目瞬间冒出几百个警告。推荐分三步走:

第一步:只标记高风险函数

@MustUseReturnValues
fun validatePayment(payment: Payment): ValidationResult = TODO()

@MustUseReturnValues
fun verifyPermission(user: User, action: Action): Boolean = TODO()

从业务关键函数开始,比如支付验证、权限检查、数据校验等,这些地方忽略返回值往往代价最高。

第二步:标记整个模块或类

@file:MustUseReturnValues
package com.example.finance

// 财务模块里所有函数的返回值都值得被检查

第三步:全量开启 full 模式

当团队习惯了这种检查节奏,再在 CI 中启用 full 模式,把防线拉到最满。

与其他 JVM 语言的对比​

特性Kotlin 2.5JavaScala
返回值检查@MustUseReturnValues@CheckReturnValue(Error Prone)@nowarn + 自定义
抑制方式val _ = exprvar _ = exprval _ = expr
全量模式-Xreturn-value-checker=full需第三方工具需编译器插件
标准库集成内置需 Error Prone部分内置

Kotlin 的优势在于语言级别原生支持,不需要依赖第三方工具,且 val _ 语法比 Java 的 var _ 更简洁(因为 Kotlin 的 _ 是语言特性,不是变量名)。

总结​

Kotlin 2.5 稳定的未使用返回值检查器,本质上是把一类常见的运行时 Bug 提前到了编译期。它不会改变你的代码逻辑,但会在你差点犯错时拍一下肩膀:

  1. @MustUseReturnValues —— 标记那些"返回值必须被处理"的函数
  2. -Xreturn-value-checker=full —— 全量检查模式,适合严格团队
  3. @IgnorableReturnValue —— 告诉检查器"这个可以忽略"
  4. val _ = expr —— 调用侧显式丢弃,意图清晰

下次你写出 list.filter { ... } 然后忘记接收结果时,编译器会第一时间提醒你。这小小的警告,可能帮你省掉一个深夜排查 Bug 的夜晚。