Swift中風(fēng)味各異的類型擦除實(shí)例詳解
前言
Swift的總體目標(biāo)是既強(qiáng)大到可以用于底層系統(tǒng)編程,又足夠容易讓初學(xué)者學(xué)習(xí),這有時(shí)會(huì)導(dǎo)致相當(dāng)有趣的情況——當(dāng)Swift的類型系統(tǒng)的力量要求我們部署相當(dāng)高級(jí)的技術(shù)來解決乍一看可能更微不足道的問題。
大多數(shù)Swift開發(fā)人員會(huì)在某一時(shí)刻或另一時(shí)刻(通常是馬上,而不是日后)會(huì)遇到這樣一種情況,即需要某種形式的類型擦除才能引用通用協(xié)議。從本周開始,讓我們看一下是什么使類型擦除在Swift中成為必不可少的技術(shù),然后繼續(xù)探索實(shí)現(xiàn)它的不同 “風(fēng)味(Flavors)”,以及每種風(fēng)味為何各有優(yōu)缺點(diǎn)。
什么時(shí)候需要類型擦除?
一開始,“類型擦除”一詞似乎與 Swift 給我們的關(guān)注類型和編譯時(shí)類型安全性的第一感覺相反,因此,最好將其描述為隱藏類型,而不是完全擦除它們。目的是使我們能夠更輕松地與通用協(xié)議進(jìn)行交互,因?yàn)檫@些通用協(xié)議對(duì)將要實(shí)現(xiàn)它們的各種類型具有特定的要求。
以標(biāo)準(zhǔn)庫中的Equatable協(xié)議為例。由于所有目的都是為了根據(jù)相等性比較兩個(gè)相同類型的值,因此Self元類型為其唯一要求的參數(shù):
protocol Equatable { static func ==(lhs: Self, rhs: Self) -> Bool }
上面的代碼使任何類型都可以符合Equatable,同時(shí)仍然需要==運(yùn)算符兩側(cè)的值都為同一類型,因?yàn)樵趯?shí)現(xiàn)上述方法時(shí)符合協(xié)議的每種類型都必須“填寫”自己的類型:
extension User: Equatable { static func ==(lhs: User, rhs: User) -> Bool { return lhs.id == rhs.id } }
該方法的優(yōu)點(diǎn)在于,它不可能意外地比較兩個(gè)不相關(guān)的相等類型(例如 User 和 String ),但是,它也導(dǎo)致不可能將Equatable引用為獨(dú)立協(xié)議(例如創(chuàng)建 [Equatable] ),因?yàn)榫幾g器需要知道實(shí)際上確切符合協(xié)議的確切類型才能使用它。
當(dāng)協(xié)議包含關(guān)聯(lián)的類型時(shí),也是如此。例如,在這里我們定義了一個(gè)Request協(xié)議,使我們可以在一個(gè)統(tǒng)一的實(shí)現(xiàn)中隱藏各種形式的數(shù)據(jù)請(qǐng)求(例如網(wǎng)絡(luò)調(diào)用,數(shù)據(jù)庫查詢和緩存提取):
protocol Request { ? ? associatedtype Response ? ? associatedtype Error: Swift.Error ? ? typealias Handler = (Result<Response, Error>) -> Void ? ? func perform(then handler: @escaping Handler) }
上面的方法為我們提供了與Equatable相同的權(quán)衡方法——它非常強(qiáng)大,因?yàn)樗刮覀兡軌驗(yàn)槿魏晤愋偷恼?qǐng)求創(chuàng)建通用抽象,但也使得無法直接引用Request協(xié)議本身,例如這:
class RequestQueue { // 報(bào)錯(cuò): protocol 'Request' can only be used as a generic // constraint because it has Self or associated type requirements func add(_ request: Request, handler: @escaping Request.Handler) { ... } }
解決上述問題的一種方法是完全按照?qǐng)?bào)錯(cuò)消息的內(nèi)容進(jìn)行操作,即不直接引用Request,而是將其用作一般約束:
class RequestQueue { func add<R: Request>(_ request: R, handler: @escaping R.Handler) { ... } }
上面的方法起作用了,因?yàn)楝F(xiàn)在編譯器能夠保證所傳遞的處理程序確實(shí)與作為請(qǐng)求傳遞的Request實(shí)現(xiàn)兼容——因?yàn)樗鼈兌蓟诜盒蚏,而后者又被限制為符合Request協(xié)議。
但是,盡管我們解決了方法的簽名問題,但仍然無法對(duì)傳遞的請(qǐng)求進(jìn)行實(shí)際的處理,因?yàn)槲覀儫o法將其存儲(chǔ)為Request屬性或[Request]數(shù)組,這將使繼續(xù)構(gòu)建我們的RequestQueue變得困難。也就是說,除非我們開始進(jìn)行類型擦除。
通用包裝器類型擦除
我們將探討的第一種類型擦除實(shí)際上并沒有涉及擦除任何類型,而是將它們包裝在一個(gè)我們可以更容易引用的通用類型中。繼續(xù)從之前的RequestQueue示例開始,我們首先創(chuàng)建該包裝器類型——該包裝器類型將捕獲每個(gè)請(qǐng)求的perform方法作為閉包,以及在請(qǐng)求完成后應(yīng)調(diào)用的處理程序:
// 這將使我們將 Request 協(xié)議的實(shí)現(xiàn)包裝在一個(gè) // 與 Request 協(xié)議具有相同的響應(yīng)和錯(cuò)誤類型的泛型中 struct AnyRequest<Response, Error: Swift.Error> { ? ? typealias Handler = (Result<Response, Error>) -> Void ? ? let perform: (@escaping Handler) -> Void ? ? let handler: Handler }
接下來,我們還將把RequestQueue本身轉(zhuǎn)換為相同的Response和Error類型的泛型——使得編譯器可以保證所有關(guān)聯(lián)的類型和泛型類型對(duì)齊,從而使我們可以將請(qǐng)求存儲(chǔ)為獨(dú)立的引用并作為數(shù)組的一部分——像這樣:
class RequestQueue<Response, Error: Swift.Error> { ? ? private typealias TypeErasedRequest = AnyRequest<Response, Error> ? ? private var queue = [TypeErasedRequest]() ? ? private var ongoing: TypeErasedRequest? ? ? // 我們修改了'add'方法,以包含一個(gè)'where'子句, ? ? // 該子句確保傳遞的請(qǐng)求已關(guān)聯(lián)的類型與隊(duì)列的通用類型匹配。 ? ? func add<R: Request>( ? ? ? ? _ request: R, ? ? ? ? handler: @escaping R.Handler ? ? ) where R.Response == Response, R.Error == Error { ? ? ? ? //要執(zhí)行類型擦除,我們只需創(chuàng)建一個(gè)實(shí)例'AnyRequest', ? ? ? ? //然后將其傳遞給基礎(chǔ)請(qǐng)求將“perform”方法與處理程序一起作為閉包。 ? ? ? ? let typeErased = AnyRequest( ? ? ? ? ? ? perform: request.perform, ? ? ? ? ? ? handler: handler ? ? ? ? ) ? ? ? ? // 由于我們要實(shí)現(xiàn)隊(duì)列,因此我們不想一次有兩個(gè)請(qǐng)求, ? ? ? ? // 所以將請(qǐng)求保存下拉,以防稍后有一個(gè)正在執(zhí)行的請(qǐng)求。 ? ? ? ? guard ongoing == nil else { ? ? ? ? ? ? queue.append(typeErased) ? ? ? ? ? ? return ? ? ? ? } ? ? ? ? perform(typeErased) ? ? } ? ? private func perform(_ request: TypeErasedRequest) { ? ? ? ? ongoing = request ? ? ? ? request.perform { [weak self] result in ? ? ? ? ? ? request.handler(result) ? ? ? ? ? ? self?.ongoing = nil ? ? ? ? ? ? // 如果隊(duì)列不為空,則執(zhí)行下一個(gè)請(qǐng)求 ? ? ? ? ? ? ... ? ? ? ? } ? ? } }
請(qǐng)注意,上面的示例以及本文中的其他示例代碼都不是線程安全的——為了使事情變得簡(jiǎn)單。有關(guān)線程安全的更多信息,請(qǐng)查看“避免在Swift 中競(jìng)爭(zhēng)條件”。
上面的方法效果很好,但有一些缺點(diǎn)。我們不僅引入了新的AnyRequest類型,還需要將RequestQueue轉(zhuǎn)換為泛型。這給我們帶來了一點(diǎn)靈活性,因?yàn)槲覀儸F(xiàn)在只能將任何給定的隊(duì)列用于具有相同 響應(yīng)/錯(cuò)誤類型 組合的請(qǐng)求。具有諷刺意味的是,如果我們想組成多個(gè)實(shí)例,將來可能還需要我們自己實(shí)現(xiàn)隊(duì)列擦除。
閉包類型擦除
我們不引入包裝類型,而是讓我們看一下如何使用閉包來實(shí)現(xiàn)相同的類型擦除,同時(shí)還要使我們的RequestQueue非泛型且通用,足以用于不同類型的請(qǐng)求。
使用閉包擦除類型時(shí),其思想是捕獲在閉包內(nèi)部執(zhí)行操作所需的所有類型信息,并使該閉包僅接受非泛型(甚至是Void)輸入。這樣一來,我們就可以引用,存儲(chǔ)和傳遞該功能,而無需實(shí)際知道功能內(nèi)部會(huì)發(fā)生什么,從而為我們提供了更強(qiáng)大的靈活性。
更新RequestQueue以使用基于閉包的類型擦除的方法如下:
class RequestQueue { ? ? private var queue = [() -> Void]() ? ? private var isPerformingRequest = false ? ? func add<R: Request>(_ request: R, ? ? ? ? ? ? ? ? ? ? ? ? ?handler: @escaping R.Handler) { ? ? ? ? // 此閉包將同時(shí)捕獲請(qǐng)求及其處理程序,而不會(huì)暴露任何類型信息 ? ? ? ? // 在其外部,提供完全的類型擦除。 ? ? ? ? let typeErased = { ? ? ? ? ? ? request.perform { [weak self] result in ? ? ? ? ? ? ? ? handler(result) ? ? ? ? ? ? ? ? self?.isPerformingRequest = false ? ? ? ? ? ? ? ? self?.performNextIfNeeded() ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? queue.append(typeErased) ? ? ? ? performNextIfNeeded() ? ? } ? ? private func performNextIfNeeded() { ? ? ? ? guard !isPerformingRequest && !queue.isEmpty else { ? ? ? ? ? ? return ? ? ? ? } ? ? ? ? isPerformingRequest = true ? ? ? ? let closure = queue.removeFirst() ? ? ? ? closure() ? ? } }
雖然過分依賴閉包來捕獲功能和狀態(tài)有時(shí)會(huì)使我們的代碼難以調(diào)試,但也可能使完全封裝類型信息成為可能——使得像RequestQueue這樣的對(duì)象可以在沒有真正了解在底層工作的類型的任何細(xì)節(jié)的情況下進(jìn)行工作。
有關(guān)基于閉包的類型擦除及其更多不同方法的更多信息,請(qǐng)查看“Swift 使用閉包實(shí)現(xiàn)類型擦除”。
外部特化(External specialization)
到目前為止,我們已經(jīng)在RequestQueue本身中執(zhí)行了所有類型擦除,這有一些優(yōu)點(diǎn)——它可以讓任何外部代碼使用我們的隊(duì)列,而不需要知道我們使用什么類型的類型擦除。然而,有時(shí)在將協(xié)議實(shí)現(xiàn)傳遞給API之前進(jìn)行一些輕量級(jí)轉(zhuǎn)換,既可以使事情變得更簡(jiǎn)單,又可以巧妙地封裝類型擦除代碼本身。
對(duì)于我們的RequestQueue,一種方法是要求在將每個(gè)Request實(shí)現(xiàn)添加到隊(duì)列之前對(duì)其進(jìn)行特化——這將把它轉(zhuǎn)換為RequestOperation,如下所示:
struct RequestOperation { fileprivate let closure: (@escaping () -> Void) -> Void func perform(then handler: @escaping () -> Void) { closure(handler) } }
與我們之前使用閉包在RequestQueue中執(zhí)行類型擦除的方式類似,上面的RequestOperation類型將使我們能夠在擴(kuò)展Request時(shí)執(zhí)行該操作:
extension Request { func makeOperation(with handler: @escaping Handler) -> RequestOperation { return RequestOperation { finisher in // 我們其實(shí)想在這里捕獲'self',因?yàn)椴贿@樣話 // 我們將冒著無法保留基本請(qǐng)求的風(fēng)險(xiǎn)。 self.perform { result in handler(result) finisher() } } } }
上述方法的優(yōu)點(diǎn)在于,無論是公共API還是內(nèi)部實(shí)現(xiàn),它都讓我們的RequestQueue更加簡(jiǎn)單。它現(xiàn)在可以完全專注于作為一個(gè)隊(duì)列,而不必關(guān)心任何類型的類型擦除:
class RequestQueue { private var queue = [RequestOperation]() private var ongoing: RequestOperation? // 因?yàn)轭愋筒脸F(xiàn)在發(fā)生在request被傳遞給 queue 之前, // 它可以簡(jiǎn)單地接受一個(gè)具體的“RequestOperation”的實(shí)例。 func add(_ operation: RequestOperation) { guard ongoing == nil else { queue.append(operation) return } perform(operation) } private func perform(_ operation: RequestOperation) { ongoing = operation operation.perform { [weak self] in self?.ongoing = nil // 如果隊(duì)列不為空,則執(zhí)行下一個(gè)請(qǐng)求 ... } } }
然而,這里的缺點(diǎn)是,在將每個(gè)請(qǐng)求添加到隊(duì)列之前,我們必須手動(dòng)將其轉(zhuǎn)換為RequestOperation——雖然這不會(huì)在每個(gè)調(diào)用點(diǎn)添加大量代碼,但這取決于必須完成相同轉(zhuǎn)換的次數(shù),它最終可能會(huì)有點(diǎn)像樣板。
結(jié)語
盡管 Swift 提供了一個(gè)功能強(qiáng)大得難以置信的類型系統(tǒng),可以幫助我們避免大量的bug,但有時(shí)它會(huì)讓人覺得我們必須與系統(tǒng)抗?fàn)帲拍苁褂猛ㄓ脜f(xié)議之類的功能。必須進(jìn)行類型擦除最初看起來像是一件不必要的雜務(wù),但它也帶來了一些好處——比如從不需要關(guān)心這些類型的代碼中隱藏特定類型信息。
在未來,我們可能還會(huì)看到 Swift 中添加了新的特性,可以自動(dòng)化創(chuàng)建類型擦除包裝類型的過程,也可以通過使協(xié)議也被用作適當(dāng)?shù)姆盒?例如能夠定義像Request這樣的協(xié)議)來消除對(duì)它的大量需求,而不僅僅依賴于相關(guān)的類型)。
什么樣的類型擦除是最合適的——無論是現(xiàn)在還是將來——當(dāng)然很大程度上取決于上下文,以及我們的功能是否可以在閉包中輕松地執(zhí)行,或者完整包裝器類型或泛型是否更適合這個(gè)問題。
到此這篇關(guān)于Swift中風(fēng)味各異的類型擦除的文章就介紹到這了,更多相關(guān)Swift類型擦除內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Swift 圖表使用Foudation庫中測(cè)量類型詳解
這篇文章主要為大家介紹了Swift 圖表使用Foudation庫中測(cè)量類型詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10Swift、Objective-C、Cocoa混合編程設(shè)置指南
這篇文章主要介紹了Swift、Objective-C、Cocoa混合編程設(shè)置指南,需要的朋友可以參考下2014-07-07在Swift中使用KVO的細(xì)節(jié)以及內(nèi)部實(shí)現(xiàn)解析(推薦)
這篇文章主要介紹了在Swift中使用KVO的細(xì)節(jié)以及內(nèi)部實(shí)現(xiàn)解析,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07Swift使用transform 實(shí)現(xiàn)重復(fù)平移動(dòng)畫效果
這篇文章主要介紹了Swift使用transform 實(shí)現(xiàn)重復(fù)平移動(dòng)畫效果,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-07-07詳解Swift的switch...case語句中break關(guān)鍵字的用法
這篇文章主要介紹了Swift的switch...case語句中break關(guān)鍵字的用法,是Swift入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2016-04-04通過Notification.Name看Swift是如何優(yōu)雅的解決String硬編碼
這篇文章主要給大家介紹了通過Notification.Name看Swift是如何優(yōu)雅的解決String硬編碼的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-08-08