<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
空指標參照 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 來處理這種問題。
建立一個 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 類, 它目前其實沒有什麼作用,就是判斷傳入值是否為空而已。我們還需要拓展一些功能。
和 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) // 拋異常
僅僅有 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)) }
從 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 }
在 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 中會解決掉。
如果決定在程式碼中使用 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 …
Some
表示存在, None
表示不存在上面說到 Option
作為資料處理型別,對資料缺失問題不是完美的,時機就是在出現異常的時候。為什麼呢?表面上的原因是: Option 只返回一個資料, 這個資料要麼是空,要麼是正常值, 當出現異常時,可能會返回提前設定的預設值,也可能會返回空。
所以,如果 Option 有一個升級版, 返回兩種不同型別,有異常時,返回異常資訊,沒異常時,返回正常資訊,這樣出現了異常,呼叫者也可以知道 ----- 於是就有了 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 不能區分計算結果的問題
其實把上面的問題總結一下,可以知道我們想擁有一個型別,可以明確的告訴我們計算結果:
有值無值計算過程中出現異常, 能給出異常資訊
Option 能滿足 1(Some) 和 2(None)
Either 能滿足 1(Right) 和 3(Left)
下面我們建立的 Result
,將是完美解決上述所有問題的終極方案。 而且 Kotlin 中也有 Result ,但是這個原生的 Reuslt 和 上面定義的 Option、 Either 差不多,並不是完美版,原始碼很簡單,讀者一看便懂。雖然也夠日常開發使用,但是為了優化資料結構,我打算基於其創作一版更好的 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)
讀者可能認為缺少了點什麼東西,因為沒有區分兩種不同的空案例。 但事實並非如此,可選資料不需要錯誤資訊。 如果讀者認為需要資訊,則資料不是可選的
實際場景中,我們會判斷 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)
到目前為止,我們除了去 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 = {}
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())
(Result<A>)-> Result<B>
,也有lift2、lift3等到此這篇關於Kotlin Option與Either及Result實現例外處理詳解的文章就介紹到這了,更多相關Kotlin 例外處理內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45