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

Swift設(shè)計思想Result<T>與Result<T,?E:?Error>類型解析

 更新時間:2022年11月30日 09:13:49   作者:王巍  
這篇文章主要為大家介紹了Swift設(shè)計思想Result<T>與Result<T,?E:?Error>的類型示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

背景知識

Cocoa API 中有很多接受回調(diào)的異步方法,比如 URLSession 的 dataTask(with:completionHandler:)。

URLSession.shared.dataTask(with: request) {
    data, response, error in
        if error != nil {
            handle(error: error!)
        } else {
            handle(data: data!)
        }
}

有些情況下,回調(diào)方法接受的參數(shù)比較復雜,比如這里有三個參數(shù):(Data?, URLResponse?, Error?),它們都是可選值。當 session 請求成功時,Data 參數(shù)包含 response 中的數(shù)據(jù),Error 為 nil;當發(fā)生錯誤時,則正好相反,Error 指明具體的錯誤 (由于歷史原因,它會是一個 NSError 對象),Data 為 nil

關(guān)于這個事實,dataTask(with:completionHandler:) 的文檔的 Discussion 部分有十分詳細的說明。另外,response: URLResponse? 相對復雜一些:不論是請求成功還是失敗,只要從 server 收到了 response,它就會被包含在這個變量里。

這么做雖然看上去無害,但其實存在改善的余地。顯然 data 和 error 是互斥的:事實上是不可能存在 data 和 error 同時為 nil 或者同時非 nil 的情況的,但是編譯器卻無法靜態(tài)地確認這個事實。編譯器沒有制止我們在錯誤的 if 語句中對 nil 值進行解包,而這種行為將導致運行時的意外崩潰。

我們可以通過一個簡單的封裝來改進這個設(shè)計:如果你實際寫過 Swift,可能已經(jīng)對 Result 很熟悉了。它的思想非常簡單,用泛型將可能的返回值包裝起來,因為結(jié)果是成功或者失敗二選一,所以我們可以藉此去除不必要的可選值。

enum Result<T, E: Error> {
    case success(T)
    case failure(E)
}

把它運用到 URLSession 中的話,包裝一下 URLSession 方法,上面調(diào)用可以變?yōu)椋?/p>

// 如果 Result 存在于標準庫的話,
// 這部分代碼應該由標準庫的 Foundataion 擴展進行實現(xiàn)
extension URLSession {
    func dataTask(with request: URLRequest, completionHandler: @escaping (Result<(Data, URLResponse), NSError>) -> Void) -> URLSessionDataTask {
        return dataTask(with: request) { data, response, error in
            if error != nil {
                completionHandler(.failure(error! as NSError))
            } else {
                completionHandler(.success((data!, response!)))
            }
        }
    }
}
URLSession.shared.dataTask(with: request) { result in
    switch result {
    case .success(let (data, _)):
        handle(data: data)
    case .failure(let error):
        handle(error: error)
    }
}

這里原文代碼中 completionHandler 里 (Result<(Data, URLResponse), NSError>) -> Void) 這個類型是錯誤的。Data 存在時 URLResponse 一定存在,但是我們上面討論過,當 NSError 不為 nil 時,URLResponse 也可能存在。原文代碼忽略了這個事實,將導致 error 狀況時無法獲取到可能的 URLResponse。正確的類型應該是 (Result<(Data), NSError>, URLResponse?) -> Void

當然,在回調(diào)中對 result 的處理也需要對應進行修改。

調(diào)用的時候看起來很棒,我們可以避免檢查可選值的情況,讓編譯器保證在對應的 case 分支中有確定的非可選值。這個設(shè)計在很多存在異步代碼的框架中被廣泛使用,比如 Swift Package Manager,Alamofire 等中都可覓其蹤。

