如何简洁优雅地处理Kotlin中的缺失值问题
问题场景
需要优雅处理值缺失的场景(比如生成缺失值的日志提示),已编写JUnit参数化测试用例:
class MyArgumentsProvider: ArgumentsProvider { override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> = Stream.of( Arguments.of(Optional.of("an A"), Optional.of("a B"), ""), Arguments.of(Optional.of("an A"), Optional.empty<String>(), "no B"), Arguments.of(Optional.empty<String>(), Optional.of("a B"), "no A"), Arguments.of(Optional.empty<String>(), Optional.empty<String>(), "no A and no B"), ) } @ParameterizedTest @ArgumentsSource(MyArgumentsProvider::class) fun `missing values test`(maybeA: Optional<String>, maybeB: Optional<String>, expectedResult: String)
核心需求是:为每个缺失的值(如A或B)生成对应提示字符串,并用“ and ”拼接成单条消息。
发现Optional无法满足需求——空Optional无法区分是哪个值缺失。尝试用Arrow库的Either类型(用Right表示值存在,Left表示缺失提示如“no A”),通过流过滤Left实例来拼接结果:
val result = Stream.of( eitherOrNoA, eitherOrNoB ) .flatMap { it.left().stream() } .collect(joining(" and "))
但困惑于没有简洁惯用的方式从Optional转换为Either,想知道Kotlin原生、Arrow或其他库有没有合适的解决方案。
解决方案
1. Kotlin原生实现(无需第三方库)
Kotlin本身支持nullable类型,完全可以替代Optional直接构建缺失提示列表,不用依赖Either:
fun buildMissingMessage(a: String?, b: String?): String { val missing = mutableListOf<String>() if (a == null) missing.add("no A") if (b == null) missing.add("no B") return missing.joinToString(" and ") }
测试时只需把Optional转成Kotlin nullable:
@Test fun `test missing values`() { val cases = listOf( Pair(Optional.of("an A"), Optional.of("a B")) to "", Pair(Optional.of("an A"), Optional.empty()) to "no B", Pair(Optional.empty(), Optional.of("a B")) to "no A", Pair(Optional.empty(), Optional.empty()) to "no A and no B" ) cases.forEach { (inputs, expected) -> val a = inputs.first.orElse(null) val b = inputs.second.orElse(null) assertEquals(expected, buildMissingMessage(a, b)) } }
这种写法完全贴合Kotlin惯用风格,无额外依赖,最直接高效。
2. Arrow库的简洁转换方式
如果一定要用Arrow的Either,可以给Optional扩展一个转换函数,让代码更符合惯用写法:
import arrow.core.Either import arrow.core.left import arrow.core.right fun <T> Optional<T>.toEither(missingMsg: String): Either<String, T> { return if (isPresent) get().right() else missingMsg.left() }
之后就能用更简洁的集合操作替代流处理:
val eitherOrNoA = maybeA.toEither("no A") val eitherOrNoB = maybeB.toEither("no B") val result = listOf(eitherOrNoA, eitherOrNoB) .mapNotNull { it.leftOrNull() } // 提取Left中的缺失提示 .joinToString(" and ")
3. 自定义缺失类型(复杂场景扩展)
如果需要处理更多字段或更复杂的状态,可以自定义密封类来表示值的存在/缺失状态:
sealed class ValueState<T> { data class Present<T>(val value: T) : ValueState<T>() data class Missing<T>(val message: String) : ValueState<T>() } fun <T> Optional<T>.toValueState(missingMsg: String): ValueState<T> { return if (isPresent) ValueState.Present(get()) else ValueState.Missing(missingMsg) }
构建消息时直接过滤缺失状态即可:
val stateA = maybeA.toValueState("no A") val stateB = maybeB.toValueState("no B") val result = listOf(stateA, stateB) .filterIsInstance<ValueState.Missing<*>>() .map { it.message } .joinToString(" and ")
这种方式扩展性更强,后续新增字段时只需添加对应的状态转换逻辑。
内容的提问来源于stack exchange,提问作者Stefan Reisner
相关产品推荐
相关产品推荐

