欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Kotlin Option與Either及Result實(shí)現(xiàn)異常處理詳解

 更新時(shí)間:2022年12月13日 15:38:44   作者:RikkaTheWorld  
Kotlin異常處理,異常是在程序運(yùn)行時(shí)可能發(fā)生的不必要的問題,并突然終止您的程序。異常處理是一個(gè)過程,使用它可以防止程序出現(xiàn)可能破壞我們代碼的異常

1. 異常處理概述

空指針引用 NPE 是編程語言最常見的異常,數(shù)十年來無處不在的和程序打交道,在Java中,我們使用“防御式編程”來處理空數(shù)據(jù),這導(dǎo)致了代碼不美觀,比如增加了縮進(jìn)、嵌套。

Kotlin是如何解決這個(gè)問題的呢?Kotlin 使用顯示的 ? 表示一個(gè)數(shù)據(jù)類型是否可空,如果參數(shù)的傳遞是非空的,那么我們就無須去處理它。

當(dāng)然不可能所有時(shí)候參數(shù)都是非空的,我們依然要處理空值的問題,最常見的就是業(yè)務(wù)空值,舉一個(gè)例子,下面代碼用于求一個(gè)整數(shù)數(shù)列的平均值:

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

如果傳入的列表非空,我們可以返回(列表所有整數(shù)的和 / 列表長度 ) -> 列表平均值。

但如果當(dāng)傳入的列表是一個(gè)空列表, 我們應(yīng)該返回什么?我們可能會(huì)有下面幾種想法:

返回 0

很明顯這是有問題的,因?yàn)檎麛?shù)列表的平均值可能是0,所以返回0的話,你能讓調(diào)用者知道這個(gè)是正常的值,還是因?yàn)檩斎霐?shù)據(jù)的異常而導(dǎo)致的0呢?返回 Double.NaN

沒什么問題,因?yàn)檫@個(gè)是一個(gè) Double 值。

但這也僅僅是針對(duì)這個(gè)函數(shù)沒問題, 設(shè)想這個(gè)函數(shù)的返回不是 Double, 而是 Int 類型, Int 類型可沒有 NaN 這種值呀拋出異常 throw Exception("Empty list!")

這個(gè)解決方案不是很好,因?yàn)樗a(chǎn)生的麻煩比它解決的問題要多,原因如下:

①: 異常通用于提示錯(cuò)誤的結(jié)果,但這里本質(zhì)上是沒有錯(cuò)誤的,沒有輸出結(jié)果的原因是沒有輸入數(shù)據(jù)

②:這里應(yīng)該拋出什么異常,是通用的還是自定義的?

③:這個(gè)函數(shù)不再是一個(gè)純函數(shù),其他函數(shù)組合它使用時(shí),必須要使用 try - catch 形式, 這是一種現(xiàn)代的goto形式返回 null

在通用編程語言中,返回 null 值是最糟糕的解決方案, 看看 Java 語言里這樣做的后果:

①:強(qiáng)制調(diào)用者處理測(cè)試結(jié)果為 null 的情況

②:如果使用裝箱,則該代碼會(huì)崩潰并出現(xiàn) NPE, 因?yàn)闊o法將 null 引用拆箱變?yōu)榛緮?shù)據(jù)類型

③:和拋異常一樣,該函數(shù)無法再組合

④:有潛在的問題,如果調(diào)用者忘記處理 null 結(jié)果,則該函數(shù)的引用鏈上,任意位置都可能會(huì)產(chǎn)生 NPE如果異常,返回一個(gè)指定的默認(rèn)值 default

這就跟一開始一樣了, 我們無法區(qū)分 default 和 真正的結(jié)果。

可以看出來,僅僅一行的代碼,可以產(chǎn)生很低的下限。 一系列思考下來,我們了解了這個(gè)問題的核心本質(zhì):我們?cè)撊绾翁幚硪粋€(gè)異常結(jié)果或者可選結(jié)果?

放心的是,編程語言庫的設(shè)計(jì)者們也對(duì)這個(gè)問題進(jìn)行思考,Java8的一個(gè)特性 Optional 就是為了解決這個(gè)問題, Kotlin 也有與之對(duì)應(yīng)的 Result,為了更好的了解它們的本質(zhì),通過學(xué)習(xí) Option、 Either、 Result,我們了解如何解決這樣的問題。