上面代碼注釋中提到,「如果 Result 存在于標準庫的話,這部分代碼應該由標準庫的 Foundataion 擴展進行實現(xiàn)」。但是考慮到原有的可選值參數(shù) ((Data?, URLResponse?, Error?)) 作為回調(diào)的 API 將會共享同樣的函數(shù)名,所以上面的函數(shù)命名是不可取的,否則將導致沖突。在這類 public API 發(fā)布后,如何改善和迭代確實是個難題。一個可行的方法是把 Foundation 的 URLSession deprecate 掉,提取出相關(guān)方法放到諸如 Network.framework 里,并讓它跨平臺。另一種可行方案是通過自動轉(zhuǎn)換工具,強制 Swift 使用 Result 的回調(diào),并保持 OC 中的多參數(shù)回調(diào)。如果你正在打算使用 Result 改善現(xiàn)有設(shè)計,并且需要考慮保持 API 的兼容性時,這會是一個不小的挑戰(zhàn)。

錯誤類型泛型參數(shù)

如此常用的一個可以改善設(shè)計的定義,為什么沒有存在于標準庫中呢?關(guān)于 Result,其實已經(jīng)有相關(guān)的提案

這個提案中值得注意的地方在于,Result 的泛型類型只對成功時的值進行了類型約束,而忽略了錯誤類型。給出的 Result 定義類似這樣:

enum Result<T> {
    case success(T)
    case failure(Error)
}

很快,在 1 樓就有人質(zhì)疑,問這樣做的意義何在,因為畢竟很多已存在的 Result 實現(xiàn)都是包含了 Error 類型約束的。確定的 Error 類型也讓人在使用時多了一份“安全感”。

不過,其實我們實際類比一下 Swift 中已經(jīng)存在的錯誤處理的設(shè)計。Swift 中的 Error 只是一個協(xié)議,在 throw 的時候,我們也并不會指明需要拋出的錯誤的類型:

func methodCanThrow() throws {
    if somethingGoesWrong {
        // 在這里可以 throw 任意類型的 Error
    }
}
do {
    try methodCanThrow()
} catch {
    if error is SomeErrorType {
        // ...
    } else if error is AnotherErrorType {
        // ...
    }
}

但是,在帶有錯誤類型約束的 Result<T, E: Error> 中,我們需要為 E 指定一個確定的錯誤類型 (或者說,Swift 并不支持在特化時使用協(xié)議,Result<Response, Error> 這樣的類型是非法的)。這與現(xiàn)有的 Swift 錯誤處理機制是背道而馳的。

關(guān)于 Swift 是否應該拋出帶有類型的錯誤,曾經(jīng)存在過一段時間的爭論。最終問題歸結(jié)于,如果一個函數(shù)可以拋出多種錯誤 (不論是該函數(shù)自身產(chǎn)生的錯誤,還是在函數(shù)中 try 其他函數(shù)時它們所帶來的更底層的錯誤),那么 throws 語法將會變得非常復雜且不可控 (試想極端情況下某個函數(shù)可能會拋出數(shù)十種錯誤)?,F(xiàn)在大家一致的看法是已有的用 protocol Error 來定義錯誤的做法是可取的,而且這也編碼在了語言層級,我們對「依賴編譯器來確定 try catch 會得到具體哪種錯誤」這件事,幾乎無能為力。

另外,半開玩笑地說,要是 Swift 能類似這樣 extension Swift.Error: Swift.Error {},支持協(xié)議遵守自身協(xié)議的話,一切就很完美了,XD。

選擇哪個比較好?

兩種方式各有優(yōu)缺點,特別在如果需要考慮 Cocoa 兼容的情況下,更并說不上哪一個就是完勝。這里將兩種寫法的優(yōu)缺點簡單比較一下,在實踐中最好是根據(jù)項目情況進行選擇。

Result<T, E: Error>

優(yōu)點

可以由編譯器幫助進行確定錯誤類型

當通過使用某個具體的錯誤類型擴展 Error 并將它設(shè)定為 Result 的錯誤類型約束后,在判斷錯誤時我們就可以比較容易地檢查錯誤處理的完備情況了:

 enum UserRegisterError: Error {
     case duplicatedUsername
     case unsafePassword
 }
 userService.register("user", "password") {
     result: Result<User, UserRegisterError> in
     switch result {
     case .success(let user):
         print("User registered: \(user)")
     case .failure(let error):
         if error == .duplicatedUsername {
             // ...
         } else if error == .unsafePassword {
             // ...
         }
     }
 }

