Kotlin Option与Either及Result实现异常处理详解

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

Kotlin Option与Either及Result实现异常处理详解

RikkaTheWorld   2022-12-13 我要评论

1. 异常处理概述

空指针引用 NPE 是编程语言最常见的异常,数十年来无处不在的和程序打交道,在Java中,我们使用“防御式编程”来处理空数据,这导致了代码不美观,比如增加了缩进、嵌套。

Kotlin是如何解决这个问题的呢?Kotlin 使用显示的 ? 表示一个数据类型是否可空,如果参数的传递是非空的,那么我们就无须去处理它。

当然不可能所有时候参数都是非空的,我们依然要处理空值的问题,最常见的就是业务空值,举一个例子,下面代码用于求一个整数数列的平均值:

fun mean(list: List<Int>): Double = when {
    list.isEmpty() -> // 返回一个值
    else -> list.sum().toDouble() / list.size
}

如果传入的列表非空,我们可以返回(列表所有整数的和 / 列表长度 ) -> 列表平均值。

但如果当传入的列表是一个空列表, 我们应该返回什么?我们可能会有下面几种想法:

返回 0

很明显这是有问题的,因为整数列表的平均值可能是0,所以返回0的话,你能让调用者知道这个是正常的值,还是因为输入数据的异常而导致的0呢?返回 Double.NaN

没什么问题,因为这个是一个 Double 值。

但这也仅仅是针对这个函数没问题, 设想这个函数的返回不是 Double, 而是 Int 类型, Int 类型可没有 NaN 这种值呀抛出异常 throw Exception("Empty list!")

这个解决方案不是很好,因为它产生的麻烦比它解决的问题要多,原因如下:

①: 异常通用于提示错误的结果,但这里本质上是没有错误的,没有输出结果的原因是没有输入数据

②:这里应该抛出什么异常,是通用的还是自定义的?

③:这个函数不再是一个纯函数,其他函数组合它使用时,必须要使用 try - catch 形式, 这是一种现代的goto形式返回 null

在通用编程语言中,返回 null 值是最糟糕的解决方案, 看看 Java 语言里这样做的后果:

①:强制调用者处理测试结果为 null 的情况

②:如果使用装箱,则该代码会崩溃并出现 NPE, 因为无法将 null 引用拆箱变为基本数据类型

③:和抛异常一样,该函数无法再组合

④:有潜在的问题,如果调用者忘记处理 null 结果,则该函数的引用链上,任意位置都可能会产生 NPE如果异常,返回一个指定的默认值 default

这就跟一开始一样了, 我们无法区分 default 和 真正的结果。

可以看出来,仅仅一行的代码,可以产生很低的下限。 一系列思考下来,我们了解了这个问题的核心本质:我们该如何处理一个异常结果或者可选结果?

放心的是,编程语言库的设计者们也对这个问题进行思考,Java8的一个特性 Optional 就是为了解决这个问题, Kotlin 也有与之对应的 Result,为了更好的了解它们的本质,通过学习 Option、 Either、 Result,我们了解如何解决这样的问题。

在介绍之前,我们了解下一个具体的问题场景,定义一个数据类用于表示用户:

data class Toon(
    val firstName: String, // 首名字
    val lastName: String, // 姓氏
    val email: String? = null  // email
)
// 定义好一份数据
val toonMap: Map<String, Toon> = mapOf(
    "Joseph" to Toon("Joseph","Joestar", "joseph@jojo.com"),
    "Jonathan" to Toon("Jonathan","Joestar"),
    "Jotaro" to Toon("Jotaro","Kujo", "jotaro@jojo.com")
)

其中 email 是可选参数, 不传也是正常的。 现在假定外部有人使用该 map,他可能会遇到下面的情况:

虽然 Kotlin 有 ? 可以帮助我们判断参数是否为空,然后强制处理以避免这种情况。

但是在极端情况下 ---- 调用代码是 Java 且开发者没有做数据判空, 那这样的代码下限是很低的,是有较大概率出错或者崩溃的。而且就算做了判空,也可能会因为多加了各种 if..else 语句,而让代码变得臃肿和不美观。

我们来实现一个 Option 来处理这种问题。

2. Option