在介紹之前,我們了解下一個(gè)具體的問題場(chǎng)景,定義一個(gè)數(shù)據(jù)類用于表示用戶:

data class Toon(
    val firstName: String, // 首名字
    val lastName: String, // 姓氏
    val email: String? = null  // email
)
// 定義好一份數(shù)據(jù)
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 是可選參數(shù), 不傳也是正常的。 現(xiàn)在假定外部有人使用該 map,他可能會(huì)遇到下面的情況:

雖然 Kotlin 有 ? 可以幫助我們判斷參數(shù)是否為空,然后強(qiáng)制處理以避免這種情況。

但是在極端情況下 ---- 調(diào)用代碼是 Java 且開發(fā)者沒有做數(shù)據(jù)判空, 那這樣的代碼下限是很低的,是有較大概率出錯(cuò)或者崩潰的。而且就算做了判空,也可能會(huì)因?yàn)槎嗉恿烁鞣N if..else 語句,而讓代碼變得臃腫和不美觀。

我們來實(shí)現(xiàn)一個(gè) Option 來處理這種問題。

2. Option

建立一個(gè) Option 模型,實(shí)現(xiàn)的目標(biāo)處理鏈如下:

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)
            }
    }
}

我們實(shí)現(xiàn)了一個(gè)很基礎(chǔ)的 Option 類, 它目前其實(shí)沒有什么作用,就是判斷傳入值是否為空而已。我們還需要拓展一些功能。

2.1 從 Option 提取值

Optional 那樣, 創(chuàng)建一個(gè) getOrElse 函數(shù):如果 Option 值不為空,則返回值, 否則返回傳入的默認(rèn)值:

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

我們可以運(yùn)用如下:

    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

看起來還不錯(cuò),但假設(shè)我們的調(diào)用者在調(diào)用 getOrElse 時(shí),傳的不是 0 ,而是:

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

那么這段代碼會(huì)出現(xiàn)什么問題? 你會(huì)認(rèn)為 max1 能輸出7, 然后 max2 拋出異常么?

答案是這段代碼會(huì)直接在一開始拋出異常,因?yàn)?Kotlin 是嚴(yán)格的靜態(tài)編程語言,在執(zhí)行函數(shù)之前,無論是否需要都會(huì)處理函數(shù)參數(shù),這就意味著 getOrElse 的參數(shù)在任何情況下都會(huì)被處理,無論是在 Some 還是 None 中調(diào)用它。如果傳參是一個(gè)值,這是無關(guān)緊要的,但是傳參是一個(gè)函數(shù)時(shí),這就會(huì)有很大的區(qū)別,任何情況下都會(huì)調(diào)用 getDefault 函數(shù),因此這段代碼的第一行就拋出異常了。

這顯然不是我們想要的結(jié)果。 為了解決這個(gè)問題, 我們可以使用惰性計(jì)算,即讓 throw Exception 在需要時(shí)被調(diào)用:

    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 函數(shù)

僅僅有 getOrElse 可能還是不夠的,List中最重要的一個(gè)函數(shù)就是 map 函數(shù),考慮到一個(gè)向列表一樣最多包含一個(gè)元素的 Option,也可以應(yīng)用同樣的函數(shù)。

添加一個(gè) map函數(shù),從 Option<A> 轉(zhuǎn)化成 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 的函數(shù)并不是安全編程中最常用的函數(shù), 因?yàn)?map 函數(shù)的入?yún)⑹牵?(A)-> B , 但是返回的卻是一個(gè) Option<B>,這可能會(huì)難以理解,而且需要的額外的工作:包裝 Some。

為了減少中間結(jié)果,會(huì)有更多的使用方法從 (A) -> Option<B>,在 List 類中也有類似的操作, 那就是 flatmap打平。

我們也創(chuàng)建一個(gè) flatmap 函數(shù)來擴(kuò)展 Option,:

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

正如需要一種方法來映射一個(gè)返回 Option 的函數(shù)(flatmap),也需要一個(gè) getOrElse 的版本來返回一個(gè) Option 的默認(rèn)值。 代碼如下:

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