上例中,由于 Error 的類型已經(jīng)可以被確定是 UserRegisterError,因此在 failure 分支中的檢查變得相對容易。

這種編譯器的類型保證給了 API 使用者相當強的信心,來從容進行錯誤處理。如果只是一個單純的 Error 類型,API 的用戶將面臨相當大的壓力,因為不翻閱文檔的話,就無從知曉需要處理怎樣的錯誤,而更多的情況會是文檔和事實不匹配…

但是帶有類型的錯誤就相當容易了,查看該類型的 public member 就能知道會面臨的情況了。在制作和發(fā)布框架,以及提供給他人使用的 API 的時候,這一點非常重要。

按條件的協(xié)議擴展

使用泛型約束的另一個好處是可以方便地對某些情況的 Result 進行擴展。

舉例來說,某些異步操作可能永遠不會失敗,對于這些操作,我們沒有必要再使用 switch 去檢查分支情況。一個很好的例子就是 Timer,我們設(shè)定一個在一段時間后執(zhí)行的 Timer 后,如果不考慮人為取消,這個 Timer 總是可以正確執(zhí)行完畢,而不會發(fā)生任何錯誤的。我們可能會選擇使用一個特定的類型來代表這種情況:

 enum NoError: Error {}
 func run(after: TimeInterval, done: @escaping (Result<Timer, NoError>) -> Void ) {
     Timer.scheduledTimer(withTimeInterval: after, repeats: false) { timer in
         done(.success(timer))
     }
 }

在使用的時候,本來我們需要這樣的代碼:

 run(after: 2) { result in
     switch result {
     case .success(let timer):
         print(timer)
     case .failure:
         fatalError("Never happen")
     }
 }

但是,通過對 E 為 NoError 的情況添加擴展,可以讓事情簡單不少:

 extension Result where E == NoError {
     var value: T {
         if case .success(let v) = self {
             return v
         }
         fatalError("Never happen")
     }
 }
 run(after: 2) {
     // $0.value is the timer object
     print($0.value)
 }

這個 Timer 的例子雖然很簡單,但是可能實際上意義不大,因為我們可以直接使用 Timer.scheduledTimer 并使用簡單的 block 完成。但是當回調(diào) block 有多個參數(shù)時,或者需要鏈式調(diào)用 (比如為 Result 添加 map,filter 之類的支持時),類似 NoError 這樣的擴展方式就會很有用。

在 NSHipster 里有一篇關(guān)于 Never 的文章,提到使用 Never 來代表無值的方式。其中就給出了一個和 Result 一起使用的例子。我們只需要使 extension Never: Error {} 就可以將它指定為 Result<T, E: Error> 的第二個類型參數(shù),從而去除掉代碼中對 .failure case 的判斷。這是比 NoError 更好的一種方式。

當然,如果你需要一個只會失敗不會成功的 Result 的話,也可以將 Never 放到第一個類型參數(shù)的位置:Result<Never, E: Error>

缺點

與 Cocoa 兼容不良

由于歷史原因,Cocoa API 中表達的錯誤都是”無類型“的 NSError 的。如果你跳出 Swift 標準庫,要去使用 Cocoa 的方法 (對于在 Apple 平臺開發(fā)來說,這簡直是一定的),就不得不面臨這個問題。很多時候,你可能會被寫成 Result<SomeValue, NSError> 的形式,這樣我們上面提到的優(yōu)點幾乎就喪失殆盡了。

可能需要多層嵌套或者封裝

即使對于限定在 Swift 標準庫的情況來說,也有可能存在某個 API 產(chǎn)生若干種不同的錯誤的情況。如果想要完整地按照類型處理這些情況,我們可能會需要將錯誤嵌套起來:

 // 用戶注冊可能產(chǎn)生的錯誤
 // 當用戶注冊的請求完成且返回有效數(shù)據(jù),但數(shù)據(jù)表明注冊失敗時觸發(fā)
 enum UserRegisterError: Error {
     case duplicatedUsername
     case unsafePassword
 }
 // Server API 整體可能產(chǎn)生的錯誤
 // 當請求成功但 response status code 不是 200 時觸發(fā)
 enum APIResponseError: Error {
     case permissionDenied // 403
     case entryNotFound    // 404
     case serverDied       // 500
 }
 // 所有的 API Client 可能發(fā)生的錯誤
 enum APIClientError: Error {
     // 沒有得到響應
     case requestTimeout
     // 得到了響應,但是 HTTP Status Code 非 200
     case apiFailed(APIResponseError)
     // 得到了響應且為 200,但數(shù)據(jù)無法解析為期望數(shù)據(jù)
     case invalidResponse(Data)
     // 請求和響應一切正常,但 API 的結(jié)果是失敗 (比如注冊不成功)
     case apiResultFailed(Error)
 }

