在使用 Kotlin 开发 Android中,我们一般返回 Result
来表示方法调用的结果。对于 Result
的返回值,我们可以很方便得使用 onSuccess
和 onFailure
等扩展方法来处理不同的结果。代码示例如下,可以看到使用 Result 可以让我们的代码变得非常简洁。
fun method1(): Result<Int> {
if (isSuccess) {
return Result.success(1)
} else {
return Result.failure(Exception("method1 error"))
}
}
fun main() {
method1()
.onSuccess { println("onSuccess $it") }
.onFailure { println("onFailure $it") }
}
虽然 kotlin 对 Result
提供了很多方法来支持结果的获取,但是对于如何处理两个及以上的 Result
之间关系,kotlin 就没有什么提供很好的方法。这么说,你可能听不懂,下面举一个例子。
比如说我们有一个更新用户的头像的需求。这里有两个步骤,第一步,上传头像获取新头像的 url,第二步更新用户的信息。
fun uploadFile(): Result<String> {
// 上传头像
...
}
fun updateProfile(url: String): Result<Boolean> {
// 更新用户信息
...
}
// 展示错误的界面
fun showErrorPage() {
...
}
// 更新用户的UI
fun updateProfileUI() {
...
}
fun main() {
// 两个方法之间需要嵌套,代码不简洁
uploadFile().onSuccess {
updateProfile(it).onSuccess {
updateProfileUI()
}.onFailure {
showErrorPage()
}
}.onFailure {
showErrorPage()
}
}
可以看到,对于两个以上的 Result
之间的关系,Result
并没有提供相关的方法来处理。这就导致代码看起来非常难看。这篇文章,就介绍如何自己扩展 Result
,处理多个 Result
之间的关系,让你的代码更简洁。
依赖关系
一般的开发中,最常见的关系就是依赖关系了。比如前面的例子,更新用户信息的接口依赖上传用户头像的接口。在开发过程,依赖关系大体分为三种,分别是 一对一、一对多以及多对一。
一对一依赖
一对一依赖简单得说就是,方法B依赖方法A的返回值。最开始的例子就可以说是一对一依赖关系。如何解决这种关系呢,我们可以为 Result
添加扩展方法 andThen
,这样就可以把之前的 uploadFile
和 updateProfile
方法链接起来,代码如下:
uploadFile()
.andThen { updateProfile(it) }
.onSuccess { updateProfileUI() }
.onFailure { showErrorPage() }
当 uploadFile
或者 updateProfile
任意一个方法失败,最后会执行 onFailure
代码块里的 showErrorPage
方法。而当 uploadFile
和 updateProfile
都执行成功时,最后则会执行 onSuccess
代码块里的 updateProfileUI
方法。
这样看起来是不是简洁多了。如果你有更多方法需要串联,只需要往后面加一个 andThen
就可以了。扩展方法是不是非常方便,接下来我们来看看 andThen
是怎么样实现的。代码如下:
// 自定义异常,表示内部出错了
class InnerException(message: String = "inner value error"): Exception(message)
@OptIn(ExperimentalContracts::class)
public inline fun <V, E> Result<V>.andThen(transform: (V) -> Result<E>): Result<E> {
// kotlin 约定,告诉编译器 transform 最多执行一次
contract {
callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
}
if (isSuccess) {
val value = getOrNull() ?: return Result.failure(InnerException())
return transform(value)
} else {
val exception = exceptionOrNull() ?: return Result.failure(InnerException())
return Result.failure(exception)
}
}
首先我们自定义了一个 InnerException
异常,这是因为在 kotlin 的 Result
中的 value 是 internal
(模块内可见的)。我们只能通过 getOrNull
或者 exceptionOrNull
获取内部的值。通常情况下,isSuccess
为 true
时, getOrNull
一定有值,如果获取不到值就抛出 InnerException
异常,表示内部出错了。
inline
关键字则表示方法内联,是为了在循环中减少对象创建,避免性能问题。而 contract
以及注解 @OptIn(ExperimentalContracts::class)
属于 kotlin 约定。
一对多依赖
一对多依赖简单说就是,方法B 和 方法C 依赖方法A的返回值。比如说,你需要看你各个平台账号的视频播放量,这时就需要先获取你的手机号(一般来说平台账号就是手机号),再通过手机号获取不同平台的播放量,最后相加获得。代码示例如下:
// 获取用户手机号
fun getUserPhoneNumber(): Result<String> {
...
}
// 获取B站视频播放数据
fun getBilbilVideoPlayNum(phoneNumber: String): Result<Long> {
...
}
// 获取抖音的视频播放数据
fun getTiktokVideoPlayNum(phoneNumber: String): Result<Long> {
...
}
为了处理这种关系,我们可以为 Result
扩展 dispatch
方法,代码如下:
@OptIn(ExperimentalContracts::class)
public inline fun <V, E> Result<V>.dispatch(transform: (V) -> E): Result<E> {
contract {
callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
}
if (isSuccess) {
val value = getOrNull() ?: return Result.failure(InnerException())
return kotlin.runCatching {
transform(value)
}
} else {
val exception = exceptionOrNull() ?: return Result.failure(InnerException())
return Result.failure(exception)
}
}
这样,我们就可以使用 dispatch 扩展方法,来处理一对多的依赖关系,代码示例如下:
getUserPhoneNumber().dispatch { phoneNumber ->
val bilbilVideoPlayNum = getBilbilVideoPlayNum(phoneNumber).getOrThrow()
val tiktokVideoPlayNum = getTiktokVideoPlayNum(phoneNumber).getOrThrow()
bilbilVideoPlayNum + tiktokVideoPlayNum
}.onFailure { println("onFailure $it") }
.onSuccess { println("onSuccess $it") }
当 getUserPhoneNumber
、getBilbilVideoPlayNum
或者 getTiktokVideoPlayNum
任意一个方法出错时,就会执行 onFailure
中的代码;当三个方法都成功执行时,最后才会执行 onSuccess
中的代码。这样我们就可以简洁得对异常进行统一地处理。
多对一依赖
多对一依赖与一对多依赖正好相反,它是指方法C依赖于方法A和方法B的返回值。比如说,公司报销出差费用,需要发票单号;由于不同的人报销种类和费用不同,也需要获取个人的信息,代码示例如下:
// 获取个人信息
fun getUserInfo(): Result<UserInfo> {
...
}
// 获取发票单号
fun getInvoiceNumber(): Result<String> {
}
// 报销费用
fun reimbursement(userInfo: UserInfo, invoiceNumber: String): Result<Boolean> {
...
}
要处理这种关系,我们可以创建 zip
方法。代码如下:
@OptIn(ExperimentalContracts::class)
public inline fun <V> zip(block: () -> V): Result<V> {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return runCatching {
block()
}
}
使用 zip
方法,我们就可以这样处理多对一依赖,代码如下所示:
zip {
val userInfo = getUserInfo().getOrThrow()
val invoiceNumber = getInvoiceNumber().getOrThrow()
userInfo to invoiceNumber
}.andThen {
reimbursement(it.first, it.second)
}.onFailure { println("onFailure $it") }
.onSuccess { println("onSuccess $it") }
可以看到它和 dispatch
方法的用法非常相似,不同是 dispatch
是 Result
的扩展函数;而 zip
不是。
选择关系
可选关系简单地说就是二选一(或者多选一)。比如说手机付款,先用零钱余额支付,如果支付失败(比如余额不足)则使用花呗支付。代码示例如下:
// 使用零钱支付
fun payByChange(): Result<Boolean> {
}
// 是否花呗支付
fun payByHuabei(): Result<Boolean> {
}
处理这种选择关系,我们可以为 Result
创建 or
方法。代码如下:
public fun <V> Result<V>.or(result: Result<V>): Result<V> {
return when {
isSuccess -> this
else -> result
}
}
有了 or
方法,我们就可以简洁地处理选择的关系了。代码示例如下:
payByChange().or(payByHuabei())
.onFailure { println("onFailure $it") }
.onSuccess { println("onSuccess $it") }
Result集合
除了前文讲的依赖关系以及选择关系外,多个 Result
也可以组成一个集合。比如说你上传多张图片到朋友圈,每一张图片都可能上传成功或者失败,这时就获取到了不同图片的 Result
的集合。代码示例如下:
// 批量上传图片
fun uploadFiles(paths: List<String>): List<Result<String>> {
...
}
对于 Result 集合,我们可以做对其很多操作。比如说,如果我们只想知道有哪些图片上传成功了,就可以创建 valuesOf
的扩展方法,用来获取成功结果的列表,代码如下:
// 获取成功结果的列表
public fun <V, R : Result<V>> valuesOf(results: List<R>): List<V> {
return results.asIterable().filterValues()
}
public fun <V> Iterable<Result<V>>.filterValues(): List<V> {
return filterValuesTo(ArrayList())
}
public fun <V, C : MutableCollection<in V>> Iterable<Result<V>>.filterValuesTo(destination: C): C {
for (element in this) {
if (element.isSuccess) {
val value = element.getOrNull() ?: continue
destination.add(value)
}
}
return destination
}
如果我们需要判断图片上传是否都执行成功了,我们可以创建 allSuccess
方法来判断是否全部执行成功。
public fun <V> Iterable<Result<V>>.allSuccess(): Boolean {
return all(Result<V>::isSuccess)
}
集合的场景有很多,这里就不一一介绍了
支持协程
在一对多依赖的例子中,我们是同步获取不同平台的视频播放量。但在开发中,我们更期望是并发获取。代码示例如下:
getUserPhoneNumber().coroutineDispatch { phoneNumber ->
val bilbilVideoPlayNum = async {
getBilbilVideoPlayNum(phoneNumber).bind()
}
val tiktokVideoPlayNum = async {
getTiktokVideoPlayNum(phoneNumber).bind()
}
bilbilVideoPlayNum.await() + tiktokVideoPlayNum.await()
}.onFailure { println("onFailure $it") }
.onSuccess { println("onSuccess $it") }
我们来看看 coroutineDispatch
的实现。
// 由于 async 需要一个 CoroutineScope 来启动,这里创建一个Result的CoroutineScope
public interface CoroutineResultScope<V> : CoroutineScope {
public suspend fun <V> Result<V>.bind(): V
}
internal class CoroutineResultScopeImpl<V> (
delegate: CoroutineScope,
) : CoroutineResultScope<V>, CoroutineScope by delegate {
private val mutex = Mutex()
var result: Result<V>? = null
// 使用 bind 支持协程的结构化编程,这样当一个协程任务异常失败时,
// 取消其他的协程任务
override suspend fun <V> Result<V>.bind(): V {
return if (isSuccess) {
getOrNull() ?: throw InnerException()
} else {
mutex.withLock {
if (result == null) {
result = Result.failure(this.exceptionOrNull() ?: InnerException())
coroutineContext.cancel()
}
throw CancellationException()
}
}
}
}
@OptIn(ExperimentalContracts::class)
public suspend inline fun <V, E> Result<V>.coroutineDispatch(crossinline block: suspend CoroutineResultScope<E>.(V) -> E): Result<E> {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
if (isSuccess) {
lateinit var receiver: CoroutineBindingScopeImpl<E>
return try {
coroutineScope {
receiver = CoroutineBindingScopeImpl(this)
val value = getOrNull() ?: throw InnerException()
with(receiver) {
Result.success(block(value))
}
}
} catch (ex: CancellationException) {
receiver.result!!
}
} else {
return Result.failure(exceptionOrNull() ?: InnerException())
}
}
总结
在这篇文章中,我们介绍了如何扩展 Result
,来处理不同的结果分支,使我们的代码更加简洁。这种处理异常分支的方式叫做轨道编程(Railway Programming)。在 Github 中,也有很多用 Kotlin 实现轨道编程的项目,比如说 michaelbull/kotlin-result
,它内部自己定义了 Result
,而不是使用的 Kotlin 的 Result
,本文的很多例子就是根据这个项目来改造的,有兴趣的可以看看。
参考
- Railway Oriented Programming | F# for fun and profit (fsharpforfunandprofit.com)
- The Result Monad • Adam Bennett
- GitHub - michaelbull/kotlin-result: A multiplatform Result monad for modelling success or failure operations.
作者:小墙程序员
链接:https://juejin.cn/post/7379509948903014451
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。