1. Preconditions.kt中的require与check函数解析
在Kotlin标准库的Preconditions.kt文件中,require和check是两个常用的参数校验函数。它们看起来功能相似,但设计意图和使用场景有本质区别。作为Kotlin开发者,理解这两个函数的差异对编写健壮代码至关重要。
require主要用于验证函数参数的有效性,当条件不满足时抛出IllegalArgumentException。这是防御性编程的重要手段,可以避免无效参数进入函数逻辑。而check则用于验证对象状态,当条件不满足时抛出IllegalStateException,通常用于确保对象在特定操作前处于合法状态。
2. require函数深度剖析
2.1 基本用法与实现原理
require函数的签名如下:
public inline fun require(value: Boolean, lazyMessage: () -> Any): Unit { if (!value) { val message = lazyMessage() throw IllegalArgumentException(message.toString()) } }典型使用场景是验证函数参数:
fun calculateArea(width: Double, height: Double): Double { require(width > 0) { "宽度必须大于0" } require(height > 0) { "高度必须大于0" } return width * height }关键细节:require使用内联函数+lazy message的设计,避免了不必要的字符串拼接开销。只有当条件不满足时才会执行消息构造逻辑。
2.2 最佳实践与性能考量
消息构造优化:对于复杂错误消息,应该使用lambda表达式延迟构造:
// 不推荐 - 无论是否抛出异常都会构造字符串 require(condition, "复杂消息:${expensiveOperation()}") // 推荐 - 仅在抛出异常时构造消息 require(condition) { "复杂消息:${expensiveOperation()}" }多条件校验顺序:应该按照参数校验成本从低到高排序:
fun process(data: List<Data>?, threshold: Int) { require(threshold >= 0) { "阈值不能为负" } // 简单检查 require(data != null) { "数据不能为空" } // 中等复杂度 require(data.all { it.isValid() }) { "包含无效数据" } // 高成本检查 }与null检查的配合:在Kotlin中通常使用非空类型声明,但需要时可以与require结合:
fun save(user: User?) { require(user != null) { "用户不能为空" } // 后续user自动智能转换为非空类型 }
3. check函数详解
3.1 设计目的与典型场景
check函数的签名与require几乎相同,但抛出的是IllegalStateException:
public inline fun check(value: Boolean, lazyMessage: () -> Any): Unit { if (!value) { val message = lazyMessage() throw IllegalStateException(message.toString()) } }它主要用于验证对象状态:
class Connection { private var isOpen = false fun send(data: ByteArray) { check(isOpen) { "连接未建立" } // 发送逻辑 } fun open() { isOpen = true } }3.2 状态验证模式
前置条件验证:在关键操作前验证状态
fun execute() { check(!isCancelled) { "任务已取消" } // 执行逻辑 }不变式维护:确保对象始终满足某些条件
class BankAccount { private var balance: Double = 0.0 fun withdraw(amount: Double) { check(amount > 0) { "取款金额必须为正数" } check(amount <= balance) { "余额不足" } balance -= amount check(balance >= 0) { "余额不能为负" } // 不变式检查 } }线程安全验证:在并发环境中验证状态
@Volatile private var initialized = false fun init() { synchronized(this) { check(!initialized) { "重复初始化" } // 初始化逻辑 initialized = true } }
4. require与check的对比分析
4.1 异常类型差异
| 函数 | 抛出异常类型 | 语义含义 |
|---|---|---|
| require | IllegalArgumentException | 参数不符合要求 |
| check | IllegalStateException | 对象状态不合法 |
4.2 使用场景对照
require适用场景:
- 验证函数参数的有效范围
- 确保输入数据满足前置条件
- 公共API的入口参数校验
check适用场景:
- 验证对象内部状态一致性
- 确保操作执行时的环境条件
- 维护类的不变式(invariant)
4.3 性能影响对比
两者在性能开销上几乎相同,都是内联函数。但误用可能导致额外开销:
// 反模式 - 在循环内部进行冗余检查 fun processItems(items: List<Item>) { items.forEach { item -> require(item.isValid()) // 每次循环都检查 // 处理逻辑 } } // 优化方案 - 提前批量检查 fun processItems(items: List<Item>) { require(items.all { it.isValid() }) items.forEach { item -> // 处理逻辑 } }5. 高级应用与常见问题
5.1 自定义扩展函数
可以基于require/check创建领域特定的验证函数:
// 邮箱格式验证扩展 fun requireEmail(email: String) = require( email.matches(Regex("^\\S+@\\S+\\.\\S+$")) ) { "无效邮箱格式: $email" } // 状态机验证扩展 fun <T> T.checkState(predicate: T.() -> Boolean, message: () -> String) { check(predicate(), message) }5.2 与Kotlin契约的结合
Kotlin 1.3引入的契约系统可以与这些函数配合:
@ExperimentalContracts fun validate(value: Int) { contract { returns() implies (value > 0) } require(value > 0) } // 编译器能推断出后续value > 0为true fun test(value: Int) { validate(value) println(value + 1) // 安全操作 }5.3 常见陷阱与解决方案
过度使用问题:
// 不必要检查 - Kotlin类型系统已经保证非空 fun length(s: String) = require(s != null).let { s.length } // 正确方式 - 直接使用 fun length(s: String) = s.length错误消息质量问题:
// 不良实践 - 无助于调试 check(state, { "状态错误" }) // 良好实践 - 包含诊断信息 check(state) { "状态错误,当前值=$currentValue,期望=$expectedValue" }测试策略: 应该为require/check条件编写专门的测试用例:
@Test fun `should throw when negative width`() { assertThrows<IllegalArgumentException> { calculateArea(-1.0, 10.0) } } @Test fun `should throw when connection not open`() { val conn = Connection() assertThrows<IllegalStateException> { conn.send(byteArrayOf()) } }
6. 工程实践建议
代码审查要点:
- 检查所有公共API是否对关键参数使用require
- 验证状态变更方法是否包含必要的check
- 确保错误消息包含足够调试信息
静态分析配置: 配置Detekt等静态分析工具检查:
style: MandatoryBracesIfStatements: active: true PreferCheckOverRequire: active: true allowedPattern: '.*Spec$'性能敏感场景处理: 对于性能关键路径,可以使用特殊构建变体:
inline fun <T> T.requireFast( condition: T.() -> Boolean, lazyMessage: T.() -> Any ) { if (BuildConfig.DEBUG && !condition()) { throw IllegalArgumentException(lazyMessage().toString()) } }与日志系统的集成: 可以扩展函数记录验证失败:
inline fun <T> T.checkWithLog( condition: T.() -> Boolean, lazyMessage: T.() -> Any ) { if (!condition()) { val msg = lazyMessage() logger.error("State check failed: $msg") throw IllegalStateException(msg.toString()) } }
在大型Kotlin项目中,合理使用require和check可以显著提高代码健壮性。根据我的经验,建议在代码规范中明确规定:
- 所有公共方法的参数必须使用require验证
- 所有状态变更操作必须使用check保持一致性
- 错误消息必须包含可操作的调试信息
这种实践可以将许多运行时错误提前到开发阶段暴露,大幅降低生产环境的事故率。