上面的錯誤嵌套比較幼稚。更好的類型結(jié)構(gòu)是將 UserRegisterError 和 APIResponseError 定義到 APIClientError 里,另外,因為不會直接拋出,因此沒有必要讓 UserRegisterError 和 APIResponseError 遵守 Error 協(xié)議,它們只需要承擔說明錯誤原因的任務(wù)即可。

對這幾個類型加以整理,并重新命名,現(xiàn)在我認為比較合理的錯誤定義如下 (為了簡短一些,我去除了注釋):

enum APIClientError: Error {
    enum ResponseErrorReason {
        case permissionDenied
        case entryNotFound
        case serverDied
    }
    enum ResultErrorReason {
        enum UserRegisterError {
            case duplicatedUsername
            case unsafePassword
        }
        case userRegisterError(UserRegisterError)
    }
    case requestTimeout
    case apiFailed(ResponseErrorReason)
    case invalidResponse(Data)
    case apiResultFailed(ResultErrorReason)
}

當然,如果隨著嵌套過深而縮進變多時,你也可以把內(nèi)嵌的 Reason enum 放到 APIClientError 的 extension 里去。

上面的 APIClientError 涵蓋了進行一次 API 請求時所有可能的錯誤,但是這套方式在使用時會很痛苦:

 API.send(request) { result in
     switch result {
     case .success(let response): //...
     case .failure(let error):
         switch error {
         case .requestTimeout: print("Timeout!")
         case .apiFailed(let apiFailedError):
             switch apiFailedError: {
                 case .permissionDenied: print("403")
                 case .entryNotFound: print("404")
                 case .serverDied: print("500")
             }
         case .invalidResponse(let data):
             print("Invalid response body data: \(data)")
         case .apiResultFailed(let apiResultError):
             if let apiResultError = apiResultError as? UserRegisterError {
                 switch apiResultError {
                     case .duplicatedUsername: print("User already exists.")
                     case .unsafePassword: print("Password too simple.")
                 }
             }
         }
     }
 }

相信我,你不會想要寫這種代碼的。

經(jīng)過半年的實踐,事實是我發(fā)現(xiàn)這樣的代碼并沒有想象中的麻煩,而它帶來的好處遠遠超過所造成的不便。

這里代碼中有唯一一個 as? 對 UserRegisterError 的轉(zhuǎn)換,如果采用更上面引用中定義的 ResultErrorReason,則可以去除這個類型轉(zhuǎn)換,而使類型系統(tǒng)覆蓋到整個錯誤處理中。

相較于對每個 API 都寫這樣一堆錯誤處理的代碼,我們顯然更傾向于集中在一個地方處理這些錯誤,這在某種程度上“強迫”我們思考如何將錯誤處理的代碼抽象化和一般化,對于減少冗余和改善設(shè)計是有好處的。另外,在設(shè)計 API 時,我們可以提供一系列的便捷方法,來讓 API 的用戶能很快定位到某幾個特定的感興趣的錯誤,并作出處理。比如:

extension APIClientError {
    var isLoginRequired: Bool {
        if case .apiFailed(.permissionDenied) = self {
            return true
        }
        return false
    }
}

用 error.isLoginRequired 即可迅速確定是否是由于用戶權(quán)限不足,需要登錄,產(chǎn)生的錯誤。這部分內(nèi)容可以由 API 的提供者主動定義 (這樣做也起到一種指導作用,來告訴 API 用戶到底哪些錯誤是特別值得關(guān)心的),也可以由使用者在之后自行進行擴展。

