Изучение программирования на Kotlin
Kotlin Flow: императивный или декларативный обработчик исключений?
Обоснование того, когда использовать оператор try-catch или catch
В документации Kotlin Asynchronously Flow показано, что можно использовать императивный способ (try-catch-finally) и декларативный способ (оператор catch и onCompletion) для обработки исключения.
Однако об этом говорится ниже.
Мы не выступаем за какой-либо конкретный подход и считаем, что оба варианта допустимы и должны выбираться в соответствии с вашими предпочтениями и стилем кода.
Хотя вышеизложенное дает одну гибкость, ниже мое предложение с приведенным обоснованием.
Perfer декларативный обработчик исключений вместо императивного
Ниже показан простой способ обязательного перехвата исключения.
fun main() = runBlocking<Unit> { try { (1..3).asFlow().collect { value -> check(value <= 1) { "Crash on $value" } println("Got $value") } } catch (e: Throwable) { println("Caught $e") } finally { println("Done") } }
Результат, как показано ниже
Got 1 Caught java.lang.IllegalStateException: Crash on 2 Done
Хотя это работает отлично, если мы декларативно используем catch
и onCompletion
, как показано ниже
fun main() = runBlocking<Unit> { (1..3).asFlow().onEach { value -> check(value <= 1) { "Crash on $value" } println("Got $value") }.catch { e -> println("Caught $e") }.onCompletion { println("Done") }.collect() }
это намного лучше как
- Он инкапсулирует
catch
иfinally
лучше - Он также имеет меньший уровень отступа.
Может быть даже лучше!
Если мы поменяем местами catch
и onCompletion
, это приведет к тому, что catch
также получит исключение, которое также возникнет в onCompletion
.
fun main() = runBlocking<Unit> { (1..3).asFlow().onEach { value -> check(value <= 1) { "Crash on $value" } println("Got $value") }.onCompletion { println("Done") throw IllegalStateException("Throw during onCompletion") }.catch { e -> println("Caught $e") }.collect() }
Конечно, если мы действительно хотим иметь еще один finally
, мы все равно можем это сделать
// Not likely someone will do this... but an interesting bit fun main() = runBlocking<Unit> { (1..3).asFlow().onEach { value -> check(value <= 1) { "Crash on $value" } println("Got $value") }.onCompletion { println("Done") throw IllegalStateException("Throw during onCompletion") }.catch { e -> println("Caught $e") }.onCompletion { println("Done again :P") }.collect() }
Если мы хотим сделать это в обязательном порядке, это должно быть так, как показано ниже… arggg…
// Ugly nested try-catch. fun main() = runBlocking<Unit> { try { (1..3).asFlow().collect { value -> check(value <= 1) { "Crash on $value" } println("Got $value") } } catch (e: Throwable) { println("Caught $e") } finally { try { println("Done") throw IllegalStateException("Throw during onCompletion") } catch (e: Throwable) { println("Caught $e") } finally { println("Done again") } } }
Означает ли это, что мы никогда не должны использовать императивную обработку исключений при использовании Kotlin Flow?
Императив все еще нужен, когда
Это за пределами Kotlin Flow
Если у вас есть код, который представляет собой не только Kotlin Flow, но и другой код помимо Kotlin Flow, который вы хотели бы объединить, то императив по-прежнему актуален.
fun main() = runBlocking<Unit> { try { (1..3).asFlow().collect { value -> check(value <= 1) { "Crash on $value" } println("Got $value") } doSomeOtherThingThatMightThrow() } catch (e: Throwable) { println("Caught $e") } finally { println("Done") } }
Когда он вылетает на терминал
Оператор catch
может перехватить только исключение, которое происходит перед ним в цепочке. В отличие от onCompletion
, он не перехватывает исключение, возникшее в приведенной ниже цепочке.
Если у нас есть оператор терминала, у нас не может быть catch
после него.
fun main() = runBlocking<Unit> { try { (1..3).asFlow().reduce { a, b -> check(a <= 1) { "Crash on $a" } a + b } } catch (e: Throwable) { println("Caught $e") } finally { println("Done") } }
Следовательно, если исключение происходит на терминале, его нужно будет перехватить внешним try-catch
.
Подводя итог, мое обоснование, как показано ниже
- Используйте
catch
иonCompletion
для Kotlin Flow, если все, что нужно поймать, находится в цепочке Kotlin Flow и до терминала. - Если у нас есть код помимо Kotlin Flow, который нужно перехватить, используйте
try-catch
. - Если у нас есть терминал Kotlin Flow, который может генерировать исключение, используйте
try-catch
.
Обязательно поделитесь, если у вас есть другой опыт и рекомендации.