建立一个 Option 模型,实现的目标处理链如下:

sealed class Option<out A> {
    abstract fun isEmpty(): Boolean
    internal object None : Option<Nothing>() {
        override fun isEmpty(): Boolean = true
        override fun equals(other: Any?): Boolean = other === null
        override fun hashCode(): Int = 0
    }
    internal data class Some<out A>(internal val value: A) : Option<A>() {
        override fun isEmpty(): Boolean = false
    }
    companion object {
        operator fun <A> invoke(a: A? = null): Option<A> =
            when (a) {
                null -> None
                else -> Some(a)
            }
    }
}

我们实现了一个很基础的 Option 类, 它目前其实没有什么作用,就是判断传入值是否为空而已。我们还需要拓展一些功能。

2.1 从 Option 提取值

Optional 那样, 创建一个 getOrElse 函数:如果 Option 值不为空,则返回值, 否则返回传入的默认值:

    fun getOrElse(default: @UnsafeVariance A): A = when(this) {
        is None -> default
        is Some -> value
    }

我们可以运用如下:

    fun max(list: List<Int>): Option<Int> = Option(list.max())
    val max1 = max(listOf(3, 1, 5, 2, 5)).getOrElse(0)  // 等于 7
    val max2 = max(listOf()).getOrElse(0)   // 等于 0

看起来还不错,但假设我们的调用者在调用 getOrElse 时,传的不是 0 ,而是:

    fun getDefault(): Int = throw RuntimeException()
    val max1 = max(listOf(3, 1, 7, 2, 5)).getOrElse(getDefault())
    val max2 = max(listOf()).getOrElse(getDefault())

那么这段代码会出现什么问题? 你会认为 max1 能输出7, 然后 max2 抛出异常么?

答案是这段代码会直接在一开始抛出异常,因为 Kotlin 是严格的静态编程语言,在执行函数之前,无论是否需要都会处理函数参数,这就意味着 getOrElse 的参数在任何情况下都会被处理,无论是在 Some 还是 None 中调用它。如果传参是一个值,这是无关紧要的,但是传参是一个函数时,这就会有很大的区别,任何情况下都会调用 getDefault 函数,因此这段代码的第一行就抛出异常了。

这显然不是我们想要的结果。 为了解决这个问题, 我们可以使用惰性计算,即让 throw Exception 在需要时被调用:

    fun getOrElse(default: () -> @UnsafeVariance A): A = when (this) {
        is None -> default()
        is Some -> value
    }
...
    val max1 = max(listOf(3, 1, 5, 2, 5)).getOrElse(::getDefault)  // 7
    val max2 = max(listOf()).getOrElse(::getDefault)   // 抛异常

2.2 添加 map 函数

仅仅有 getOrElse 可能还是不够的,List中最重要的一个函数就是 map 函数,考虑到一个向列表一样最多包含一个元素的 Option,也可以应用同样的函数。

添加一个 map函数,从 Option<A> 转化成 Option<B>

    fun <B> map(f:(A) -> B): Option<B> = when(this) {
        is None -> None
        is Some -> Some(f(this.value))
    }

2.3 处理 Option 组合

从 A 到 B 的函数并不是安全编程中最常用的函数, 因为 map 函数的入参是: (A)-> B , 但是返回的却是一个 Option<B>,这可能会难以理解,而且需要的额外的工作:包装 Some。

为了减少中间结果,会有更多的使用方法从 (A) -> Option<B>,在 List 类中也有类似的操作, 那就是 flatmap打平。

我们也创建一个 flatmap 函数来扩展 Option,:

    fun <B> flatmap(f: (A) -> Option<B>): Option<B> = when (this) {
        is None -> None
        is Some -> f(this.value)
    }

正如需要一种方法来映射一个返回 Option 的函数(flatmap),也需要一个 getOrElse 的版本来返回一个 Option 的默认值。 代码如下:

fun orElse(default: () -> Option<@UnsafeVariance A>): Option<A> = map {<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E--> this }.getOrElse(default)

通过 map{ this } 可以产生一个 Option<Option<A>> ,通过 getOrElse 方法拿到里面那一层, 这么写相较于直接返回更为优雅

