Изучив причины, по которым Котлин ввел такое поведение, я обнаружил, что, если исключения не будут распространяться таким образом, будет сложно написать корректный код, который будет своевременно отменен. Например:
runBlocking {
val deferredA = async {
Thread.sleep(10_000)
println("Done after delay")
1
}
val deferredB = async<Int> { throw Exception() }
println(deferredA.await() + deferredB.await())
}
Поскольку a
- это первый результат, которого мы ожидаем, этот код будет работать в течение 10 секунд, а затем приведет к ошибке и никакой полезной работе не будет достигнуто. В большинстве случаев мы хотели бы отменить все, как только один компонент выходит из строя. Мы могли бы сделать это так:
val (a, b) = awaitAll(deferredA, deferredB)
println(a + b)
Этот код менее элегантен: мы вынуждены ждать всех результатов в одном месте и теряем безопасность типов, потому что awaitAll
возвращает список общего супертипа всех аргументов. Если у нас есть
suspend fun suspendFun(): Int {
delay(10_000)
return 2
}
и мы хотим написать
val c = suspendFun()
val (a, b) = awaitAll(deferredA, deferredB)
println(a + b + c)
Мы лишены возможности спастись до завершения suspendFun
. Мы могли бы работать так:
val deferredC = async { suspendFun() }
val (a, b, c) = awaitAll(deferredA, deferredB, deferredC)
println(a + b + c)
но это хрупко, потому что вы должны остерегаться, чтобы убедиться, что вы делаете это для каждого приостановленного звонка. Это также противоречит доктрине Котлина о «последовательном по умолчанию»
В заключение: нынешний дизайн, хотя и поначалу нелогичный, имеет смысл как практическое решение. Это дополнительно усиливает правило не использовать async-await
, если вы не выполняете параллельную декомпозицию задачи.