写一个 golang 风格的协程扩展

2020-02-20 13:21:02 浏览数 (1)

本文概要

Kotlin 的协程库 kotlinx.coroutines 当中有个比较常用的 async 函数,返回的 Deferred<T> 有个 await 方法,这个方法在子协程正常返回时返回结果,否则直接抛异常,而我们的目标是定义一个扩展 awaitOrError

代码语言:javascript复制
launch {
    val deferred =   ...
    val (result, error) = deferred.awaitOrError()
    if(error == null){
        dealWithResult(result)    
    } else {
        handleError(error)
    }
}

需求的诞生

最近因为要定制 BatteryHistorian 这个框架的某些小功能,近距离接触了一些 golang,发现这门语言当中很多可能出异常的函数调用返回两个结果,例如:

代码语言:javascript复制
bytes, err := ioutil.ReadFile("Hello.go")
if err == nil {
    fmt.Print(string(bytes))
} else {
    fmt.Print(err)
}

而对比我们 Kotlin 的协程库, await 要么返回结果,要么抛异常,我对于这一点觉得还是有点儿不太喜欢的,尽管我们可以用一个 try...catch 来捕获异步任务的异常,但写起来还是感觉在犯错误。

代码语言:javascript复制
try {
    val deferred =   ...
    val result = deferred.await()
    dealWithResult(result) 
} catch (e: Exception) {
    handleError(error)
}

我当时想,如果 Kotlin 的协程能写出 golang 风格的返回,那体验起来还是很不错的。

返回多个值

可是刚要动手写,就要扑街了,Kotlin 不支持多个返回值哎,咋整?

没关系,别忘了我们还有 Pair<A,B>,我们只需要在扩展的方法中返回这个类型,调用处用数据类的解构写法,返回多个值也不是什么问题了:

代码语言:javascript复制
suspend fun <T> Deferred<T>.awaitOrError(): Pair<T, Throwable> {
    return try {
        await() to null
    } catch (e: Exception) {
        null to e
    }
}

可空类型的返回值

嗯,看上去不错,只是没法通过编译。为什么呢?返回结果的泛型参数需要定义为可空类型才可以。

代码语言:javascript复制
suspend fun <T> Deferred<T>.awaitOrError2(): Pair<T?, Throwable?> {
    ...
}

这也是没办法的事儿啊,我们总是有返回 null 的可能嘛。

嗯,这回不仅看上去不错,编译也能通过了。不过,用起来却有点儿蛋疼。

代码语言:javascript复制
val (result, err) = async { ... }.awaitOrError()

这里拿到的 result 也好, err 也好,都是可空类型的,显然这对于后者来说到不是什么问题,而对于 result 来说,可空类型意味着我们在后面使用它的时候就需要判空:

代码语言:javascript复制
if(err != null) {
    if (result != null) {
        dealWithResult(result)
    }
}

额,这就有点儿尴尬了,因为从我们的代码的角度,只要 err 不为空,那么 result 一定不为空,可是编译器却对于这样的一对儿互斥关系一无所知。

平台类型

所以我们进入了一个尴尬的境地,我们想要的 Kotlin 语法本身似乎无法直接给我们了。我们现在就是想要让 awaitOrError 返回的 result 类型为不可空类型,或者至少看起来像是这样,这样我们用起来会轻松一些;而一旦它真正会是 null 的时候,我们又不会去使用它,这样做本身没有什么风险。只是,有什么途径允许我们这么做呢?

代码语言:javascript复制
T!

平台类型。没错就是平台类型。如果返回的 resultT!,那么 Kotlin 就不会对它有太多的约束,你愿意把它当做可空类型,那他就可以是可空类型,反之,你愿意把它当做不可空类型,只要在使用前能确定它不为空就好。听起来不错。

所以我们决定返回值不用 Pair,而是使用一个 Java 类:

代码语言:javascript复制
public class Result<T> {

    private T result;
    private Throwable error;

    public T getResult() {
        return result;
    }

    @Nullable
    public Throwable getError() {
        return error;
    }

    public static <T> Result<T> of(Throwable error) {
        Result<T> result = new Result<T>();
        result.error = error;
        return result;
    }

    public static <T> Result<T> of(T result) {
        Result<T> resultJava = new Result<T>();
        resultJava.result = result;
        return resultJava;
    }
}

注意到对于 getError ,我明确用注解标注其返回值为可空,这就是告诉 Kotlin,这个可以为 null,而 getResult 没有。

Java 数据类与解构

只是,这时候又产生了新的问题,Java 中要怎么定义数据类呢?不是数据类又怎么解构呢?

相比之下,这个问题就简单多了,如果你对 Kotlin 的数据类的字节码比较熟悉,你就会想到只要我们在前面的 Result 类当中添加两个方法:

代码语言:javascript复制
...
    public T component1() {
        return result;
    }

    @Nullable
    public Throwable component2() {
        return error;
    }
    ...

只要你定义了 componentN 方法,哪怕是在 Java 当中定义,Kotlin 当中对于这个类的实例也是可以进行解构的。有了前面的方法,我们的 awaitOrError 就可以进一步修改了:

代码语言:javascript复制
suspend fun <T> Deferred<T>.awaitOrError(): Result<T> {
    return try {
        Result.of(await())
    } catch (e: Exception) {
        Result.of(e)
    }
}

而在调用处,也能按照我们的意愿去检查错误,使用结果,就像文章开头提到的那样:

代码语言:javascript复制
launch {
    val deferred =   ...
    val (result, error) = deferred.awaitOrError()
    if(error == null){
        dealWithResult(result)    
    } else {
        handleError(error)
    }
}

注意到上述的 resultT! 类型,即平台类型。

小结

终于可以在协程中抛弃 try...catch... 了!


0 人点赞