接下来编写一个 filter 函数,筛选出不满足谓词表达式的所有函数:

// 比较智能的实现, 因为之前已经定义过 flatmap, 所以可以直接组合
fun filter(p: (A) -> Boolean): Option<A> = flatmap { if (p(it)) this else None }

2.4 Option用例

在 Java 的 Optional 中有个方法叫 ifPresent() , 表示该 Optional 中是否包含值, 那对于 Option 来说,这个方法应该叫 isSome(), 如果实现了这个方法,那么开发者就可以在使用 Option 的值之前,使用这个方法查询 Option 是否有值可用。如下代码:

if (a.isSome()) {
   // 当 a 有值的操作
} else {
   // 当 a 没有值的操作
}

等等! 这个方法和我自己判断 a 是否为空 效果是一样的,那既然是一样的,为什么还要把值封装到 Option 里面去呢?

所以, isSome() 并不是测试的最佳方法,它和提前测试null值引用唯一的区别就是:如果先前忘记判断异常值,那么在运行的时候会抛出 IllegalStateException 或 NoSuchElement 等异常,而不是 NPE。

使用 Option 最佳的方式就是组合去使用。为此,必须为所有的用例创建所有必要的函数, 这些用例可以在测试出该值非null后将如何处理,他应该有如下操作:

  • 将这个值作为另一个函数的输入
  • 对值添加作用
  • 如果不是空值,就是用这个值,否则使用默认值来应用函数操作

第一个和第三个之前创建的函数已经能够做到了,第二点以后会讲到。

有一个例子,如果使用 Option 类来改变使用映射的方式,以 Toon 为例,我们在 Map 上实现一个扩展函数,以便在查询给定键时,返回一个 Option:

data class Toon(
    val firstName: String, // 首名字
    val lastName: String, // 姓氏
    val email: Option<String> = Option(null)  // 可选email
) {
    companion object {
        operator fun invoke(firstName: String, lastName: String, email: String? = null) =
            Toon(firstName, lastName, Option(email))
    }
}
// 扩展函数来实现前检查模式, 以避免返回空引用
fun <K,V> Map<K,V>.getOption(key: K) = Option(this[key])
fun main() {
    val toons: Map<String, Toon> = mapOf(
        "Joseph" to Toon("Joseph", "Joestar", "joseph@jojo.com"),
        "Jonathan" to Toon("Jonathan", "Joestar"),
        "Jotaro" to Toon("Jotaro", "Kujo", "jotaro@jojo.com")
    )
    val joseph = toons.getOption("Joseph").flatmap { it.email }
    val jonathan = toons.getOption("Jonathan").flatmap { it.email }
    val jolyne = toons.getOption("Jolyne").flatmap { it.email }
    print(joseph.getOrElse { "No data" })
    print(jonathan.getOrElse { "No data" })
    print(jolyne.getOrElse { "No data" })
}

// 最终打印:
joseph@jojo.com
No data
No data

在这个过程中,我们可以看到组合 Option 的操作来达到目的而不需要冒着 NPE 的风险,由于 Kotlin 的便捷性,即使不用 Option,我们也可以使用 Kotlin 封装好的代码来实现

    val joseph = toons["Joseph"]?.email ?: "No data"
    val jonathan = toons["Jonathan"]?.email ?: "No data"
    val jolyne = toons["Jolyne"]?.email ?: "No data"
    ...

可以看到 Kotlin 风格更加方便,但是打印值却如下:

Some(value=joseph@jojo.com)
Option$None@0
No data

第二行是 None, 因为 jonathan 没有 email, 第三行 No Data 是因为 jolyne 不在映射中,需要一种方法来区分这两种情况,但无论使用可空类型还是 Option, 都无法区分。这个问题下面学习的 Either 和 Result 中会解决掉。

2.5 其他的组合方法

如果决定在代码中使用 Option ,可能会产生一些巨大的后果,因为代码一写出来就已经过时了。当出现了一些场景,当前 api 不满足,我们需要去重新编写库函数吗?得益于 Kotlin 的扩展函数,我们可以通过组合的方式,来构建原来库中没有的api。

练习1. 定义一个 lift 函数, 该函数的参数是 (A) -> B , 并返回一个 (Option<A>) -> Option<B>

