扩展Result,让你的代码更简洁

屏幕截图 2024-06-12 235521.png

在使用 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,本文的很多例子就是根据这个项目来改造的,有兴趣的可以看看。

参考

如何扩展 kotlin.Result 让代码更简洁

作者:小墙程序员
链接:https://juejin.cn/post/7379509948903014451
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

<think>我们正在讨论如何简化代码,减少代码行数。根据用户提供的引用,我们可以总结出一些通用的简化代码的方法:1.使用一行代码(one-liner)代替多行代码(引用[1])2.使用模式匹配来避免复杂的嵌套条件判断(引用[2])3.使用三元运算符简化条件赋值(引用[3])4.使用Lambda表达式代替冗长的匿名类(引用[4])5.使用扩展运算符(spread operator)等现代语法简化操作(引用[5])结合这些方法,我们可以给出一些具体的简化代码的技巧,并举例说明。注意:简化代码的同时也要保证可读性,不能为了简化而简化。以下是一些常见的简化代码的技巧:</think>### 让代码简洁的 8 个核心技巧(附多语言示例) #### 1. **一行代码替代多行逻辑** [^1] ```python # 传统写法 if x > 0: result = "正数" else: result = "非正数" # 简化写法 result = "正数" if x > 0 else "非正数" # 三元运算符 [^3] ``` #### 2. **函数式编程替代循环** [^4] ```javascript // 传统写法 let squares = []; for(let i=0; i<10; i++){ squares.push(i*i); } // 简化写法 const squares = [...Array(10)].map((_,i) => i*i); // 扩展运算符+map [^5] ``` #### 3. **链式调用代替临时变量** ```java // 传统写法 List<String> filtered = list.stream() .filter(s -> s.length()>3) .collect(Collectors.toList()); Collections.sort(filtered); // 简化写法 list.stream() .filter(s -> s.length()>3) .sorted() .collect(Collectors.toList()); // 链式调用 [^4] ``` #### 4. **模式匹配代替嵌套 if-else** [^2] ```csharp // 传统嵌套判断 if (obj is int i) { return i * 2; } else if (obj is string s) { return s.Length; } ... // 简化写法 return obj switch { int i => i * 2, string s => s.Length, _ => throw new Exception("未知类型") }; // C# 模式匹配 ``` #### 5. **解构赋值简化数据访问** ```javascript // 传统写法 const firstName = user.firstName; const lastName = user.lastName; // 简化写法 const { firstName, lastName } = user; // 解构赋值 [^5] ``` #### 6. **使用内置高阶函数** ```python # 传统写法 total = 0 for num in numbers: if num % 2 == 0: total += num # 简化写法 total = sum(filter(lambda x: x%2==0, numbers)) # filter+lambda [^4] ``` #### 7. **合理使用默认参数** ```javascript // 传统写法 function connect(host, port, timeout) { host = host || "localhost"; port = port || 8080; timeout = timeout || 3000; } // 简化写法 function connect(host="localhost", port=8080, timeout=3000) {} // 默认参数 ``` #### 8. **利用语言特性合并操作** ```python # 传统写法 headers = {'Content-Type': 'application/json'} if token: headers['Authorization'] = f'Bearer {token}' # 简化写法 headers = {'Content-Type': 'application/json', **({'Authorization': f'Bearer {token}'} if token else {})} # 字典解包+条件表达式 ``` ### 关键原则总结: 1. **函数长度控制**:单函数不超过 20 行(Java)或 5 行(Python)[^1] 2. **消除重复**:相同逻辑只出现一次(DRY 原则) 3. **面向表达**:用表达式替代过程式语句 4. **善用现代语法**:如模式匹配、解构、扩展运算符等 5. **函数式思维**:map/filter/reduce 替代循环 [^4] > 简化不等于简单化:在减少代码行的同时,需确保可读性和可维护性不受影响。使用 CheckStyle 等工具可自动检测长函数 [^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值