Scala имеет тип Try для функциональной обработки исключений. Я мог бы получить голову, используя этот тип, используя отличный
Путеводитель Неофита по Скале от Дэниела Вестхайде . Этот пост будет копировать этот тип, используя
Котлин .
Фон
Рассмотрим простую функцию, которая принимает две строки, преобразует их в целое число, а затем делит их (пример на основе скалярной
Попробуй ):
1
2
3
4
5
|
fun divide(dividend: String, divisor: String): Int { val num = dividend.toInt() val denom = divisor.toInt() return num / denom } |
Ответственность за то, чтобы все исключения, распространяемые из этой реализации, обрабатывались надлежащим образом с использованием механизма обработки исключений Java / Kotlin:
1
2
3
4
5
6
7
|
try { divide( "5t" , "4" ) } catch (e: ArithmeticException) { println( "Got an exception $e" ) } catch (e: NumberFormatException) { println( "Got an exception $e" ) } |
Моя цель с кодом «Try» будет состоять в том, чтобы преобразовать «деление» во что-то похожее на это:
1
2
3
4
5
|
fun divideFn(dividend: String, divisor: String): Try<Int> { val num = Try { dividend.toInt() } val denom = Try { divisor.toInt() } return num.flatMap { n -> denom.map { d -> n / d } } } |
Вызывающая сторона этого варианта функции «деления» не будет иметь исключение для обработки через блок try / catch, вместо этого она получит исключение в виде значения, которое оно может проанализировать и при необходимости действовать.
1
2
3
4
5
|
val result = divideFn( "5t" , "4" ) when(result) { is Success -> println( "Got ${result.value}" ) is Failure -> println( "An error : ${result.e}" ) } |
Котлинская реализация
Тип «Try» имеет две реализации, соответствующие пути «Success» или «Failure», и реализованы как запечатанный класс следующим образом:
1
2
3
4
5
|
sealed class Try<out T> {} data class Success<out T>(val value: T) : Try<T>() {} data class Failure<out T>(val e: Throwable) : Try<T>() {} |
Тип «Success» охватывает успешный результат выполнения, а тип «Failure» — любое исключение, выбрасываемое из выполнения.
Итак, теперь, чтобы добавить немного мяса к этому, мой первый тест должен вернуть один из этих типов, основанный на чистой и исключительной реализации, по следующим направлениям:
01
02
03
04
05
06
07
08
09
10
|
val trySuccessResult: Try<Int> = Try { 4 / 2 } assertThat(trySuccessResult.isSuccess()).isTrue() val tryFailureResult: Try<Int> = Try { 1 / 0 } assertThat(tryFailureResult.isFailure()).isTrue() |
Этого можно достичь с помощью «объекта-компаньона» в Kotlin, аналогичного статическим методам в Java, который возвращает либо тип Success, либо тип Failure, основанный на выполнении лямбда-выражения:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
sealed class Try<out T> { ... companion object { operator fun <T> invoke(body: () -> T): Try<T> { return try { Success(body()) } catch (e: Exception) { Failure(e) } } } ... } |
Теперь, когда вызывающая сторона имеет тип «Try», они могут проверить, является ли это типом «Success» или «Failure», используя выражение «when», как раньше, или используя методы «isSuccess» и «isFailure», которые делегированы для подтипов, как это:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
sealed class Try<out T> { abstract fun isSuccess(): Boolean abstract fun isFailure(): Boolean } data class Success<out T>(val value: T) : Try<T>() { override fun isSuccess(): Boolean = true override fun isFailure(): Boolean = false } data class Failure<out T>(val e: Throwable) : Try<T>() { override fun isSuccess(): Boolean = false override fun isFailure(): Boolean = true } |
в случае сбоя вызывающему абоненту может быть возвращено значение по умолчанию, что-то вроде этого в тесте:
1
2
3
4
5
6
7
8
9
|
val t1 = Try { 1 } assertThat(t1.getOrElse( 100 )).isEqualTo( 1 ) val t2 = Try { "something" } .map { it.toInt() } .getOrElse( 100 ) assertThat(t2).isEqualTo( 100 ) |
снова реализуется путем делегирования подтипам:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
sealed class Try<out T> { abstract fun get(): T abstract fun getOrElse( default : @UnsafeVariance T): T abstract fun orElse( default : Try< @UnsafeVariance T>): Try<T> } data class Success<out T>(val value: T) : Try<T>() { override fun getOrElse( default : @UnsafeVariance T): T = value override fun get() = value override fun orElse( default : Try< @UnsafeVariance T>): Try<T> = this } data class Failure<out T>(val e: Throwable) : Try<T>() { override fun getOrElse( default : @UnsafeVariance T): T = default override fun get(): T = throw e override fun orElse( default : Try< @UnsafeVariance T>): Try<T> = default } |
Однако самое большое преимущество возврата типа «Try» заключается в цепочке дальнейших операций над этим типом.
Сцепление с картой и flatMap
Операции «map» передается лямбда-выражение для преобразования значения в какой-либо форме — возможно, даже в другой тип:
1
2
3
4
5
|
val t1 = Try { 2 } val t2 = t1.map({ it * 2 }).map { it.toString()} assertThat(t2).isEqualTo(Success( "4" )) |
Здесь число удваивается, а затем преобразуется в строку. Если начальная попытка была «Отказ», то окончательное значение просто вернет «Отказ» в соответствии с этим тестом:
1
2
3
4
5
6
7
|
val t1 = Try { 2 / 0 } val t2 = t1.map({ it * 2 }).map { it * it } assertThat(t2).isEqualTo(Failure<Int>((t2 as Failure).e)) |
Реализация «карты» довольно проста:
01
02
03
04
05
06
07
08
09
10
|
sealed class Try<out T> { fun <U> map(f: (T) -> U): Try<U> { return when ( this ) { is Success -> Try { f( this .value) } is Failure -> this as Failure<U> } } } |
flatmap, с другой стороны, принимает лямбда-выражение, которое возвращает другой тип «Try» и сглаживает результат обратно в тип «Try», в соответствии со строками этого теста:
1
2
3
4
5
6
7
|
val t1 = Try { 2 } val t2 = t1 .flatMap { i -> Try { i * 2 } } .flatMap { i -> Try { i.toString() } } assertThat(t2).isEqualTo(Success( "4" )) |
Реализовать это тоже просто, по следующим направлениям:
1
2
3
4
5
6
7
8
|
sealed class Try<out T> { fun <U> flatMap(f: (T) -> Try<U>): Try<U> { return when ( this ) { is Success -> f( this .value) is Failure -> this as Failure<U> } } } |
Методы «map» и «flatMap» являются мощными инструментами этого типа, позволяющими объединять в цепочку сложные операции, ориентируясь на счастливый путь.
Вывод
Try — это мощный тип, позволяющий функционально обрабатывать исключения в коде. У меня есть соломенная реализация с использованием Kotlin, доступная в моем репозитории github здесь — https://github.com/bijukunjummen/kfun
Смотрите оригинальную статью здесь: Kotlin — Попробуйте тип для функциональной обработки исключений
Мнения, высказанные участниками Java Code Geeks, являются их собственными. |