Изучение программирования на 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.

Обязательно поделитесь, если у вас есть другой опыт и рекомендации.