Swift設(shè)計(jì)思想Result<T>與Result<T,?E:?Error>類型解析
背景知識(shí)
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ù)比較復(fù)雜,比如這里有三個(gè)參數(shù):(Data?, URLResponse?, Error?)
,它們都是可選值。當(dāng) session 請(qǐng)求成功時(shí),Data
參數(shù)包含 response 中的數(shù)據(jù),Error
為 nil
;當(dāng)發(fā)生錯(cuò)誤時(shí),則正好相反,Error
指明具體的錯(cuò)誤 (由于歷史原因,它會(huì)是一個(gè) NSError
對(duì)象),Data
為 nil
。
關(guān)于這個(gè)事實(shí),dataTask(with:completionHandler:)
的文檔的 Discussion 部分有十分詳細(xì)的說明。另外,response: URLResponse?
相對(duì)復(fù)雜一些:不論是請(qǐng)求成功還是失敗,只要從 server 收到了 response
,它就會(huì)被包含在這個(gè)變量里。
這么做雖然看上去無害,但其實(shí)存在改善的余地。顯然 data
和 error
是互斥的:事實(shí)上是不可能存在 data
和 error
同時(shí)為 nil
或者同時(shí)非 nil
的情況的,但是編譯器卻無法靜態(tài)地確認(rèn)這個(gè)事實(shí)。編譯器沒有制止我們?cè)阱e(cuò)誤的 if
語句中對(duì) nil
值進(jìn)行解包,而這種行為將導(dǎo)致運(yùn)行時(shí)的意外崩潰。
我們可以通過一個(gè)簡單的封裝來改進(jìn)這個(gè)設(shè)計(jì):如果你實(shí)際寫過 Swift,可能已經(jīng)對(duì) Result
很熟悉了。它的思想非常簡單,用泛型將可能的返回值包裝起來,因?yàn)榻Y(jié)果是成功或者失敗二選一,所以我們可以藉此去除不必要的可選值。
enum Result<T, E: Error> { case success(T) case failure(E) }
把它運(yùn)用到 URLSession
中的話,包裝一下 URLSession
方法,上面調(diào)用可以變?yōu)椋?/p>
// 如果 Result 存在于標(biāo)準(zhǔn)庫的話, // 這部分代碼應(yīng)該由標(biāo)準(zhǔn)庫的 Foundataion 擴(kuò)展進(jìn)行實(shí)現(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)
這個(gè)類型是錯(cuò)誤的。Data
存在時(shí) URLResponse
一定存在,但是我們上面討論過,當(dāng) NSError
不為 nil
時(shí),URLResponse
也可能存在。原文代碼忽略了這個(gè)事實(shí),將導(dǎo)致 error 狀況時(shí)無法獲取到可能的 URLResponse
。正確的類型應(yīng)該是 (Result<(Data), NSError>, URLResponse?) -> Void
當(dāng)然,在回調(diào)中對(duì) result
的處理也需要對(duì)應(yīng)進(jìn)行修改。
調(diào)用的時(shí)候看起來很棒,我們可以避免檢查可選值的情況,讓編譯器保證在對(duì)應(yīng)的 case
分支中有確定的非可選值。這個(gè)設(shè)計(jì)在很多存在異步代碼的框架中被廣泛使用,比如 Swift Package Manager,Alamofire 等中都可覓其蹤。
上面代碼注釋中提到,「如果 Result 存在于標(biāo)準(zhǔn)庫的話,這部分代碼應(yīng)該由標(biāo)準(zhǔn)庫的 Foundataion 擴(kuò)展進(jìn)行實(shí)現(xiàn)」。但是考慮到原有的可選值參數(shù) ((Data?, URLResponse?, Error?)
) 作為回調(diào)的 API 將會(huì)共享同樣的函數(shù)名,所以上面的函數(shù)命名是不可取的,否則將導(dǎo)致沖突。在這類 public API 發(fā)布后,如何改善和迭代確實(shí)是個(gè)難題。一個(gè)可行的方法是把 Foundation 的 URLSession
deprecate 掉,提取出相關(guān)方法放到諸如 Network.framework 里,并讓它跨平臺(tái)。另一種可行方案是通過自動(dòng)轉(zhuǎn)換工具,強(qiáng)制 Swift 使用 Result
的回調(diào),并保持 OC 中的多參數(shù)回調(diào)。如果你正在打算使用 Result
改善現(xiàn)有設(shè)計(jì),并且需要考慮保持 API 的兼容性時(shí),這會(huì)是一個(gè)不小的挑戰(zhàn)。
錯(cuò)誤類型泛型參數(shù)
如此常用的一個(gè)可以改善設(shè)計(jì)的定義,為什么沒有存在于標(biāo)準(zhǔn)庫中呢?關(guān)于 Result
,其實(shí)已經(jīng)有相關(guān)的提案:
這個(gè)提案中值得注意的地方在于,Result
的泛型類型只對(duì)成功時(shí)的值進(jìn)行了類型約束,而忽略了錯(cuò)誤類型。給出的 Result
定義類似這樣:
enum Result<T> { case success(T) case failure(Error) }
很快,在 1 樓就有人質(zhì)疑,問這樣做的意義何在,因?yàn)楫吘购芏嘁汛嬖诘?nbsp;Result
實(shí)現(xiàn)都是包含了 Error
類型約束的。確定的 Error
類型也讓人在使用時(shí)多了一份“安全感”。
不過,其實(shí)我們實(shí)際類比一下 Swift 中已經(jīng)存在的錯(cuò)誤處理的設(shè)計(jì)。Swift 中的 Error
只是一個(gè)協(xié)議,在 throw 的時(shí)候,我們也并不會(huì)指明需要拋出的錯(cuò)誤的類型:
func methodCanThrow() throws { if somethingGoesWrong { // 在這里可以 throw 任意類型的 Error } } do { try methodCanThrow() } catch { if error is SomeErrorType { // ... } else if error is AnotherErrorType { // ... } }
但是,在帶有錯(cuò)誤類型約束的 Result<T, E: Error>
中,我們需要為 E
指定一個(gè)確定的錯(cuò)誤類型 (或者說,Swift 并不支持在特化時(shí)使用協(xié)議,Result<Response, Error>
這樣的類型是非法的)。這與現(xiàn)有的 Swift 錯(cuò)誤處理機(jī)制是背道而馳的。
關(guān)于 Swift 是否應(yīng)該拋出帶有類型的錯(cuò)誤,曾經(jīng)存在過一段時(shí)間的爭論。最終問題歸結(jié)于,如果一個(gè)函數(shù)可以拋出多種錯(cuò)誤 (不論是該函數(shù)自身產(chǎn)生的錯(cuò)誤,還是在函數(shù)中 try 其他函數(shù)時(shí)它們所帶來的更底層的錯(cuò)誤),那么 throws
語法將會(huì)變得非常復(fù)雜且不可控 (試想極端情況下某個(gè)函數(shù)可能會(huì)拋出數(shù)十種錯(cuò)誤)?,F(xiàn)在大家一致的看法是已有的用 protocol Error
來定義錯(cuò)誤的做法是可取的,而且這也編碼在了語言層級(jí),我們對(duì)「依賴編譯器來確定 try catch
會(huì)得到具體哪種錯(cuò)誤」這件事,幾乎無能為力。
另外,半開玩笑地說,要是 Swift 能類似這樣 extension Swift.Error: Swift.Error {}
,支持協(xié)議遵守自身協(xié)議的話,一切就很完美了,XD。
選擇哪個(gè)比較好?
兩種方式各有優(yōu)缺點(diǎn),特別在如果需要考慮 Cocoa 兼容的情況下,更并說不上哪一個(gè)就是完勝。這里將兩種寫法的優(yōu)缺點(diǎn)簡單比較一下,在實(shí)踐中最好是根據(jù)項(xiàng)目情況進(jìn)行選擇。
Result<T, E: Error>
優(yōu)點(diǎn)
可以由編譯器幫助進(jìn)行確定錯(cuò)誤類型
當(dāng)通過使用某個(gè)具體的錯(cuò)誤類型擴(kuò)展 Error
并將它設(shè)定為 Result
的錯(cuò)誤類型約束后,在判斷錯(cuò)誤時(shí)我們就可以比較容易地檢查錯(cuò)誤處理的完備情況了:
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
分支中的檢查變得相對(duì)容易。
這種編譯器的類型保證給了 API 使用者相當(dāng)強(qiáng)的信心,來從容進(jìn)行錯(cuò)誤處理。如果只是一個(gè)單純的 Error
類型,API 的用戶將面臨相當(dāng)大的壓力,因?yàn)椴环單臋n的話,就無從知曉需要處理怎樣的錯(cuò)誤,而更多的情況會(huì)是文檔和事實(shí)不匹配…
但是帶有類型的錯(cuò)誤就相當(dāng)容易了,查看該類型的 public member 就能知道會(huì)面臨的情況了。在制作和發(fā)布框架,以及提供給他人使用的 API 的時(shí)候,這一點(diǎn)非常重要。
按條件的協(xié)議擴(kuò)展
使用泛型約束的另一個(gè)好處是可以方便地對(duì)某些情況的 Result
進(jìn)行擴(kuò)展。
舉例來說,某些異步操作可能永遠(yuǎn)不會(huì)失敗,對(duì)于這些操作,我們沒有必要再使用 switch 去檢查分支情況。一個(gè)很好的例子就是 Timer
,我們?cè)O(shè)定一個(gè)在一段時(shí)間后執(zhí)行的 Timer 后,如果不考慮人為取消,這個(gè) Timer 總是可以正確執(zhí)行完畢,而不會(huì)發(fā)生任何錯(cuò)誤的。我們可能會(huì)選擇使用一個(gè)特定的類型來代表這種情況:
enum NoError: Error {} func run(after: TimeInterval, done: @escaping (Result<Timer, NoError>) -> Void ) { Timer.scheduledTimer(withTimeInterval: after, repeats: false) { timer in done(.success(timer)) } }
在使用的時(shí)候,本來我們需要這樣的代碼:
run(after: 2) { result in switch result { case .success(let timer): print(timer) case .failure: fatalError("Never happen") } }
但是,通過對(duì) E
為 NoError
的情況添加擴(kuò)展,可以讓事情簡單不少:
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) }
這個(gè) Timer
的例子雖然很簡單,但是可能實(shí)際上意義不大,因?yàn)槲覀兛梢灾苯邮褂?nbsp;Timer.scheduledTimer
并使用簡單的 block 完成。但是當(dāng)回調(diào) block 有多個(gè)參數(shù)時(shí),或者需要鏈?zhǔn)秸{(diào)用 (比如為 Result
添加 map
,filter
之類的支持時(shí)),類似 NoError
這樣的擴(kuò)展方式就會(huì)很有用。
在 NSHipster 里有一篇關(guān)于 Never 的文章,提到使用 Never
來代表無值的方式。其中就給出了一個(gè)和 Result
一起使用的例子。我們只需要使 extension Never: Error {}
就可以將它指定為 Result<T, E: Error>
的第二個(gè)類型參數(shù),從而去除掉代碼中對(duì) .failure
case 的判斷。這是比 NoError
更好的一種方式。
當(dāng)然,如果你需要一個(gè)只會(huì)失敗不會(huì)成功的 Result
的話,也可以將 Never
放到第一個(gè)類型參數(shù)的位置:Result<Never, E: Error>
。
缺點(diǎn)
與 Cocoa 兼容不良
由于歷史原因,Cocoa API 中表達(dá)的錯(cuò)誤都是”無類型“的 NSError
的。如果你跳出 Swift 標(biāo)準(zhǔn)庫,要去使用 Cocoa 的方法 (對(duì)于在 Apple 平臺(tái)開發(fā)來說,這簡直是一定的),就不得不面臨這個(gè)問題。很多時(shí)候,你可能會(huì)被寫成 Result<SomeValue, NSError>
的形式,這樣我們上面提到的優(yōu)點(diǎn)幾乎就喪失殆盡了。
可能需要多層嵌套或者封裝
即使對(duì)于限定在 Swift 標(biāo)準(zhǔn)庫的情況來說,也有可能存在某個(gè) API 產(chǎn)生若干種不同的錯(cuò)誤的情況。如果想要完整地按照類型處理這些情況,我們可能會(huì)需要將錯(cuò)誤嵌套起來:
// 用戶注冊(cè)可能產(chǎn)生的錯(cuò)誤 // 當(dāng)用戶注冊(cè)的請(qǐng)求完成且返回有效數(shù)據(jù),但數(shù)據(jù)表明注冊(cè)失敗時(shí)觸發(fā) enum UserRegisterError: Error { case duplicatedUsername case unsafePassword } // Server API 整體可能產(chǎn)生的錯(cuò)誤 // 當(dāng)請(qǐng)求成功但 response status code 不是 200 時(shí)觸發(fā) enum APIResponseError: Error { case permissionDenied // 403 case entryNotFound // 404 case serverDied // 500 } // 所有的 API Client 可能發(fā)生的錯(cuò)誤 enum APIClientError: Error { // 沒有得到響應(yīng) case requestTimeout // 得到了響應(yīng),但是 HTTP Status Code 非 200 case apiFailed(APIResponseError) // 得到了響應(yīng)且為 200,但數(shù)據(jù)無法解析為期望數(shù)據(jù) case invalidResponse(Data) // 請(qǐng)求和響應(yīng)一切正常,但 API 的結(jié)果是失敗 (比如注冊(cè)不成功) case apiResultFailed(Error) }
上面的錯(cuò)誤嵌套比較幼稚。更好的類型結(jié)構(gòu)是將 UserRegisterError
和 APIResponseError
定義到 APIClientError
里,另外,因?yàn)椴粫?huì)直接拋出,因此沒有必要讓 UserRegisterError
和 APIResponseError
遵守 Error
協(xié)議,它們只需要承擔(dān)說明錯(cuò)誤原因的任務(wù)即可。
對(duì)這幾個(gè)類型加以整理,并重新命名,現(xiàn)在我認(rèn)為比較合理的錯(cuò)誤定義如下 (為了簡短一些,我去除了注釋):
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) }
當(dāng)然,如果隨著嵌套過深而縮進(jìn)變多時(shí),你也可以把內(nèi)嵌的 Reason
enum 放到 APIClientError
的 extension 里去。
上面的 APIClientError
涵蓋了進(jìn)行一次 API 請(qǐng)求時(shí)所有可能的錯(cuò)誤,但是這套方式在使用時(shí)會(huì)很痛苦:
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.") } } } } }
相信我,你不會(huì)想要寫這種代碼的。
經(jīng)過半年的實(shí)踐,事實(shí)是我發(fā)現(xiàn)這樣的代碼并沒有想象中的麻煩,而它帶來的好處遠(yuǎn)遠(yuǎn)超過所造成的不便。
這里代碼中有唯一一個(gè) as?
對(duì) UserRegisterError
的轉(zhuǎn)換,如果采用更上面引用中定義的 ResultErrorReason
,則可以去除這個(gè)類型轉(zhuǎn)換,而使類型系統(tǒng)覆蓋到整個(gè)錯(cuò)誤處理中。
相較于對(duì)每個(gè) API 都寫這樣一堆錯(cuò)誤處理的代碼,我們顯然更傾向于集中在一個(gè)地方處理這些錯(cuò)誤,這在某種程度上“強(qiáng)迫”我們思考如何將錯(cuò)誤處理的代碼抽象化和一般化,對(duì)于減少冗余和改善設(shè)計(jì)是有好處的。另外,在設(shè)計(jì) API 時(shí),我們可以提供一系列的便捷方法,來讓 API 的用戶能很快定位到某幾個(gè)特定的感興趣的錯(cuò)誤,并作出處理。比如:
extension APIClientError { var isLoginRequired: Bool { if case .apiFailed(.permissionDenied) = self { return true } return false } }
用 error.isLoginRequired
即可迅速確定是否是由于用戶權(quán)限不足,需要登錄,產(chǎn)生的錯(cuò)誤。這部分內(nèi)容可以由 API 的提供者主動(dòng)定義 (這樣做也起到一種指導(dǎo)作用,來告訴 API 用戶到底哪些錯(cuò)誤是特別值得關(guān)心的),也可以由使用者在之后自行進(jìn)行擴(kuò)展。
另一種”方便“的做法是使用像是 AnyError
的類型來對(duì) Error
提供封裝:
struct AnyError: Error { let error: Error }
這可以把任意 Error
封裝并作為 Result<Value, AnyError>
的 .failure
成員進(jìn)行使用。但是這時(shí) Result<T, E: Error>
中的 E
幾乎就沒有意義了。
Swift 中存在不少 Any
開頭的類型,比如 AnyIterator
,AnyCollection
,AnyIndex
等等。這些類型起到的作用是類型抹消,有它們存在的歷史原因,但是隨著 Swift 的發(fā)展,特別是加入了 Conditional Conformance 以后,這一系列 Any
類型存在的意義就變小了。
使用 AnyError
來進(jìn)行封裝 (或者說對(duì)具體 Error 類型進(jìn)行抹消),可以讓我們拋出任意類型的錯(cuò)誤。這更多的是一種對(duì)現(xiàn)有 Cocoa API 的妥協(xié)。對(duì)于純 Swift 環(huán)境來說,AnyError
并不是理想中應(yīng)該存在的類型。因此如果你選擇了 Result<T, E: Error>
的話,我們就應(yīng)該盡可能避免拋出這種無類型的錯(cuò)誤。
那問題就回到了,對(duì)于 Cocoa API 拋出的錯(cuò)誤 (也就是以前的 NSError
),我們應(yīng)該怎樣處理?一種方式是按照文檔進(jìn)行封裝,比如將所有 NSURLSessionError
歸類到一個(gè) URLSessionErrorReason
,然后把從 Cocoa 得到的 NSError
作為關(guān)聯(lián)值傳遞給使用者;另一種方式是在拋出給 API 使用者之前,在內(nèi)部就對(duì)這個(gè) Cocoa 錯(cuò)誤進(jìn)行“消化”,將它轉(zhuǎn)換為有意義的特定的某個(gè)已經(jīng)存在的 Error Reason。后者雖然減輕了 API 使用者的壓力,但是勢必會(huì)丟失一些信息,所以如果沒有特別理由的話,第一種的做法可能更加合適。
- 錯(cuò)誤處理的 API 兼容存在風(fēng)險(xiǎn)
- 現(xiàn)在來說,為 enum 添加一個(gè) case 的操作是無法做到 API 兼容的。使用側(cè)如果枚舉了所有的 case 進(jìn)行處理的話,在 case 增加時(shí),原來的代碼將無法編譯。(不過對(duì)于錯(cuò)誤處理來說,這倒可能對(duì)強(qiáng)制開發(fā)者對(duì)應(yīng)錯(cuò)誤情況是一種督促 233..)
- 如果一個(gè)框架或者一套 API 嚴(yán)格遵守 semantic version 的話,這意味著一個(gè)大版本的更新。但是其實(shí)我們都心知肚明,增加一個(gè)之前可能忽略了的錯(cuò)誤情況,卻帶來一個(gè)大版本更新,帶來的麻煩顯然得不償失。
- Swift 社區(qū)現(xiàn)在對(duì)于增加 enum case 時(shí)如何保持 API compatibility 也有一個(gè)成熟而且已經(jīng)被接受了的提案。將 enum 定義為
frozen
和nonFrozen
,并對(duì)nonFrozen
的 enum 使用unknown
關(guān)鍵字來保證源碼兼容。我們?cè)谙聜€(gè)版本的 Swift 中應(yīng)該就可以使用這個(gè)特性了。
Result
不帶 Error
類型的優(yōu)缺點(diǎn)正好和上面相反。
相對(duì)于 Result<T, E: Error>
,Result<T>
不在外部對(duì)錯(cuò)誤類型提出任何限制,API 的創(chuàng)建者可以擺脫 AnyError
,直接將任意的 Error
作為 .failure
值使用。
但同時(shí)很明顯,相對(duì)的,一個(gè)最重要的特性缺失就是我們無法針對(duì)錯(cuò)誤類型的特點(diǎn)為 Result
進(jìn)行擴(kuò)展了。
結(jié)論
因?yàn)?Swift 并沒有提供使用協(xié)議類型作為泛型中特化的具體類型的支持,這導(dǎo)致在 API 的強(qiáng)類型嚴(yán)謹(jǐn)性和靈活性上無法取得兩端都完美的做法。硬要對(duì)比的話,可能 Result<T, E: Error>
對(duì)使用者更加友好一些,因?yàn)樗峁┝艘粋€(gè)定義錯(cuò)誤類型的機(jī)會(huì)。但是相對(duì)地,如果創(chuàng)建者沒有掌握好錯(cuò)誤類型的程度,而將多層嵌套的錯(cuò)誤傳遞時(shí),反而會(huì)增加使用者的負(fù)擔(dān)。同時(shí),由于錯(cuò)誤類型被限定,導(dǎo)致 API 的變更要比只定義了結(jié)果類型的 Result<T>
困難得多。
不過 Result
暫時(shí)看起來不太可能被添加到標(biāo)準(zhǔn)庫中,因?yàn)樗澈蟠嬖谝粋€(gè)更大的協(xié)程和整個(gè)語言的異步模型該如何處理錯(cuò)誤的話題。在有更多的實(shí)踐和討論之前,如果沒有革 命性和語言創(chuàng)新的話,對(duì)如何進(jìn)行處理的話題,恐怕很難達(dá)成完美的共識(shí)。
結(jié)論:錯(cuò)誤處理真的是一件相當(dāng)艱難的事情。
最近這半年,在不同項(xiàng)目里,我對(duì) Result<T, E: Error>
和 Result<T>
兩種方式都進(jìn)行了一些嘗試。現(xiàn)在看來,我會(huì)更多地選擇帶有錯(cuò)誤類型的 Result<T, E: Error>
的形式,特別是在開發(fā)框架或者需要嚴(yán)謹(jǐn)?shù)腻e(cuò)誤處理的時(shí)候。將框架中可能拋出的錯(cuò)誤進(jìn)行統(tǒng)一封裝,可以很大程度上減輕使用者的壓力,讓錯(cuò)誤處理的代碼更加健壯。如果設(shè)計(jì)得當(dāng),它也能提供更好的擴(kuò)展性。
以上就是Swift設(shè)計(jì)思想Result<T>與Result<T, E: Error>類型解析的詳細(xì)內(nèi)容,更多關(guān)于Swift Result類型設(shè)計(jì)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Swift中static和class關(guān)鍵字的深入講解
這篇文章主要給大家介紹了關(guān)于Swift中static和class關(guān)鍵字的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者使用Java具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03Swift中風(fēng)味各異的類型擦除實(shí)例詳解
你也許曾聽過類型擦除,甚至也使用過標(biāo)準(zhǔn)庫提供的類型擦除類型如 AnySequence,下面這篇文章主要給大家介紹了關(guān)于Swift中風(fēng)味各異的類型擦除的相關(guān)資料,需要的朋友可以參考下2022-04-04Swift中用到extension的一些基本的擴(kuò)展功能講解
這篇文章主要介紹了Swift的一些基本的擴(kuò)展功能,即extension關(guān)鍵字的使用,需要的朋友可以參考下2015-11-11