解决方法很简单,可以在包级别声明:

fun <A, B> lift(f: (A) -> B): (Option<A>) -> Option<B> = {<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E--> it.map(f) }

这样,我们可以用其写一些值函数,以此生成目标的 Option版本的函数,例如, 将字母转化为大写的函数: String.toUpperCase 的 Option 版本可以这样实现:

val upperOption: (Option<String>) -> Option<String> = lift(String::toUpperCase)

练习2. 前面的 lift 函数如果抛出了异常,那么就有不确定性,例如f函数抛出了异常,lift就无效了,编写一个对抛出异常函数仍然有效的函数。

只需要写一个 try catch 即可:

fun <A, B> lift(f: (A) -> B): (Option<A>) -> Option<B> =
    {
        try {
            it.map(f)
        } catch (e: Exception) {
            Option()
        }
    }

可能还需要将函数 (A) -> B ,生成函数 (A) -> Option<B>,可以用同样的方法:

fun <A, B> hLift(f: (A) -> B): (A) -> Option<B> = {
    try {
        Option(it).map(f)
    } catch (e: Exception) {
        Option()
    }
}

但是这种转化其实有点问题,因为产生了异常,我们把异常给“掩盖”了,实际上我们应该要让外部的调用者知道有这个异常。下面的两章会解决这个问题。

练习3. 编写一个函数 map2,该函数一个 Option<A> , 一个 Option<B> 和一个 从 (A, B) 到 C的柯里化形式的函数作为参数,然后返回一个 Option<C>

下面是使用 flatmap 和 map 的解决方法,理解这个模式很重要,以后会经常遇到,下篇文章将重点讲述这一内容:

fun <A, B, C> map2(oa: Option<A>, ob: Option<B>, f: (A) -> (B) -> C): Option<C> =
    oa.flatmap { a -> ob.map { b -> f(a)(b) } }

通过规律甚至可以写出 map3 、 map4 …

2.6 Option 小结

  • 用可选数据来表示函数意味数据可能存在或不存在, Some 表示存在, None 表示不存在
  • 用 null 指针表示数据的确实不切实际而且很危险,字面值和空列表是表示数据确实的其他方法,但是他们组合的不好
  • Option 数据类型是一种表示可选数据的更好方式
  • 将 map、flatmap 高阶函数应用到 Option 上,可以方便的组合 Option
  • Option 是有局限性的,比如不能区分数据不存在还是异常等其他情况,其次,虽然 Option 可以表示产生异常的计算结果,但是它没有关于发生异常的所有信息

3. Either

上面说到 Option 作为数据处理类型,对数据缺失问题不是完美的,时机就是在出现异常的时候。为什么呢?表面上的原因是: Option 只返回一个数据, 这个数据要么是空,要么是正常值, 当出现异常时,可能会返回提前设置的默认值,也可能会返回空。

所以,如果 Option 有一个升级版, 返回两种不同类型,有异常时,返回异常信息,没异常时,返回正常信息,这样出现了异常,调用者也可以知道 ----- 于是就有了 Either

Either 类型

因为 Kotlin、Java 返回值只能用一个数据类型,所以我们的类型既可以返回错误信息、也可以返回正常值,就要将其塞到一个数据类型里面去, 例如 Map映射 、 一个新的数据Bean、一个 Pair。

注:像Kotlin这种强类型语言必须借助包装结构,像 Python 这种直接用字典就好了。

来看下 Either<Left, Right> 实现,基于国际惯例,Left是异常,Right是正常。 再 Option 的实现,我们顺带把一些基本的 map、 flatmap、getOrElse 、orElse 也实现进去:

