作者:Olivier Halligon,原文連結,原文日期:2016-02-06
譯者:ray16897188;校對:小鍋;定稿:numbbbbb
在之前的一篇文章中,我介紹了如何在Swift中使用
throw
做錯誤處理。但是如果你處理的是異步流程,
throw
就無法勝任,該怎麼辦?
throw
和異步有啥問題?
throw
回顧下,我們可以像下面這樣,在一個可能失敗的函數中使用
throw
關鍵字:
// 定義錯誤類型和一個可抛出的函數
enum ComputationError: ErrorType { case DivisionByZero }
func inverse(x: Float) throws -> Float {
guard x != 0 else { throw ComputationError.DivisionByZero }
return 1.0/x
}
// 調用它
do {
let y = try inverse(5.0)
} catch {
print("Woops: \(error)")
}
但如果函數是異步的,需要等待一段時間才會傳回結果,比如帶着 completion block 的函數,這個時候怎麼辦?
func fetchUser(completion: User? /* throws */ -> Void) /* throws */ {
let url = …
NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
// if let error = error { throw error } // 我們不能這樣做, fetchUser 不能“異步地抛出”
let user = data.map { User(fromData: $0) }
completion(user)
}.resume()
}
// 調用
fetchUser() { (user: User?) in
/* do something */
}
這種情況下如果請求失敗的話,你怎麼
throw
?
- 讓
函數fetchUser
是不合理的,因為這個函數(被調用後)會立即傳回,而網絡錯誤隻會在這之後發生。是以當錯誤發生時再throw
一個錯誤就太晚了,throw
函數調用已經傳回。fetchUser
- 你可能想把
标成completion
?但是調用throws
的代碼在completion(user)
裡,不是在調用fetchUser
的代碼裡。是以接受并處理錯誤的代碼必須是fetchUser
本身,而非fetchUser
的調用點。是以這個方案也不行。?fetchUser
攻克這道難題
可以曲線救國:讓
completion
不直接傳回
User?
,而是傳回一個
Void throws -> User
的 throwing 函數,這個 throwing 函數會傳回一個
User
(我們把這個函數命名為
UserBuilder
)。這樣我們就又能
throw
了。
之後當 completion 傳回這個
userBuilder
函數時,我們用
try userBuilder()
去通路裡面的
User
... 或者讓它
throw
出錯誤。
enum UserError: ErrorType { case NoData, ParsingError }
struct User {
init(fromData: NSData) throws { /* … */ }
/* … */
}
typealias UserBuilder = Void throws -> User
func fetchUser(completion: UserBuilder -> Void) {
let url = …
NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
completion({ UserBuilder in
if let error = error { throw error }
guard let data = data else { throw UserError.NoData }
return try User(fromData: data)
})
}.resume()
}
fetchUser { (userBuilder: UserBuilder) in
do {
let user = try userBuilder()
} catch {
print("Async error while fetching User: \(error)")
}
}
這樣 completion 就不會直接傳回一個
User
,而是傳回一個
User
... 或抛出錯誤。之後你就又可以做錯誤處理了。
但說實話,用
Void throws -> User
來代替
User?
并不是最優雅、可讀性最強的解決方案。還有其他辦法嗎?
介紹 Result
回到 Swift 1.0 的時代,那時還沒有
throw
,人們得用一種函數式的方法來處理錯誤。由于 Swift 從函數式程式設計的世界中借鑒過來很多特性,是以當時人們在 Swift 中用
Result
模式來做錯誤處理還是很合理的。
Result
長這樣1:
enum Result<T> {
case Success(T)
case Failure(ErrorType)
}
Result
這個類型其實很簡單:它要麼指代一次成功 —— 附着一個關聯值(associated value)代表着成功的結果 —— 要麼指代一次失敗 —— 有一個關聯的錯誤。它是對可能會失敗的操作的完美抽象。
那麼我們怎麼用它?建立一個
Result.Success
或者一個
Result.Failure
,然後把作為結果的
Result
2 傳入
completion
,最後調用
completion
:
func fetchUser(completion: Result<User> -> Void) {
let url = …
NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
if let error = error {
return completion( Result.Failure(error) )
}
guard let data = data else {
return completion( Result.Failure(UserError.NoData) )
}
do {
let user = try User(fromData: data)
completion( Result.Success(user) )
} catch {
completion( Result.Failure(error) )
}
}.resume()
}
還記得 monads 麼?
Result
的好處就是它可以變成一個 Monad。記得Monads麼?這意味着我們可以給
Result
添加高階的
map
和
flatMap
方法,後兩者會接受一個
f: T->U
或者
f: T->Result<U>
類型的閉包,然後傳回一個
Result<U>
。
如果一開始的
Result
是一個
.Success(let t)
,那就對這個
t
使用這個閉包,得到
f(t)
的結果。如果是一個
.Failure
,那就把這個錯誤繼續傳下去:
extension Result {
func map<U>(f: T->U) -> Result<U> {
switch self {
case .Success(let t): return .Success(f(t))
case .Failure(let err): return .Failure(err)
}
}
func flatMap<U>(f: T->Result<U>) -> Result<U> {
switch self {
case .Success(let t): return f(t)
case .Failure(let err): return .Failure(err)
}
}
}
如果想要了解更多資訊,我建議你去重讀我寫的關于 Monads 的文章,但現在長話短說,我們來修改代碼:
func readFile(file: String) -> Result<NSData> { … }
func toJSON(data: NSData) -> Result<NSDictionary> { … }
func extractUserDict(dict: NSDictionary) -> Result<NSDictionary> { … }
func buildUser(userDict: NSDictionary) -> Result<User> { … }
let userResult = readFile("me.json")
.flatMap(toJSON)
.flatMap(extractUserDict)
.flatMap(buildUser)
上面代碼中最酷的地方:如果其中一個方法(比如
toJSON
)失敗了,傳回了一個
.Failure
,那随後這個 failure 會一直被傳遞到最後,而且不會被傳入到
extractUserDict
和
buildUser
方法裡面去。
這就可以讓錯誤“走一條捷徑”:和
do...catch
一樣,你可以在鍊條的結尾一并處理所有錯誤,而不是在每個中間階段做處理,很酷,不是麼?
從 Result
到 throw
,再從 throw
到 Result
Result
throw
throw
Result
問題是,
Result
不包含在 Swift 标準庫中,而無論怎樣,還是有很多函數使用
throw
來報告同步錯誤(譯注:synchronous errors,與異步錯誤 asynchronous errors 相對)。比如,在實際應用場景中從一個
NSDictionary
建立一個
User
,我們可能得用
init(dict: NSDictionary) throws
構造器,而不是
NSDictionary -> Result<User>
函數。
那怎麼去融合這兩個世界呢?簡單,我們來擴充一下
Result
3!
extension Result {
// 如果是 .Success 就直接傳回值,如果是 .Failure 抛出錯誤
func resolve() throws -> T {
switch self {
case Result.Success(let value): return value
case Result.Failure(let error): throw error
}
}
// 如果表達式傳回值則建構一個 .Success,否則就建構一個 .Failure
init(@noescape _ throwingExpr: Void throws -> T) {
do {
let value = try throwingExpr()
self = Result.Success(value)
} catch {
self = Result.Failure(error)
}
}
}
現在我們就可以很輕松地将 throwing 構造器轉換成一個閉包,該閉包傳回一個
Result
:
func buildUser(userDict: NSDictionary) -> Result<User> {
// 這裡我們調用了 `init` 并使用一個可抛出的尾閉包來建構 `Result`
return Result { try User(dictionary: userDict) }
}
之後如果我們将
NSURLSession
封裝到一個函數中,這個函數就會異步的傳回一個
Result
,我們可以按個人喜好來調整這兩個世界的平衡,例如:
func fetch(url: NSURL, completion: Result<NSData> -> Void) {
NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
completion(Result {
if let error = error { throw error }
guard let data = data else { throw UserError.NoData }
return data
})
}.resume()
}
上面的代碼也調用了 completion block,往裡面傳了一個由 throwing closure4 建立的
Result
對象。
随後我們就可以用
flatMap
把這些都串起來,再根據實際需求決定是否進入
do...catch
的世界:
fetch(someURL) { (resultData: Result<NSData>) in
let resultUser = resultData
.flatMap(toJSON)
.flatMap(extractUserDict)
.flatMap(buildUser)
// 如果我們想在剩下代碼中回到 do/try/catch 的世界
do {
let user = try resultUser.resolve()
updateUI(user: user)
} catch {
print("Error: \(error)")
}
}
我承諾,這就是未來
(校對注:作者這裡的标題使用了雙關語,承諾的英文為 "Promise", 未來的單詞為 "Future"。)(定稿注:這篇文章提到的這種模式術語就是 "Promise",是以說是雙關。)
Result
很炫酷,但是既然它們的主要用途是異步函數(因為同步函數我們已經有了
throw
),那何不讓它也實作對異步的管理呢?
實際上已經有一個這樣的類型TM,它就是
Promise
(有時候也叫
Future
,這兩個術語很像)。
Promise
類型結合了
Result
類型(能成功或者失敗)和異步性。一個
Promise<T>
既可以在一段時間後(展現了異步方面的特性)被成功賦成
T
類型的值(譯注:這裡的指派英文是 fulfill,原意是履行,而 Promise 本身也有承諾的意思。
Promise<T>
被成功指派,等同于承諾被履行),又可能在錯誤發生時被拒絕(reject)。
一個
Promise
也是一個 monad。但和通常以
map
和
flatMap
的名字來調用它的 monadic 函數不同,按規定這兩個函數都通過
then
來調用:
class Promise<T> {
// 與 map 對應的 monad,在 Promise 通常被稱為 then
func then(f: T->U) -> Promise<U>
// 與 flatMap 對應的 monad,在 Promise 中也被稱為 then
func then(f: T->Promise<U>) -> Promise<U>
}
錯誤也通過
.error
和
.recover
解包。在代碼中,它的使用方式和你使用一個
Result
基本相同,畢竟它倆都是 monad:
fetch(someURL) // returns a `Promise<NSData>`
.then(toJSON) // assuming toJSON is now a `NSData -> Promise<NSDictionary>`
.then(extractUserDict) // assuming extractUserDict is now a `NSDictionary -> Promise<NSDictionary>`
.then(buildUser) // assuming buildUser is now a `NSDictionary -> Promise<User>`
.then {
updateUI(user: user)
}
.error { err in
print("Error: \(err)")
}
感受到了嗎,這看起來多麼流暢多麼優雅!這就是把一些微處理步驟精密連接配接起來的流(stream),而且它還替你做了異步處理和錯誤處理這樣的髒活兒累活兒。如果在處理流程中有錯誤發生,比如在
extractUserDict
中出錯,那就直接跳到
error
回調中。就像用
do...catch
或者
Result
一樣。
在
fetch
中使用
Promise
—— 取代 completion block 或者
Result
—— 看起來應該是這樣的:
func fetch(url: NSURL) -> Promise<NSData> {
// PromiseKit 有一個便利的 `init`,會傳回一個 (T?, NSError?) 閉包到 `Promise` 中
return Promise { resolve in
NSURLSession.sharedSession().dataTaskWithURL(url) { (data, _, error) -> Void in
resolve(data, error)
})
}.resume()
}
fetch
方法會立即傳回,是以就沒必要用
completionBlock
了。但它會傳回一個
Promise
對象,這個對象隻去執行
then
裡面的閉包 - 在(異步)資料延時到達、
Promise
這個對象被成功指派(譯注:promise is fulfilled,也是承諾被履行的意思)之後。
Observe 和 Reactive
Promise
很酷,但還有另外一個概念,可以在實作微處理步驟流的同時支援異步操作,并且支援處理這個流中任何時間任何地點發生的錯誤。
這個概念叫做 Reactive Programming(響應式程式設計)。你們之中可能有人知道
ReactiveCocoa
(簡寫 RAC),或者
RxSwift
。即便它和
Promises
有部分相同的理念(異步、錯誤傳遞,...),它還是超越了
Futures
和
Promises
這個級别:
Rx
允許某時刻有多個值被發送(不僅僅有一個傳回值),而且還擁有其他繁多豐富的特性。
這就是另外一個全新話題了,之後我會對它一探究竟。
- 這是對
可能的實作方式中的一種。其他的實作也許就會有一個更明确的錯誤類型。↩Result
- 在這裡我調用
時用了一個小花招,并沒有調用return completion(…)
然後再completion(...)
來退出函數的作用域。這個花招能成功,是因為return
傳回一個completion
,Void
也傳回一個fetchUser
(什麼都不傳回),而且Void
和單個return Void
一樣。這完全是個人偏好,但我還是覺得能用一行寫完更好。↩return
- 這段代碼中,
關鍵字的意思是@noescape
能被保證在throwingExpr
函數的作用域裡是被直接拿來使用 - 相反則是把它存在某個屬性中以後再用。用了這個關鍵字你的編譯器不用強迫你在傳進一個閉包時在調用點使用init
或者self.
了,還能避免引用循環的産生。↩[weak self]
- 在這裡暫停一下,看看這段代碼多像在開篇的時候我們寫的
的那段,感覺我們開篇時就走在了正确的路上。? ↩UserBuilder
本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請通路 http://swift.gg。