Swift 基于閉包的類型擦除
與許多其他語(yǔ)言相比,使Swift更加安全,更不易出錯(cuò)的原因之一是其先進(jìn)的(并且在某種程度上是不容忍的)類型系統(tǒng)。這是一種語(yǔ)言功能,有時(shí)可能會(huì)給人留下深刻的印象,使您的工作效率提高很多,而有時(shí)卻令人沮喪。
今天,我想重點(diǎn)介紹在 Swift 中處理泛型時(shí)可能發(fā)生的一種情況,以及我通常如何使用基于閉包的類型擦除技術(shù)來(lái)解決這種情況。
假設(shè)我們要編寫一個(gè)類,使我們可以通過(guò)網(wǎng)絡(luò)加載模型。由于我們不想為應(yīng)用程序中的每個(gè)模型都復(fù)制此類,因此我們選擇使其成為泛型類,如下所示:
- class ModelLoader<T: Unboxable & Requestable> {
- func load(completionHandler: (Result<T>) -> Void) {
- networkService.loadData(from: T.requestURL) { data in
- do {
- try completionHandler(.success(unbox(data: data)))
- } catch {
- let error = ModelLoadingError.unboxingFailed(error)
- completionHandler(.error(error))
- }
- }
- }
- }
到目前為止,我們現(xiàn)在有了一個(gè) ModelLoader,它能夠加載任何模型(只要它是遵守 Unboxable 協(xié)議的),并且能夠向我們提供requestURL。但是,我們還希望啟用使用此模型加載器的代碼易于測(cè)試,因此我們將其API提取到一個(gè)協(xié)議中:
- protocol ModelLoading {
- associatedtype Model
- func load(completionHandler: (Result<Model>) -> Void)
- }
這和依賴注入一起使我們能夠輕松地在測(cè)試中模擬我們的模型加載API。但這帶來(lái)了一些復(fù)雜性——在每當(dāng)我們要使用此API時(shí),我們現(xiàn)在都必須將其稱為協(xié)議 ModelLoading,該協(xié)議具有相關(guān)的類型要求。這意味著僅引用 ModelLoading 是不夠的,因?yàn)樵跊]有更多信息的情況下編譯器無(wú)法推斷其關(guān)聯(lián)類型。因此,嘗試執(zhí)行以下操作:
- class ViewController: UIViewController {
- init(modelLoader: ModelLoading) {
- ...
- }
- }
會(huì)給我們這個(gè)錯(cuò)誤:
- Protocol 'ModelLoading' can only be used as a generic constraint because it as Self or associated type requirements
但不用擔(dān)心,我們可以通過(guò)使用泛型輕松擺脫此錯(cuò)誤,強(qiáng)制執(zhí)行符合 Modelloading 的具體類型將由API用戶指定,并且它將加載我們期待的模型。像這樣:
- class ViewController: UIViewController {
- init<T: ModelLoading>(modelLoader: T) where T.Model == MyModel {
- ...
- }
- }
這是有效的,但由于我們還希望在我們的視圖控制器中引用我們的模型加載程序,我們需要能夠指定屬性的類型。 T 只在我們的初始化程序的上下文中知道,因此我們無(wú)法定義T類型的屬性,除非我們使視圖控制器類本身成為泛型 - 這將非常迅速使我們進(jìn)一步陷入到處都是通用課程的兔子洞中(down into a rabit hole 出自愛麗絲夢(mèng)游記,意只簡(jiǎn)單的事情變得越來(lái)來(lái)復(fù)雜和荒謬)。
相反,讓我們使用類型擦除,使我們能夠保存某種 T 的引用,而無(wú)需實(shí)際使用其類型。這可以通過(guò)創(chuàng)建擦除類型的類,例如 包裝類 來(lái)完成:
- class AnyModelLoader<T>: ModelLoading {
- typealias CompletionHandler = (Result<T>) -> Void
- private let loadingClosure: (CompletionHandler) -> Void
- init<L: ModelLoading>(loader: L) where L.Model == T {
- loadingClosure = loader.load
- }
- func load(completionHandler: CompletionHandler) {
- loadingClosure(completionHandler)
- }
- }
以上這種類型擦除技術(shù),其實(shí)在 Swift 標(biāo)準(zhǔn)庫(kù)中也很常用,例如在 AnySequence 類型中?;旧?,您將關(guān)聯(lián)值要求的協(xié)議包裝為泛型類型,然后您可以直接使用它而無(wú)需使使用它的類也是泛型的。
我們現(xiàn)在可以更新我們之前的 ViewController,使用 AnyModelloader:
- class ViewController: UIViewController {
- private let modelLoader: AnyModelLoader<MyModel>
- init<T: ModelLoading>(modelLoader: T) where T.Model == MyModel {
- self.modelLoader = AnyModelLoader(loader: modelLoader)
- super.init(nibName: nil, bundle: nil)
- }
- }
好了!我們現(xiàn)在擁有一個(gè)面向協(xié)議的API,具有易于Mock的特性,且仍然可以在普通類中使用,這歸功于類型擦除。
現(xiàn)在,獎(jiǎng)勵(lì)時(shí)間的時(shí)間。上述技術(shù)實(shí)際上很好,但它確實(shí)涉及一個(gè)額外的步驟,為我們的代碼增加了一些復(fù)雜化。但是,事實(shí)證明,我們實(shí)際上可以直接在我們的視圖控制器中進(jìn)行基于閉合的類型擦除 ——而不是必須通過(guò) AnyModelloader 類。然后,我們的視圖控制器將如下所示:
- class ViewController: UIViewController {
- private let loadModel: ((Result<MyModel>) -> Void) -> Void
- init<T: ModelLoading>(modelLoader: T) where T.Model == MyModel {
- loadModel = modelLoader.load
- super.init(nibName: nil, bundle: nil)
- }
- }
與我們的類型擦除類 AnyModelloader 一樣,我們可以參考 load 函數(shù)作為閉包的實(shí)現(xiàn),并只需在我們的視圖控制器中保存引用?,F(xiàn)在,每當(dāng)我們想要加載模型時(shí),我們只需調(diào)用 loadmodel,就像我們的任何其他函數(shù)或閉包一樣:
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- loadModel { result in
- switch result {
- case .success(let model):
- render(model)
- case .error(let error):
- render(error)
- }
- }
- }
就是這樣!希望在處理Swift代碼中的泛型和協(xié)議時(shí),您可以找到上述技術(shù)。
本文轉(zhuǎn)載自微信公眾號(hào)「 Swift社區(qū)」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系 Swift社區(qū)公眾號(hào)。