sealed class Either<E, out A> {
    /**
     * Either<E, A> -> Either<E, B>
     */
    abstract fun <B> map(f: (A) -> B): Either<E, B>
    /**
     * (A) -> Either<E, B>
     */
    abstract fun <B> flatmap(f: (A) -> Either<E, B>): Either<E, B>
    fun getOrElse(default: () -> @UnsafeVariance A): A = when (this) {
        is Right -> this.value
        is Left -> default()
    }
    fun orElse(default: () -> Either<E, @UnsafeVariance A>): Either<E, A> = map { this }.getOrElse(default)
    /**
     * 错误信息
     */
    internal class Left<E, out A>(internal val value: E) : Either<E, A>() {
        override fun <B> map(f: (A) -> B): Either<E, B> = Left(value)
        override fun <B> flatmap(f: (A) -> Either<@UnsafeVariance E, B>): Either<E, B> = Left(value)
    }
    /**
     * 正常信息
     */
    internal class Right<E, out A>(internal val value: A) : Either<E, A>() {
        override fun <B> map(f: (A) -> B): Either<E, B> = Right(f(value))

        override fun <B> flatmap(f: (A) -> Either<E, B>): Either<E, B> = f(value)
    }
    companion object {
        fun <E, A> left(value: E): Either<E, A> = Left(value)
        fun <E, A> right(value: A): Either<E, A> = Right(value)
    }
}

Either 类很有用,而且已经完美融入到了 Scala 语言中作为常规的数据而使用。

但是 Either 没有达到理想的效果: 在没有可用值时,不知道会发生什么。

此时会得到默认值,但是却不知道这个默认值是计算出来的,还是因为异常而产生的结果, 它解决了 Option 不能给出错误信息的问题,但未能解决 Option 不能区分计算结果的问题

4. Result

其实把上面的问题总结一下,可以知道我们想拥有一个类型,可以明确的告诉我们计算结果:

有值无值计算过程中出现异常, 能给出异常信息

Option 能满足 1(Some) 和 2(None)

Either 能满足 1(Right) 和 3(Left)

下面我们创建的 Result ,将是完美解决上述所有问题的终极方案。 而且 Kotlin 中也有 Result ,但是这个原生的 Reuslt 和 上面定义的 Option、 Either 差不多,并不是完美版,源码很简单,读者一看便懂。虽然也够日常开发使用,但是为了优化数据结构,我打算基于其创作一版更好的 Result。

4.1 Result 类型

Reult 使用 Success 表示有值,使用 Failure 表示异常, 使用 Empty 表示无值。

并且对 map 、flatmap 函数进行了保护,是一个安全的版本,使用者更放心,我们才更安心。

sealed class Result<out A> : Serializable {
    abstract fun <B> map(f: (A) -> B): Result<B>
    abstract fun <B> flatMap(f: (A) -> Result<B>): Result<B>
    internal class Success<out A>(internal val data: A) : Result<A>() {
        override fun <B> map(f: (A) -> B): Result<B> = try {
            Success(f(data))
        } catch (e: RuntimeException) {
            Failure(e)
        } catch (e: Exception) {
            Failure(RuntimeException(e))
        }
        override fun <B> flatMap(f: (A) -> Result<B>): Result<B> = try {
            f(data)
        } catch (e: RuntimeException) {
            Failure(e)
        } catch (e: Exception) {
            Failure(RuntimeException(e))
        }
    }
    internal object Empty : Result<Nothing>() {
        override fun <B> map(f: (Nothing) -> B): Result<B> = Empty
        override fun <B> flatMap(f: (Nothing) -> Result<B>): Result<B> = Empty
    }
    internal class Failure(val exception: RuntimeException) : Result<Nothing>() {
        override fun <B> map(f: (Nothing) -> B): Result<B> = Failure(exception)
        override fun <B> flatMap(f: (Nothing) -> Result<B>): Result<B> = Failure(exception)
    }
    /**
     * 没有 / 错误 返回一个 default, 不能为空, 如果需要空, 使用 [getOrNull]
     */
    fun getOrElse(defaultValue: () -> @UnsafeVariance A): A = when (this) {
        is Success -> this.data
        else -> defaultValue()
    }
    /**
     * 没有 / 错误 返回一个 Result-default, 不能为空
     */
    fun orElse(defaultValue: () -> Result<@UnsafeVariance A>): Result<A> = when (this) {
        is Success -> this
        else -> try {
            defaultValue()
        } catch (e: RuntimeException) {
            failure(e)
        } catch (e: Exception) {
            failure(RuntimeException(e))
        }
    }
    companion object {
        operator fun <A> invoke(a: A? = null): Result<A> = when (a) {
            null -> Failure(NullPointerException())
            else -> Success(a)
        }
        operator fun <A> invoke(): Result<A> = Empty
        fun <A> failure(message: String): Result<A> = Failure(IllegalStateException(message))
        fun <A> failure(exception: RuntimeException): Result<A> = Failure(exception)
        fun <A> failure(exception: Exception): Result<A> = Failure(IllegalStateException(exception))
    }
}

