Я пытаюсь изучить и реализовать механизм отмены/восстановления исключений сопрограммы в ViewModel. Я обнаружил, что следующий код в моей ViewModel не перехватывает исключение и приводит к сбою приложения:
viewModelScope.launch {
try {
supervisorScope {
launch {
throw Exception()
}
}
} catch (e: Exception) {
println("Exception caught")
}
}
Но если я заменю supervisorScope
на coroutineScope
, это поймается. Разве он не должен быть пойман в обоих случаях? Может ли кто-нибудь объяснить, почему исключение области superviscope отменяет здесь родительскую область?
Я попробовал запустить следующий код в Intellij: Дело 1 :
runBlocking {
supervisorScope {
launch {
throw Exception("Supervisor launch exception")
}
}
}
против случай 2:
runBlocking {
launch {
throw Exception("Launch Exception")
}
}
В первом случае процесс завершился с кодом выхода 0, а во втором случае - с кодом выхода 1. Почему он выдает другой код выхода, когда оба передают исключение родительскому элементу?
Основная функция supervisorScope
, в отличие от coroutineScope
, — продолжать работу, даже если дочерняя сопрограмма выходит из строя. Это особенно полезно, если работает несколько дочерних элементов, которые не должны влиять друг на друга.
Вместо сбоя при сбое дочерней сопрограммы (как в вашем примере), она распространяет исключение на своего родителя. В вашем случае это сопрограмма, запущенная в viewModelScope
. Это эффективно обходит ваш блок try/catch и напрямую приводит к сбою viewModelScope
. В качестве примечания: если бы вы выбросили исключение непосредственно в supervisorScope
вместо вложенного launch
, supervisorScope
потерпит неудачу, и исключение будет перехвачено блоком try/catch. Специально занимаются только с неудавшимися детьми supervisorScope
.
В качестве альтернативы блоку try/catch исключения в сопрограммах могут эффективно обрабатываться с помощью CoroutineExceptionHandler:
val handler = CoroutineExceptionHandler { _, e ->
println("Exception caught: $e")
}
viewModelScope.launch(handler) {
supervisorScope {
launch {
throw Exception()
}
}
}
Теперь, когда supervisorScope
передает исключение родительскому элементу launch
, определенный там обработчик исключений перехватывает исключение и предотвращает отмену сопрограммы, тем самым предотвращая сбой вашего приложения. То же самое будет работать и при использовании coroutineScope
вместо supervisorScope
.
Подробнее об обработке исключений сопрограмм читайте в документации: https://kotlinlang.org/docs/Exception-handling.html
Похоже, это связано с тем, как runBlocking
обрабатывает неудавшийся дочерний элемент по сравнению с распространяемым исключением. Однако я не знаю, почему может измениться код возврата, поскольку оба варианта приводят к принудительному завершению работающей программы. Вам следует задать это как новый вопрос.
Разница между обработкой исключений в coroutineScope
и supervisorScope
заключается в распространении дочерних ошибок.
В coroutineScope
все дочерние сопрограммы делегируют обработку своих исключений родительской сопрограмме. Когда один из них встречает исключение, отличное от CancellationException
, он отменяет своего родителя с этим исключением. Вот почему try
с coroutineScope
работает — coroutineScope
просто выбрасывает исключение из своей дочерней сопрограммы.
В supervisorScope
неудача ребенка не передается родителю. Каждый ребенок должен самостоятельно обрабатывать свои исключения с помощью CoroutineExceptionHandler
. Если этого не происходит, исключение становится неперехваченным и продолжается до тех пор, пока не найдет родительскую сопрограмму с установленной CoroutineExceptionHandler
. Если CoroutineExceptionHandler
не найдено, исключение будет обработано Thread.defaultUncaughtExceptionHandler
или, если оно не установлено, просто выведено в поток вывода ошибок.
Первый пример:
supervisorScope
не знает о сбое дочерней сопрограммы, поэтому try...catch
не будет работать. Чтобы обработать исключение, установите CoroutineExceptionHandler
либо на viewModelScope.launch
, либо на внутренний launch
.
Дело 1:
launch
в supervisorScope
не делегирует обработку исключений и не обрабатывает само исключение, исключение не перехватывается. runBlocking
не имеет CoroutineExceptionHandler
, поэтому механизм обработки неперехваченных исключений Thread
обрабатывает его.
Случай 2:
launch
делегирует обработку исключений runBlocking
, поскольку он не контролируется. CoroutineExceptionHandler
в данном случае не поможет, потому что это не неперехваченное исключение. Исключение не обрабатывается runBlocking
, код выхода — 1.
Отличное объяснение. Изучена разница между неперехваченными и делегированными исключениями в сопрограммах.
Спасибо, что нашли время ответить на мой вопрос. Я обновил исходный вопрос, добавив дополнительные наблюдения после запуска кода в IntelliJ, чтобы передать исключение своему родителю.