通過 map{ this } 可以產(chǎn)生一個(gè) Option<Option<A>> ,通過 getOrElse 方法拿到里面那一層, 這么寫相較于直接返回更為優(yōu)雅

接下來編寫一個(gè) filter 函數(shù),篩選出不滿足謂詞表達(dá)式的所有函數(shù):

// 比較智能的實(shí)現(xiàn), 因?yàn)橹耙呀?jīng)定義過 flatmap, 所以可以直接組合
fun filter(p: (A) -> Boolean): Option<A> = flatmap { if (p(it)) this else None }

2.4 Option用例

在 Java 的 Optional 中有個(gè)方法叫 ifPresent() , 表示該 Optional 中是否包含值, 那對(duì)于 Option 來說,這個(gè)方法應(yīng)該叫 isSome(), 如果實(shí)現(xiàn)了這個(gè)方法,那么開發(fā)者就可以在使用 Option 的值之前,使用這個(gè)方法查詢 Option 是否有值可用。如下代碼:

if (a.isSome()) {
   // 當(dāng) a 有值的操作
} else {
   // 當(dāng) a 沒有值的操作
}

等等! 這個(gè)方法和我自己判斷 a 是否為空 效果是一樣的,那既然是一樣的,為什么還要把值封裝到 Option 里面去呢?

所以, isSome() 并不是測(cè)試的最佳方法,它和提前測(cè)試null值引用唯一的區(qū)別就是:如果先前忘記判斷異常值,那么在運(yùn)行的時(shí)候會(huì)拋出 IllegalStateException 或 NoSuchElement 等異常,而不是 NPE。

使用 Option 最佳的方式就是組合去使用。為此,必須為所有的用例創(chuàng)建所有必要的函數(shù), 這些用例可以在測(cè)試出該值非null后將如何處理,他應(yīng)該有如下操作:

  • 將這個(gè)值作為另一個(gè)函數(shù)的輸入
  • 對(duì)值添加作用
  • 如果不是空值,就是用這個(gè)值,否則使用默認(rèn)值來應(yīng)用函數(shù)操作

第一個(gè)和第三個(gè)之前創(chuàng)建的函數(shù)已經(jīng)能夠做到了,第二點(diǎn)以后會(huì)講到。

有一個(gè)例子,如果使用 Option 類來改變使用映射的方式,以 Toon 為例,我們?cè)?Map 上實(shí)現(xiàn)一個(gè)擴(kuò)展函數(shù),以便在查詢給定鍵時(shí),返回一個(gè) 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))
    }
}
// 擴(kuò)展函數(shù)來實(shí)現(xiàn)前檢查模式, 以避免返回空引用
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

在這個(gè)過程中,我們可以看到組合 Option 的操作來達(dá)到目的而不需要冒著 NPE 的風(fēng)險(xiǎn),由于 Kotlin 的便捷性,即使不用 Option,我們也可以使用 Kotlin 封裝好的代碼來實(shí)現(xiàn)

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

可以看到 Kotlin 風(fēng)格更加方便,但是打印值卻如下:

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

第二行是 None, 因?yàn)?jonathan 沒有 email, 第三行 No Data 是因?yàn)?jolyne 不在映射中,需要一種方法來區(qū)分這兩種情況,但無論使用可空類型還是 Option, 都無法區(qū)分。這個(gè)問題下面學(xué)習(xí)的 Either 和 Result 中會(huì)解決掉。

2.5 其他的組合方法

如果決定在代碼中使用 Option ,可能會(huì)產(chǎn)生一些巨大的后果,因?yàn)榇a一寫出來就已經(jīng)過時(shí)了。當(dāng)出現(xiàn)了一些場(chǎng)景,當(dāng)前 api 不滿足,我們需要去重新編寫庫函數(shù)嗎?得益于 Kotlin 的擴(kuò)展函數(shù),我們可以通過組合的方式,來構(gòu)建原來庫中沒有的api。

練習(xí)1. 定義一個(gè) lift 函數(shù), 該函數(shù)的參數(shù)是 (A) -> B , 并返回一個(gè) (Option<A>) -> Option<B>