这样再运用到之前的例子:

// 改变原有数据结构:
data class Toon(
    val firstName: String, // 首名字
    val lastName: String, // 姓氏
    val email: Result<String>
) {
    companion object {
        operator fun invoke(firstName: String, lastName: String, email: String? = null) =
            Toon(firstName, lastName, Result(email))
        operator fun invoke(firstName: String, lastName: String) =
            Toon(firstName, lastName, Result.Empty)
    }
}
// 创建一个 Result 版本的getMap
fun <K, V> Map<K, V>.getResult(key: K) = when {
    this.containsKey(key) -> Result(this[key])
    else -> Result.Empty
}
fun main() {
    val toonMap: Map<String, Toon> = mapOf(
        "Joseph" to Toon("Joseph", "Joestar", "joseph@jojo.com"),
        "Jonathan" to Toon("Jonathan", "Joestar"),
        "Jotaro" to Toon("Jotaro", "Kujo", "jotaro@jojo.com")
    )
    val toon = getName()
        .flatMap(toonMap::getResult)
        .flatMap(Toon::email)
    print(toon)
}
fun getName(): Result<String> = try {
    validate(readLine())
} catch (e: IOException) {
    Result.failure(e)
}
fun validate(name: String?): Result<String> = when {
    name?.isNotEmpty() ?: false -> Result(name)
    else -> Result.failure(IOException())
}

当我们在输入: Joseph、Jonathan、Josuke、空字符串时,会有如下结果:

// Joseph
Result$Success(joseph@jojo.com)
Result$Empty
Result$Empty
Result$Failure(java.io.IOException)

读者可能认为缺少了点什么东西,因为没有区分两种不同的空案例。 但事实并非如此,可选数据不需要错误信息。 如果读者认为需要信息,则数据不是可选的

4.2 Result 高级处理

4.2.1 使用断言

实际场景中,我们会判断 Result 中的值是否符合断言(条件),匹配后才能使用这个值。

所以我们可以创建一个函数, 传入一个 predicate 谓词函数,进行条件判定,如果成功返回 Result,失败返回 failure,或者指定的 message:

    fun filter(p: (A) -> Boolean): Result<A> = flatMap {
        if (p(it)) this
        else failure("Condition not matched")
    }
    fun filter(message: String, p: (A) -> Boolean): Result<A> = flatMap {
        if (p(it)) this
        else failure(message)
    }

组合使用了 flatmap, flatmap可以帮我们处理 Result 的各个类型的情况,所以我们不用再判断 Result 的类型从而去处理各种情况, 可以说是十分好用,其实 Result 的实际使用,都离不开 map 和 flatmap。

我们还可以使用断言去做别的事情,例如传入一个条件,条件符合就返回 true, 反之返回 false,代码如下:

fun exists(p: (A) -> Boolean): Boolean = map(p).getOrElse(false)

4.2.2 应用作用

到目前为止,我们除了去 get 这个 Result 中的值,也没有做其他事情。 我们可以让外部去应用这个值,产生做用, 就像 List forEach 函数那样去操作每个元素。

abstract fun forEach(effect: (A) -> Unit)
// Success 实现
override fun forEach(effect: (A) -> Unit) = effect(data)
// Empty 实现
override fun forEach(effect: (Nothing) -> Unit) = Unit
// Failure 实现
override fun forEach(effect: (Nothing) -> Unit) = Unit

上面的实现不是很适合 Result,因为一般我们会对 Failure 做一些操作。

为此我们实现一个方法, 他必须能同时处理 Failure、 Empty:

    abstract fun forEachOrElse(
        onSuccess: (A) -> Unit,
        onFailure: (java.lang.RuntimeException) -> Unit,
        onEmpty: () -> Unit
    )
