写一个 golang 风格的协程扩展

Posted Kotlin

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了写一个 golang 风格的协程扩展相关的知识,希望对你有一定的参考价值。

本文概要

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

 
   
   
 
  1. launch {

  2.    val deferred =   ...

  3.    val (result, error) = deferred.awaitOrError()

  4.    if(error == null){

  5.        dealWithResult(result)    

  6.    } else {

  7.        handleError(error)

  8.    }

  9. }

需求的诞生

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

 
   
   
 
  1. bytes, err := ioutil.ReadFile("Hello.go")

  2. if err == nil {

  3.    fmt.Print(string(bytes))

  4. } else {

  5.    fmt.Print(err)

  6. }

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

 
   
   
 
  1. try {

  2.    val deferred =   ...

  3.    val result = deferred.await()

  4.    dealWithResult(result)

  5. } catch (e: Exception) {

  6.    handleError(error)

  7. }

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

返回多个值

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

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

 
   
   
 
  1. suspend fun <T> Deferred<T>.awaitOrError(): Pair<T, Throwable> {

  2.    return try {

  3.        await() to null

  4.    } catch (e: Exception) {

  5.        null to e

  6.    }

  7. }

可空类型的返回值

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

 
   
   
 
  1. suspend fun <T> Deferred<T>.awaitOrError2(): Pair<T?, Throwable?> {

  2.    ...

  3. }

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

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

 
   
   
 
  1. val (result, err) = async { ... }.awaitOrError()

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

 
   
   
 
  1. if(err != null) {

  2.    if (result != null) {

  3.        dealWithResult(result)

  4.    }

  5. }

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

平台类型

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

 
   
   
 
  1. T!

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

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

 
   
   
 
  1. public class Result<T> {

  2.    private T result;

  3.    private Throwable error;

  4.    public T getResult() {

  5.        return result;

  6.    }

  7.    @Nullable

  8.    public Throwable getError() {

  9.        return error;

  10.    }

  11.    public static <T> Result<T> of(Throwable error) {

  12.        Result<T> result = new Result<T>();

  13.        result.error = error;

  14.        return result;

  15.    }

  16.    public static <T> Result<T> of(T result) {

  17.        Result<T> resultJava = new Result<T>();

  18.        resultJava.result = result;

  19.        return resultJava;

  20.    }

  21. }

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

Java 数据类与解构

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

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

 
   
   
 
  1.    ...

  2.    public T component1() {

  3.        return result;

  4.    }

  5.    @Nullable

  6.    public Throwable component2() {

  7.        return error;

  8.    }

  9.    ...

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

 
   
   
 
  1. suspend fun <T> Deferred<T>.awaitOrError(): Result<T> {

  2.    return try {

  3.        Result.of(await())

  4.    } catch (e: Exception) {

  5.        Result.of(e)

  6.    }

  7. }

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

 
   
   
 
  1. launch {

  2.    val deferred =   ...

  3.    val (result, error) = deferred.awaitOrError()

  4.    if(error == null){

  5.        dealWithResult(result)    

  6.    } else {

  7.        handleError(error)

  8.    }

  9. }

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

小结

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


以上是关于写一个 golang 风格的协程扩展的主要内容,如果未能解决你的问题,请参考以下文章

[Golang]实现一个带有等待和超时功能的协程池 - 类似Java中的ExecutorService接口实现

[Golang]实现一个带有等待和超时功能的协程池 - 类似Java中的ExecutorService接口实现

golang 高手才会的协程数量控制套路总结

golang 高手才会的协程数量控制套路总结

golang 高手才会的协程数量控制套路总结

golang 高手才会的协程数量控制套路总结