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

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

 更新時間:2022年04月01日 08:39:18   作者:Swift君  
你也許曾聽過類型擦除,甚至也使用過標準庫提供的類型擦除類型如 AnySequence,下面這篇文章主要給大家介紹了關(guān)于Swift中風味各異的類型擦除的相關(guān)資料,需要的朋友可以參考下

前言

Swift的總體目標是既強大到可以用于底層系統(tǒng)編程,又足夠容易讓初學者學習,這有時會導致相當有趣的情況——當Swift的類型系統(tǒng)的力量要求我們部署相當高級的技術(shù)來解決乍一看可能更微不足道的問題。

大多數(shù)Swift開發(fā)人員會在某一時刻或另一時刻(通常是馬上,而不是日后)會遇到這樣一種情況,即需要某種形式的類型擦除才能引用通用協(xié)議。從本周開始,讓我們看一下是什么使類型擦除在Swift中成為必不可少的技術(shù),然后繼續(xù)探索實現(xiàn)它的不同 “風味(Flavors)”,以及每種風味為何各有優(yōu)缺點。

什么時候需要類型擦除?

一開始,“類型擦除”一詞似乎與 Swift 給我們的關(guān)注類型和編譯時類型安全性的第一感覺相反,因此,最好將其描述為隱藏類型,而不是完全擦除它們。目的是使我們能夠更輕松地與通用協(xié)議進行交互,因為這些通用協(xié)議對將要實現(xiàn)它們的各種類型具有特定的要求。

以標準庫中的Equatable協(xié)議為例。由于所有目的都是為了根據(jù)相等性比較兩個相同類型的值,因此Self元類型為其唯一要求的參數(shù):

protocol Equatable {
    static func ==(lhs: Self, rhs: Self) -> Bool
}

上面的代碼使任何類型都可以符合Equatable,同時仍然需要==運算符兩側(cè)的值都為同一類型,因為在實現(xiàn)上述方法時符合協(xié)議的每種類型都必須“填寫”自己的類型:

extension User: Equatable {
    static func ==(lhs: User, rhs: User) -> Bool {
        return lhs.id == rhs.id
    }
}

該方法的優(yōu)點在于,它不可能意外地比較兩個不相關(guān)的相等類型(例如 User 和 String ),但是,它也導致不可能將Equatable引用為獨立協(xié)議(例如創(chuàng)建 [Equatable] ),因為編譯器需要知道實際上確切符合協(xié)議的確切類型才能使用它。

當協(xié)議包含關(guān)聯(lián)的類型時,也是如此。例如,在這里我們定義了一個Request協(xié)議,使我們可以在一個統(tǒng)一的實現(xiàn)中隱藏各種形式的數(shù)據(jù)請求(例如網(wǎng)絡調(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)衡方法——它非常強大,因為它使我們能夠為任何類型的請求創(chuàng)建通用抽象,但也使得無法直接引用Request協(xié)議本身,例如這:

class RequestQueue {
    // 報錯: 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) {
        ...
    }
}

解決上述問題的一種方法是完全按照報錯消息的內(nèi)容進行操作,即不直接引用Request,而是將其用作一般約束:

class RequestQueue {
    func add<R: Request>(_ request: R,
                         handler: @escaping R.Handler) {
        ...
    }
}

上面的方法起作用了,因為現(xiàn)在編譯器能夠保證所傳遞的處理程序確實與作為請求傳遞的Request實現(xiàn)兼容——因為它們都基于泛型R,而后者又被限制為符合Request協(xié)議。

但是,盡管我們解決了方法的簽名問題,但仍然無法對傳遞的請求進行實際的處理,因為我們無法將其存儲為Request屬性或[Request]數(shù)組,這將使繼續(xù)構(gòu)建我們的RequestQueue變得困難。也就是說,除非我們開始進行類型擦除。

通用包裝器類型擦除

我們將探討的第一種類型擦除實際上并沒有涉及擦除任何類型,而是將它們包裝在一個我們可以更容易引用的通用類型中。繼續(xù)從之前的RequestQueue示例開始,我們首先創(chuàng)建該包裝器類型——該包裝器類型將捕獲每個請求的perform方法作為閉包,以及在請求完成后應調(diào)用的處理程序:

// 這將使我們將 Request 協(xié)議的實現(xiàn)包裝在一個
// 與 Request 協(xié)議具有相同的響應和錯誤類型的泛型中
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)的類型和泛型類型對齊,從而使我們可以將請求存儲為獨立的引用并作為數(shù)組的一部分——像這樣:

class RequestQueue<Response, Error: Swift.Error> {
? ? private typealias TypeErasedRequest = AnyRequest<Response, Error>
? ? private var queue = [TypeErasedRequest]()
? ? private var ongoing: TypeErasedRequest?
? ? // 我們修改了'add'方法,以包含一個'where'子句,
? ? // 該子句確保傳遞的請求已關(guān)聯(lián)的類型與隊列的通用類型匹配。
? ? func add<R: Request>(
? ? ? ? _ request: R,
? ? ? ? handler: @escaping R.Handler
? ? ) where R.Response == Response, R.Error == Error {
? ? ? ? //要執(zhí)行類型擦除,我們只需創(chuàng)建一個實例'AnyRequest',
? ? ? ? //然后將其傳遞給基礎請求將“perform”方法與處理程序一起作為閉包。
? ? ? ? let typeErased = AnyRequest(
? ? ? ? ? ? perform: request.perform,
? ? ? ? ? ? handler: handler
? ? ? ? )
? ? ? ? // 由于我們要實現(xiàn)隊列,因此我們不想一次有兩個請求,
? ? ? ? // 所以將請求保存下拉,以防稍后有一個正在執(zhí)行的請求。
? ? ? ? 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
? ? ? ? ? ? // 如果隊列不為空,則執(zhí)行下一個請求
? ? ? ? ? ? ...
? ? ? ? }
? ? }
}

