Kotlin中require()使用时机与参数校验后对象状态处理最佳实践
一、require()与check()的理解是否正确?
完全正确。
require()用于校验调用方传入的参数是否符合契约,属于对外的参数合法性检查,抛出IllegalArgumentException,明确指向调用方的错误。check()用于校验方法内部的状态或逻辑是否符合预期,是内部一致性检查,抛出IllegalStateException,指向自身代码的设计或实现故障。
二、能否在循环中使用require()?
语法上可行,但你的代码会引发对象状态不一致的问题:一旦遍历中途遇到非法元素,require()抛出异常时,elementArray已添加部分元素、moduleCount也已累加,这些修改无法回滚,会让对象处于半更新的无效状态。若调用方捕获异常后继续使用该对象,必然触发后续逻辑错误,这是需要避免的。
三、此类场景的最佳实践
针对这种“遍历过程中才能校验参数,且不想重复遍历Sequence”的场景,推荐以下方案:
1. 先构建临时容器完成校验,再原子化更新内部状态
先将输入转换为临时List<Int>(对Sequence来说,这会触发一次终端操作),完成全量校验后,再一次性更新内部状态:
public fun loadElements(e: Iterable<Int>) { val tempList = e.toList() tempList.forEach { require(it >= 0) } // 校验通过后,统一修改内部状态 elementArray.clear() elementArray.addAll(tempList) moduleCount = tempList.sum() if (elementArray.size % 2 == 1) { elementArray.add(0) } check(elementArray.size % 2 == 0) computeInternalSizes() }
优点:保证内部状态的原子性,要么全更新要么全不更新;校验逻辑集中,代码清晰。
缺点:对Sequence会触发终端操作,需额外内存存储临时列表,但这是保证状态安全的合理代价——状态一致性的优先级远高于这点性能损耗。
2. 手动实现事务式状态更新(异常时回滚)
如果必须避免提前构建全量临时列表,可以在遍历前备份原始状态,一旦发现异常就回滚:
public fun loadElements(e: Iterable<Int>) { // 备份原始状态 val originalElements = ArrayList(elementArray) val originalModuleCount = moduleCount try { elementArray.clear() moduleCount = 0 e.forEach { require(it >= 0) elementArray.add(it) moduleCount += it } if (elementArray.size % 2 == 1) { elementArray.add(0) } check(elementArray.size % 2 == 0) computeInternalSizes() } catch (ex: IllegalArgumentException) { // 回滚到原始状态 elementArray.clear() elementArray.addAll(originalElements) moduleCount = originalModuleCount throw ex // 重新抛出异常,不隐藏调用方错误 } }
优点:无需提前构建全量临时列表,对内存敏感场景更友好。
缺点:需要手动备份和恢复状态,代码复杂度略高;若内部状态较多,备份逻辑会更繁琐。
3. 明确禁止从require()异常中恢复
从设计语义看,require()抛出的IllegalArgumentException属于不可恢复的调用错误,调用方不应捕获并继续使用该对象。你可以在方法文档中明确标注:
若传入包含负整数的Iterable,会抛出IllegalArgumentException,此时对象状态未定义,禁止继续使用。
这种方案依赖调用方遵守契约,适合内部系统或可控调用场景,无需额外回滚逻辑,代码最简洁。
关于是否从require()异常恢复的问题
程序不应该从require()的异常中恢复。require()的语义是“调用方违反契约,当前操作无法继续”,此时对象状态已不可靠,继续使用只会引发更多错误。调用方的正确做法是修复参数后重新调用,而非捕获异常继续执行。
内容的提问来源于stack exchange,提问作者fiatCurrency

