Kotlin Option與Either及Result實(shí)現(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)楫惓6a(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)文章
Android中使用Service實(shí)現(xiàn)后臺(tái)發(fā)送郵件功能實(shí)例
這篇文章主要介紹了Android中使用Service實(shí)現(xiàn)后臺(tái)發(fā)送郵件功能的方法,結(jié)合實(shí)例形式分析了Service實(shí)現(xiàn)郵件的發(fā)送、接收及權(quán)限控制相關(guān)技巧,需要的朋友可以參考下2016-01-01Android開發(fā)中自定義ProgressBar控件的方法示例
這篇文章主要介紹了Android開發(fā)中自定義ProgressBar控件的方法,結(jié)合實(shí)例形式分析了自定義ProgressBar控件的定義與使用方法,需要的朋友可以參考下2017-10-10Android添加自定義下拉刷新布局阻尼滑動(dòng)懸停彈動(dòng)畫效果
這篇文章主要為大家介紹了Android添加自定義下拉刷新布局阻尼滑動(dòng)懸停彈動(dòng)畫效果詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02Android中Fragment的分屏顯示處理橫豎屏顯示的實(shí)現(xiàn)方法
今天小編就為大家分享一篇關(guān)于Android中Fragment的分屏顯示處理橫豎屏顯示的實(shí)現(xiàn)方法,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-03-03Android開發(fā)中父組件調(diào)用子組件方法demo
這篇文章主要為大家介紹了Android開發(fā)中父組件調(diào)用子組件方法demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12AndroidSDK Support自帶夜間、日間模式切換詳解
這篇文章主要為大家詳細(xì)介紹了AndroidSDK Support自帶夜間、日間模式切換,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-09-09手把手教你實(shí)現(xiàn)Android編譯期注解
今天給大家介紹Android編譯期注解sdk的步驟以及注意事項(xiàng),并簡要分析了運(yùn)行時(shí)注解以及字節(jié)碼技術(shù)在生成代碼上與編譯期注解的不同與優(yōu)劣,感興趣的朋友一起看看吧2021-07-07Android基于google Zxing實(shí)現(xiàn)各類二維碼掃描效果
這篇文章主要介紹了Android基于google Zxing實(shí)現(xiàn)各類二維碼掃描效果的相關(guān)資料,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-02-02