写一个 golang 风格的协程扩展
Posted Kotlin
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了写一个 golang 风格的协程扩展相关的知识,希望对你有一定的参考价值。
本文概要
Kotlin 的协程库 kotlinx.coroutines 当中有个比较常用的 async
函数,返回的 Deferred<T>
有个 await
方法,这个方法在子协程正常返回时返回结果,否则直接抛异常,而我们的目标是定义一个扩展 awaitOrError
:
launch {
val deferred = ...
val (result, error) = deferred.awaitOrError()
if(error == null){
dealWithResult(result)
} else {
handleError(error)
}
}
需求的诞生
最近因为要定制 BatteryHistorian 这个框架的某些小功能,近距离接触了一些 golang,发现这门语言当中很多可能出异常的函数调用返回两个结果,例如:
bytes, err := ioutil.ReadFile("Hello.go")
if err == nil {
fmt.Print(string(bytes))
} else {
fmt.Print(err)
}
而对比我们 Kotlin 的协程库, await
要么返回结果,要么抛异常,我对于这一点觉得还是有点儿不太喜欢的,尽管我们可以用一个 try...catch
来捕获异步任务的异常,但写起来还是感觉在犯错误。
try {
val deferred = ...
val result = deferred.await()
dealWithResult(result)
} catch (e: Exception) {
handleError(error)
}
我当时想,如果 Kotlin 的协程能写出 golang 风格的返回,那体验起来还是很不错的。
返回多个值
可是刚要动手写,就要扑街了,Kotlin 不支持多个返回值哎,咋整?
没关系,别忘了我们还有 Pair<A,B>
,我们只需要在扩展的方法中返回这个类型,调用处用数据类的解构写法,返回多个值也不是什么问题了:
suspend fun <T> Deferred<T>.awaitOrError(): Pair<T, Throwable> {
return try {
await() to null
} catch (e: Exception) {
null to e
}
}
可空类型的返回值
嗯,看上去不错,只是没法通过编译。为什么呢?返回结果的泛型参数需要定义为可空类型才可以。
suspend fun <T> Deferred<T>.awaitOrError2(): Pair<T?, Throwable?> {
...
}
这也是没办法的事儿啊,我们总是有返回 null
的可能嘛。
嗯,这回不仅看上去不错,编译也能通过了。不过,用起来却有点儿蛋疼。
val (result, err) = async { ... }.awaitOrError()
这里拿到的 result
也好, err
也好,都是可空类型的,显然这对于后者来说到不是什么问题,而对于 result
来说,可空类型意味着我们在后面使用它的时候就需要判空:
if(err != null) {
if (result != null) {
dealWithResult(result)
}
}
额,这就有点儿尴尬了,因为从我们的代码的角度,只要 err
不为空,那么 result
一定不为空,可是编译器却对于这样的一对儿互斥关系一无所知。
平台类型
所以我们进入了一个尴尬的境地,我们想要的 Kotlin 语法本身似乎无法直接给我们了。我们现在就是想要让 awaitOrError
返回的 result
类型为不可空类型,或者至少看起来像是这样,这样我们用起来会轻松一些;而一旦它真正会是 null
的时候,我们又不会去使用它,这样做本身没有什么风险。只是,有什么途径允许我们这么做呢?
T!
平台类型。没错就是平台类型。如果返回的 result
是 T!
,那么 Kotlin 就不会对它有太多的约束,你愿意把它当做可空类型,那他就可以是可空类型,反之,你愿意把它当做不可空类型,只要在使用前能确定它不为空就好。听起来不错。
所以我们决定返回值不用 Pair
,而是使用一个 Java 类:
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
类当中添加两个方法:
...
public T component1() {
return result;
}
@Nullable
public Throwable component2() {
return error;
}
...
只要你定义了 componentN
方法,哪怕是在 Java 当中定义,Kotlin 当中对于这个类的实例也是可以进行解构的。有了前面的方法,我们的 awaitOrError
就可以进一步修改了:
suspend fun <T> Deferred<T>.awaitOrError(): Result<T> {
return try {
Result.of(await())
} catch (e: Exception) {
Result.of(e)
}
}
而在调用处,也能按照我们的意愿去检查错误,使用结果,就像文章开头提到的那样:
launch {
val deferred = ...
val (result, error) = deferred.awaitOrError()
if(error == null){
dealWithResult(result)
} else {
handleError(error)
}
}
注意到上述的 result
是 T!
类型,即平台类型。
小结
终于可以在协程中抛弃 try...catch...
了!
以上是关于写一个 golang 风格的协程扩展的主要内容,如果未能解决你的问题,请参考以下文章
[Golang]实现一个带有等待和超时功能的协程池 - 类似Java中的ExecutorService接口实现