另一種”方便“的做法是使用像是 AnyError 的類型來對 Error 提供封裝:

 struct AnyError: Error {
     let error: Error
 }

這可以把任意 Error 封裝并作為 Result<Value, AnyError> 的 .failure 成員進行使用。但是這時 Result<T, E: Error> 中的 E 幾乎就沒有意義了。

Swift 中存在不少 Any 開頭的類型,比如 AnyIteratorAnyCollection,AnyIndex 等等。這些類型起到的作用是類型抹消,有它們存在的歷史原因,但是隨著 Swift 的發(fā)展,特別是加入了 Conditional Conformance 以后,這一系列 Any 類型存在的意義就變小了。

使用 AnyError 來進行封裝 (或者說對具體 Error 類型進行抹消),可以讓我們拋出任意類型的錯誤。這更多的是一種對現(xiàn)有 Cocoa API 的妥協(xié)。對于純 Swift 環(huán)境來說,AnyError 并不是理想中應該存在的類型。因此如果你選擇了 Result<T, E: Error> 的話,我們就應該盡可能避免拋出這種無類型的錯誤。

那問題就回到了,對于 Cocoa API 拋出的錯誤 (也就是以前的 NSError),我們應該怎樣處理?一種方式是按照文檔進行封裝,比如將所有 NSURLSessionError 歸類到一個 URLSessionErrorReason,然后把從 Cocoa 得到的 NSError 作為關(guān)聯(lián)值傳遞給使用者;另一種方式是在拋出給 API 使用者之前,在內(nèi)部就對這個 Cocoa 錯誤進行“消化”,將它轉(zhuǎn)換為有意義的特定的某個已經(jīng)存在的 Error Reason。后者雖然減輕了 API 使用者的壓力,但是勢必會丟失一些信息,所以如果沒有特別理由的話,第一種的做法可能更加合適。

  • 錯誤處理的 API 兼容存在風險
  • 現(xiàn)在來說,為 enum 添加一個 case 的操作是無法做到 API 兼容的。使用側(cè)如果枚舉了所有的 case 進行處理的話,在 case 增加時,原來的代碼將無法編譯。(不過對于錯誤處理來說,這倒可能對強制開發(fā)者對應錯誤情況是一種督促 233..)
  • 如果一個框架或者一套 API 嚴格遵守 semantic version 的話,這意味著一個大版本的更新。但是其實我們都心知肚明,增加一個之前可能忽略了的錯誤情況,卻帶來一個大版本更新,帶來的麻煩顯然得不償失。
  • Swift 社區(qū)現(xiàn)在對于增加 enum case 時如何保持 API compatibility 也有一個成熟而且已經(jīng)被接受了的提案。將 enum 定義為 frozen 和 nonFrozen,并對 nonFrozen 的 enum 使用 unknown 關(guān)鍵字來保證源碼兼容。我們在下個版本的 Swift 中應該就可以使用這個特性了。

Result

不帶 Error 類型的優(yōu)缺點正好和上面相反。

相對于 Result<T, E: Error>,Result<T> 不在外部對錯誤類型提出任何限制,API 的創(chuàng)建者可以擺脫 AnyError,直接將任意的 Error 作為 .failure 值使用。

但同時很明顯,相對的,一個最重要的特性缺失就是我們無法針對錯誤類型的特點為 Result 進行擴展了。

結(jié)論

因為 Swift 并沒有提供使用協(xié)議類型作為泛型中特化的具體類型的支持,這導致在 API 的強類型嚴謹性和靈活性上無法取得兩端都完美的做法。硬要對比的話,可能 Result<T, E: Error> 對使用者更加友好一些,因為它提供了一個定義錯誤類型的機會。但是相對地,如果創(chuàng)建者沒有掌握好錯誤類型的程度,而將多層嵌套的錯誤傳遞時,反而會增加使用者的負擔。同時,由于錯誤類型被限定,導致 API 的變更要比只定義了結(jié)果類型的 Result<T> 困難得多。

不過 Result 暫時看起來不太可能被添加到標準庫中,因為它背后存在一個更大的協(xié)程和整個語言的異步模型該如何處理錯誤的話題。在有更多的實踐和討論之前,如果沒有革 命性和語言創(chuàng)新的話,對如何進行處理的話題,恐怕很難達成完美的共識。

