首頁 > 軟體

Kotlin Option與Either及Result實現例外處理詳解

2022-12-15 14:03:45

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 資料

到此這篇關於Kotlin Option與Either及Result實現例外處理詳解的文章就介紹到這了,更多相關Kotlin 例外處理內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com