解決方法很簡單,可以在包級(jí)別聲明:

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

這樣,我們可以用其寫一些值函數(shù),以此生成目標(biāo)的 Option版本的函數(shù),例如, 將字母轉(zhuǎn)化為大寫的函數(shù): String.toUpperCase 的 Option 版本可以這樣實(shí)現(xiàn):

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

練習(xí)2. 前面的 lift 函數(shù)如果拋出了異常,那么就有不確定性,例如f函數(shù)拋出了異常,lift就無效了,編寫一個(gè)對(duì)拋出異常函數(shù)仍然有效的函數(shù)。

只需要寫一個(gè) try catch 即可:

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

可能還需要將函數(shù) (A) -> B ,生成函數(shù) (A) -> Option<B>,可以用同樣的方法:

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

但是這種轉(zhuǎn)化其實(shí)有點(diǎn)問題,因?yàn)楫a(chǎn)生了異常,我們把異常給“掩蓋”了,實(shí)際上我們應(yīng)該要讓外部的調(diào)用者知道有這個(gè)異常。下面的兩章會(huì)解決這個(gè)問題。

練習(xí)3. 編寫一個(gè)函數(shù) map2,該函數(shù)一個(gè) Option<A> , 一個(gè) Option<B> 和一個(gè) 從 (A, B) 到 C的柯里化形式的函數(shù)作為參數(shù),然后返回一個(gè) Option<C>。

下面是使用 flatmap 和 map 的解決方法,理解這個(gè)模式很重要,以后會(huì)經(jīng)常遇到,下篇文章將重點(diǎn)講述這一內(nèi)容:

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) } }

通過規(guī)律甚至可以寫出 map3 、 map4 …

2.6 Option 小結(jié)

  • 用可選數(shù)據(jù)來表示函數(shù)意味數(shù)據(jù)可能存在或不存在, Some 表示存在, None 表示不存在
  • 用 null 指針表示數(shù)據(jù)的確實(shí)不切實(shí)際而且很危險(xiǎn),字面值和空列表是表示數(shù)據(jù)確實(shí)的其他方法,但是他們組合的不好
  • Option 數(shù)據(jù)類型是一種表示可選數(shù)據(jù)的更好方式
  • 將 map、flatmap 高階函數(shù)應(yīng)用到 Option 上,可以方便的組合 Option
  • Option 是有局限性的,比如不能區(qū)分?jǐn)?shù)據(jù)不存在還是異常等其他情況,其次,雖然 Option 可以表示產(chǎn)生異常的計(jì)算結(jié)果,但是它沒有關(guān)于發(fā)生異常的所有信息

3. Either

上面說到 Option 作為數(shù)據(jù)處理類型,對(duì)數(shù)據(jù)缺失問題不是完美的,時(shí)機(jī)就是在出現(xiàn)異常的時(shí)候。為什么呢?表面上的原因是: Option 只返回一個(gè)數(shù)據(jù), 這個(gè)數(shù)據(jù)要么是空,要么是正常值, 當(dāng)出現(xiàn)異常時(shí),可能會(huì)返回提前設(shè)置的默認(rèn)值,也可能會(huì)返回空。

所以,如果 Option 有一個(gè)升級(jí)版, 返回兩種不同類型,有異常時(shí),返回異常信息,沒異常時(shí),返回正常信息,這樣出現(xiàn)了異常,調(diào)用者也可以知道 ----- 于是就有了 Either。

Either 類型

因?yàn)?Kotlin、Java 返回值只能用一個(gè)數(shù)據(jù)類型,所以我們的類型既可以返回錯(cuò)誤信息、也可以返回正常值,就要將其塞到一個(gè)數(shù)據(jù)類型里面去, 例如 Map映射 、 一個(gè)新的數(shù)據(jù)Bean、一個(gè) Pair。

注:像Kotlin這種強(qiáng)類型語言必須借助包裝結(jié)構(gòu),像 Python 這種直接用字典就好了。