請注意,上面的示例以及本文中的其他示例代碼都不是線程安全的——為了使事情變得簡單。有關(guān)線程安全的更多信息,請查看“避免在Swift 中競爭條件”。

上面的方法效果很好,但有一些缺點。我們不僅引入了新的AnyRequest類型,還需要將RequestQueue轉(zhuǎn)換為泛型。這給我們帶來了一點靈活性,因為我們現(xiàn)在只能將任何給定的隊列用于具有相同 響應/錯誤類型 組合的請求。具有諷刺意味的是,如果我們想組成多個實例,將來可能還需要我們自己實現(xiàn)隊列擦除。

閉包類型擦除

我們不引入包裝類型,而是讓我們看一下如何使用閉包來實現(xiàn)相同的類型擦除,同時還要使我們的RequestQueue非泛型且通用,足以用于不同類型的請求。

使用閉包擦除類型時,其思想是捕獲在閉包內(nèi)部執(zhí)行操作所需的所有類型信息,并使該閉包僅接受非泛型(甚至是Void)輸入。這樣一來,我們就可以引用,存儲和傳遞該功能,而無需實際知道功能內(nèi)部會發(fā)生什么,從而為我們提供了更強大的靈活性。

更新RequestQueue以使用基于閉包的類型擦除的方法如下:

class RequestQueue {
? ? private var queue = [() -> Void]()
? ? private var isPerformingRequest = false
? ? func add<R: Request>(_ request: R,
? ? ? ? ? ? ? ? ? ? ? ? ?handler: @escaping R.Handler) {
? ? ? ? // 此閉包將同時捕獲請求及其處理程序,而不會暴露任何類型信息
? ? ? ? // 在其外部,提供完全的類型擦除。
? ? ? ? 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)有時會使我們的代碼難以調(diào)試,但也可能使完全封裝類型信息成為可能——使得像RequestQueue這樣的對象可以在沒有真正了解在底層工作的類型的任何細節(jié)的情況下進行工作。

有關(guān)基于閉包的類型擦除及其更多不同方法的更多信息,請查看“Swift 使用閉包實現(xiàn)類型擦除”。

外部特化(External specialization)
到目前為止,我們已經(jīng)在RequestQueue本身中執(zhí)行了所有類型擦除,這有一些優(yōu)點——它可以讓任何外部代碼使用我們的隊列,而不需要知道我們使用什么類型的類型擦除。然而,有時在將協(xié)議實現(xiàn)傳遞給API之前進行一些輕量級轉(zhuǎn)換,既可以使事情變得更簡單,又可以巧妙地封裝類型擦除代碼本身。

對于我們的RequestQueue,一種方法是要求在將每個Request實現(xiàn)添加到隊列之前對其進行特化——這將把它轉(zhuǎn)換為RequestOperation,如下所示:

struct RequestOperation {
    fileprivate let closure: (@escaping () -> Void) -> Void
    func perform(then handler: @escaping () -> Void) {
        closure(handler)
    }
}

與我們之前使用閉包在RequestQueue中執(zhí)行類型擦除的方式類似,上面的RequestOperation類型將使我們能夠在擴展Request時執(zhí)行該操作:

extension Request {
    func makeOperation(with handler: @escaping Handler) -> RequestOperation {
        return RequestOperation { finisher in
            // 我們其實想在這里捕獲'self',因為不這樣話
            // 我們將冒著無法保留基本請求的風險。
            self.perform { result in
                handler(result)
                finisher()
            }
        }
    }
}

上述方法的優(yōu)點在于,無論是公共API還是內(nèi)部實現(xiàn),它都讓我們的RequestQueue更加簡單。它現(xiàn)在可以完全專注于作為一個隊列,而不必關(guān)心任何類型的類型擦除:

class RequestQueue {
    private var queue = [RequestOperation]()
    private var ongoing: RequestOperation?
    // 因為類型擦除現(xiàn)在發(fā)生在request被傳遞給 queue 之前,
    // 它可以簡單地接受一個具體的“RequestOperation”的實例。
    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
            // 如果隊列不為空,則執(zhí)行下一個請求
            ...
        }
    }
}

然而,這里的缺點是,在將每個請求添加到隊列之前,我們必須手動將其轉(zhuǎn)換為RequestOperation——雖然這不會在每個調(diào)用點添加大量代碼,但這取決于必須完成相同轉(zhuǎn)換的次數(shù),它最終可能會有點像樣板。

結(jié)語

盡管 Swift 提供了一個功能強大得難以置信的類型系統(tǒng),可以幫助我們避免大量的bug,但有時它會讓人覺得我們必須與系統(tǒng)抗爭,才能使用通用協(xié)議之類的功能。必須進行類型擦除最初看起來像是一件不必要的雜務,但它也帶來了一些好處——比如從不需要關(guān)心這些類型的代碼中隱藏特定類型信息。

在未來,我們可能還會看到 Swift 中添加了新的特性,可以自動化創(chuàng)建類型擦除包裝類型的過程,也可以通過使協(xié)議也被用作適當?shù)姆盒?例如能夠定義像Request這樣的協(xié)議)來消除對它的大量需求,而不僅僅依賴于相關(guān)的類型)。

什么樣的類型擦除是最合適的——無論是現(xiàn)在還是將來——當然很大程度上取決于上下文,以及我們的功能是否可以在閉包中輕松地執(zhí)行,或者完整包裝器類型或泛型是否更適合這個問題。

到此這篇關(guān)于Swift中風味各異的類型擦除的文章就介紹到這了,更多相關(guān)Swift類型擦除內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論