結(jié)論:錯誤處理真的是一件相當艱難的事情。

最近這半年,在不同項目里,我對 Result<T, E: Error> 和 Result<T> 兩種方式都進行了一些嘗試?,F(xiàn)在看來,我會更多地選擇帶有錯誤類型的 Result<T, E: Error> 的形式,特別是在開發(fā)框架或者需要嚴謹?shù)腻e誤處理的時候。將框架中可能拋出的錯誤進行統(tǒng)一封裝,可以很大程度上減輕使用者的壓力,讓錯誤處理的代碼更加健壯。如果設(shè)計得當,它也能提供更好的擴展性。

以上就是Swift設(shè)計思想Result<T>與Result<T, E: Error>類型解析的詳細內(nèi)容,更多關(guān)于Swift Result類型設(shè)計的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Swift和C語言混合編程教程

    Swift和C語言混合編程教程

    這篇文章主要介紹了Swift和C語言混合編程教程,介紹基本數(shù)據(jù)類型對比、指針、常量等內(nèi)容,需要的朋友可以參考下
    2014-07-07
  • Swift中static和class關(guān)鍵字的深入講解

    Swift中static和class關(guān)鍵字的深入講解

    這篇文章主要給大家介紹了關(guān)于Swift中static和class關(guān)鍵字的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者使用Java具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧
    2019-03-03
  • Swift中類與結(jié)構(gòu)的初始化示例解析

    Swift中類與結(jié)構(gòu)的初始化示例解析

    這篇文章主要為大家介紹了Swift中類與結(jié)構(gòu)的初始化解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步早日升職加薪
    2022-03-03
  • Swift中的高階函數(shù)功能作用示例詳解

    Swift中的高階函數(shù)功能作用示例詳解

    這篇文章主要為大家介紹了Swift中的高階函數(shù)功能作用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-03-03
  • Swift方法調(diào)度之類的普通方法底層探究

    Swift方法調(diào)度之類的普通方法底層探究

    這篇文章主要介紹了Swift-方法調(diào)度-類的普通方法底層探究,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-11-11
  • IOS 實現(xiàn)簡單的彈幕功能

    IOS 實現(xiàn)簡單的彈幕功能

    本文主要介紹IOS 實現(xiàn)彈幕功能,這里給大家一個實例來展現(xiàn)彈幕功能,有需要的小伙伴可以參考下
    2016-07-07
  • Swift教程之類與結(jié)構(gòu)詳解

    Swift教程之類與結(jié)構(gòu)詳解

    這篇文章主要介紹了Swift教程之類與結(jié)構(gòu)詳解,本文講解了類和結(jié)構(gòu)的異同、結(jié)構(gòu)和枚舉類型是數(shù)值類型、類是引用類型、如何選擇使用類還是結(jié)構(gòu)、集合類型的賦值和復制操作等內(nèi)容,需要的朋友可以參考下
    2015-01-01
  • Swift調(diào)用Objective-C代碼

    Swift調(diào)用Objective-C代碼

    目前Swift語言所編寫的應用才剛剛可以使用Xcode 6 GM版本提交,而Objective-C作為蘋果的主開發(fā)語言存在了很多年了。目前尚無成熟的Swift庫可用,所以當前編寫應用可以說基本離不開調(diào)用Objective-C代碼的情況。
    2014-09-09
  • Swift中風味各異的類型擦除實例詳解

    Swift中風味各異的類型擦除實例詳解

    你也許曾聽過類型擦除,甚至也使用過標準庫提供的類型擦除類型如 AnySequence,下面這篇文章主要給大家介紹了關(guān)于Swift中風味各異的類型擦除的相關(guān)資料,需要的朋友可以參考下
    2022-04-04
  • Swift中用到extension的一些基本的擴展功能講解

    Swift中用到extension的一些基本的擴展功能講解

    這篇文章主要介紹了Swift的一些基本的擴展功能,即extension關(guān)鍵字的使用,需要的朋友可以參考下
    2015-11-11

最新評論