來看下 Either<Left, Right> 實(shí)現(xiàn),基于國際慣例,Left是異常,Right是正常。 再 Option 的實(shí)現(xiàn),我們順帶把一些基本的 map、 flatmap、getOrElse 、orElse 也實(shí)現(xiàn)進(jìn)去:

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)
    /**
     * 錯(cuò)誤信息
     */
    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 類很有用,而且已經(jīng)完美融入到了 Scala 語言中作為常規(guī)的數(shù)據(jù)而使用。

但是 Either 沒有達(dá)到理想的效果: 在沒有可用值時(shí),不知道會(huì)發(fā)生什么。

此時(shí)會(huì)得到默認(rèn)值,但是卻不知道這個(gè)默認(rèn)值是計(jì)算出來的,還是因?yàn)楫惓6a(chǎn)生的結(jié)果, 它解決了 Option 不能給出錯(cuò)誤信息的問題,但未能解決 Option 不能區(qū)分計(jì)算結(jié)果的問題

4. Result

其實(shí)把上面的問題總結(jié)一下,可以知道我們想擁有一個(gè)類型,可以明確的告訴我們計(jì)算結(jié)果:

有值無值計(jì)算過程中出現(xiàn)異常, 能給出異常信息

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

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

下面我們創(chuàng)建的 Result ,將是完美解決上述所有問題的終極方案。 而且 Kotlin 中也有 Result ,但是這個(gè)原生的 Reuslt 和 上面定義的 Option、 Either 差不多,并不是完美版,源碼很簡單,讀者一看便懂。雖然也夠日常開發(fā)使用,但是為了優(yōu)化數(shù)據(jù)結(jié)構(gòu),我打算基于其創(chuàng)作一版更好的 Result。

4.1 Result 類型

Reult 使用 Success 表示有值,使用 Failure 表示異常, 使用 Empty 表示無值。