// Success 实现
        override fun forEachOrElse(
            onSuccess: (A) -> Unit,
            onFailure: (java.lang.RuntimeException) -> Unit,
            onEmpty: () -> Unit
        ) = onSuccess(data)
// Empty 实现:
        override fun forEachOrElse(
            onSuccess: (Nothing) -> Unit,
            onFailure: (java.lang.RuntimeException) -> Unit,
            onEmpty: () -> Unit
        ) = onEmpty()    
// Failure 实现:
        override fun forEachOrElse(
            onSuccess: (Nothing) -> Unit,
            onFailure: (java.lang.RuntimeException) -> Unit,
            onEmpty: () -> Unit
        ) = onFailure(exception)    

forEachOrElse 函数虽然可用,但不是最优的,这是因为参数特定时, forEach 和 forEachOrElse 都具有同样的效果,如何解决呢?

答案是把参数设置为可选:

    abstract fun forEach(
        onSuccess: (A) -> Unit = {},
        onFailure: (java.lang.RuntimeException) -> Unit = {},
        onEmpty: () -> Unit = {}

4.2.3 推导模式

Result 是进阶版的 Option, 所以它也可以试着使用 Option 中的 lift:

fun <A, B> lift(f: (A) -> B): (Result<A>) -> Result<B> = { it.map(f) }

这里不需要捕获异常,因为异常已经被 map 处理了。

同理我们可以定义 lift2、lift3:

fun <A, B, C> lift2(f: (A) -> (B) -> C): (Result<A>) -> (Result<B>) -> Result<C> = { a ->
    { b ->
        a.map(f).flatMap { b.map(it) }
    }
}
fun <A, B, C, D> lift3(f: (A) -> (B) -> (C) -> D): (Result<A>) -> (Result<B>) -> (Result<C>) -> Result<D> =
    { a -> { b -> { c -> a.map(f).flatMap { b.map(it) }.flatMap { c.map(it) } } } }

接下来我们可以用 lift2 函数来实现 map2,将数据实现转化:

fun <A, B, C> map2(a: Result<A>, b: Result<B>, f: (A) -> (B) -> C): Result<C> = lift2(f)(a)(b)

这类函数最常见的用例是使用其他函数返回的 Result 类型的参数调用函数或构造函数。

以之前的 ToonMail 为例子,为了填充 Toon 的映射,可以通过要求用户使用以下函数在控制台上名、姓、邮箱来构造:

fun getFirstName(): Result<String> = Result("Joseph")
fun getLastName(): Result<String> = Result("Jostar")
fun getMail(): Result<String> = Result("joseph@jojo.com")

我们可以模拟这个过程,创造一个多参的构造函数:

var createPerson: (String) -> (String) -> (String) -> Toon =
    { x -> { y -> { z -> Toon(x, y, z) } } }
val toon = lift3(createPerson)(getFirstName())(getLastName())(getMail())

这种情况下,抽象已经达到了极致,必须调用具有三个以上参数的函数或者构造函数。

在这种情况下,可以使用推导模式,这样就可以使用任意数量的参数而不需要定义每一个函数:

val toon = getFirstName()
    .flatMap { firstName ->
        getLastName().flatMap { lastName ->
            getMail().map { mail ->
                Toon(firstName, lastName, mail)
            }
        }
    }

也可以在不定义函数的情况下,使用 lift3 ,但由于 Kotlin 的类型推断能力有限,所以必须要指定类型:

val toon2: Result<Toon> = lift3 { x: String ->
    { y: String ->
        { z: String ->
            Toon(x, y, z)
        }
    }
}(getFirstName())(getLastName())(getMail())

5. 小结

  • 表示由于错误而导致的数据确实问题很有必要。 Option 做不到,而 Either、Result能够做到
  • 提供的默认值必须进行惰性计算
  • Result 添加了 Empty 类型后比较强大,可以完全替代 Option
  • 可以通过 forEach 函数对 Result 应用作用,此功能允许对 Success、Failure 和 Empty 应用不同的作用
  • 可以使用 lift 函数,从 A->B 提升到 (Result<A>)-> Result<B>,也有lift2、lift3等
  • 可以使用推导模式来组合任意数量的 Result 数据

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们