并且對(duì) map 、flatmap 函數(shù)進(jìn)行了保護(hù),是一個(gè)安全的版本,使用者更放心,我們才更安心。

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)
    }
    /**
     * 沒有 / 錯(cuò)誤 返回一個(gè) default, 不能為空, 如果需要空, 使用 [getOrNull]
     */
    fun getOrElse(defaultValue: () -> @UnsafeVariance A): A = when (this) {
        is Success -> this.data
        else -> defaultValue()
    }
    /**
     * 沒有 / 錯(cuò)誤 返回一個(gè) 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))
    }
}

這樣再運(yùn)用到之前的例子:

// 改變?cè)袛?shù)據(jù)結(jié)構(gòu):
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)
    }
}
// 創(chuàng)建一個(gè) 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())
}

當(dāng)我們?cè)谳斎耄?Joseph、Jonathan、Josuke、空字符串時(shí),會(huì)有如下結(jié)果:

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

讀者可能認(rèn)為缺少了點(diǎn)什么東西,因?yàn)闆]有區(qū)分兩種不同的空案例。 但事實(shí)并非如此,可選數(shù)據(jù)不需要錯(cuò)誤信息。 如果讀者認(rèn)為需要信息,則數(shù)據(jù)不是可選的

4.2 Result 高級(jí)處理

4.2.1 使用斷言

實(shí)際場(chǎng)景中,我們會(huì)判斷 Result 中的值是否符合斷言(條件),匹配后才能使用這個(gè)值。

所以我們可以創(chuàng)建一個(gè)函數(shù), 傳入一個(gè) predicate 謂詞函數(shù),進(jìn)行條件判定,如果成功返回 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 的各個(gè)類型的情況,所以我們不用再判斷 Result 的類型從而去處理各種情況, 可以說是十分好用,其實(shí) Result 的實(shí)際使用,都離不開 map 和 flatmap。

我們還可以使用斷言去做別的事情,例如傳入一個(gè)條件,條件符合就返回 true, 反之返回 false,代碼如下:

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

4.2.2 應(yīng)用作用

到目前為止,我們除了去 get 這個(gè) Result 中的值,也沒有做其他事情。 我們可以讓外部去應(yīng)用這個(gè)值,產(chǎn)生做用, 就像 List forEach 函數(shù)那樣去操作每個(gè)元素。

abstract fun forEach(effect: (A) -> Unit)
// Success 實(shí)現(xiàn)
override fun forEach(effect: (A) -> Unit) = effect(data)
// Empty 實(shí)現(xiàn)
override fun forEach(effect: (Nothing) -> Unit) = Unit
// Failure 實(shí)現(xiàn)
override fun forEach(effect: (Nothing) -> Unit) = Unit

上面的實(shí)現(xiàn)不是很適合 Result,因?yàn)橐话阄覀儠?huì)對(duì) Failure 做一些操作。

為此我們實(shí)現(xiàn)一個(gè)方法, 他必須能同時(shí)處理 Failure、 Empty:

    abstract fun forEachOrElse(
        onSuccess: (A) -> Unit,
        onFailure: (java.lang.RuntimeException) -> Unit,
        onEmpty: () -> Unit
    )
// Success 實(shí)現(xiàn)
        override fun forEachOrElse(
            onSuccess: (A) -> Unit,
            onFailure: (java.lang.RuntimeException) -> Unit,
            onEmpty: () -> Unit
        ) = onSuccess(data)
// Empty 實(shí)現(xiàn):
        override fun forEachOrElse(
            onSuccess: (Nothing) -> Unit,
            onFailure: (java.lang.RuntimeException) -> Unit,
            onEmpty: () -> Unit
        ) = onEmpty()    
// Failure 實(shí)現(xiàn):
        override fun forEachOrElse(
            onSuccess: (Nothing) -> Unit,
            onFailure: (java.lang.RuntimeException) -> Unit,
            onEmpty: () -> Unit
        ) = onFailure(exception)    

forEachOrElse 函數(shù)雖然可用,但不是最優(yōu)的,這是因?yàn)閰?shù)特定時(shí), forEach 和 forEachOrElse 都具有同樣的效果,如何解決呢?

答案是把參數(shù)設(shè)置為可選:

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

4.2.3 推導(dǎo)模式

Result 是進(jìn)階版的 Option, 所以它也可以試著使用 Option 中的 lift:

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

這里不需要捕獲異常,因?yàn)楫惓R呀?jīng)被 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 函數(shù)來實(shí)現(xiàn) map2,將數(shù)據(jù)實(shí)現(xiàn)轉(zhuǎn)化:

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

這類函數(shù)最常見的用例是使用其他函數(shù)返回的 Result 類型的參數(shù)調(diào)用函數(shù)或構(gòu)造函數(shù)。

以之前的 ToonMail 為例子,為了填充 Toon 的映射,可以通過要求用戶使用以下函數(shù)在控制臺(tái)上名、姓、郵箱來構(gòu)造:

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

我們可以模擬這個(gè)過程,創(chuàng)造一個(gè)多參的構(gòu)造函數(shù):

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

這種情況下,抽象已經(jīng)達(dá)到了極致,必須調(diào)用具有三個(gè)以上參數(shù)的函數(shù)或者構(gòu)造函數(shù)。

在這種情況下,可以使用推導(dǎo)模式,這樣就可以使用任意數(shù)量的參數(shù)而不需要定義每一個(gè)函數(shù):

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

也可以在不定義函數(shù)的情況下,使用 lift3 ,但由于 Kotlin 的類型推斷能力有限,所以必須要指定類型:

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

5. 小結(jié)

  • 表示由于錯(cuò)誤而導(dǎo)致的數(shù)據(jù)確實(shí)問題很有必要。 Option 做不到,而 Either、Result能夠做到
  • 提供的默認(rèn)值必須進(jìn)行惰性計(jì)算
  • Result 添加了 Empty 類型后比較強(qiáng)大,可以完全替代 Option
  • 可以通過 forEach 函數(shù)對(duì) Result 應(yīng)用作用,此功能允許對(duì) Success、Failure 和 Empty 應(yīng)用不同的作用
  • 可以使用 lift 函數(shù),從 A->B 提升到 (Result<A>)-> Result<B>,也有l(wèi)ift2、lift3等
  • 可以使用推導(dǎo)模式來組合任意數(shù)量的 Result 數(shù)據(jù)

到此這篇關(guān)于Kotlin Option與Either及Result實(shí)現(xiàn)異常處理詳解的文章就介紹到這了,更多相關(guān)Kotlin 異常處理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論