寫更好的 Swift 程式碼: 關聯協議與型別擦除

語言: CN / TW / HK
  1. 關聯協議:帶有關聯型別的協議(PATs, Protocols with Associated Types)。

  2. 型別擦除:利用一個具體實現的通用泛型類(參看系統庫的AnySequence),去包裝具體實現了該泛型協議的類。用以解決不能直接使用泛型協議進行變數定義的問題。

上方丟擲了2個概念,對於關聯協議還是比較容易理解的,比如下面的Request的定義:

```swift enum HTTPMethod: String { case get = "GET" case post = "POST" case put = "PUT" case patch = "PATCH" case delete = "DELETE" }

protocol Request { associatedtype Output

var url: URL {get}
var method: HTTPMethod {get}
func decode(_ data: Data) throws -> Output

} ```

Request這個關聯協議也比較簡單,Output是一個關聯型別。decode目的是用來泛指完成網路請求後,將二進位制轉化為Output型別。如果我們想載入一張圖片,那麼可以定義 ImageRequest

```swift enum Error: Swift.Error { case invalidData }

struct ImageRequest: Request { let url: URL var method: HTTPMethod { .get }

func decode(_ data: Data) throws -> UIImage {
    guard let image = UIImage(data: data) else {
        throw Error.invalidData
    }
    return image
}

} ```

為了讓問題更易理解,我們將案例的網路請求層也展示出來:

```swift protocol Networking { func fetch(_ request: R) -> AnyPublisher }

class Networker: Networking {
func fetch(_ request: R) -> AnyPublisher { var urlRequest = URLRequest(url: request.url) urlRequest.httpMethod = request.method.rawValue

    var publisher = URLSession.shared
        .dataTaskPublisher(for: urlRequest)
        .compactMap { $0.data }
        .eraseToAnyPublisher()

    return publisher.tryMap(request.decode)
        .eraseToAnyPublisher()
}

} ```

Networker實現Networking協議,在 fetch 方法中,構造網路請求,待請求成功,將資料進行解析。

假設有一個文章列表,我們要載入這些文章的封面,大致的請求如下:

swift let request: Request = ImageRequest(url: article.image) let networker: Networking = Networker() networker.fetch(request) .sink(receiveCompletion: { completion in switch completion { case .failure(let error): print(error) default: break } }, receiveValue: { [weak self] image in self?.articles[articleIndex].downloadedImage = image }) .store(in: &cancellables)

假設我們想讓載入好的圖片快取起來,這樣在滾列表的時候我們可以複用這些圖片而不需要重新載入圖片。

swift class RequestCache<Value> { // 報錯: // 1.Protocol 'Request' can only be used as a generic constraint because it has Self or associated type requirements // 2.Type 'Request' does not conform to protocol 'Hashable' private var store: [Request: Value] = [:] }

RequestCache 的目的是將這個 Request 作為key,將下載好的資料作為 value。在載入圖片的時候優先從 store 中取出 request 對應的 value,如果 value 不存在則進行載入圖片。但是當我們定義好 RequestCache 的時候,編譯發生錯誤。

為了解決這個問題,我們需要使用 型別擦除

利用一個具體實現的通用泛型類(參看系統庫的AnySequence),去包裝具體實現了該泛型協議的類

那麼如何實現?

swift //可以將任何 Request 轉化為 AnyRequest, 從而擺脫關聯型別 //AnyRequest 是一個普通結構體 struct AnyRequest: Hashable { let url: URL let method: HTTPMethod }

我們將 RequestCache 的定義如下:

swift class RequestCache<Value> { private var store: [AnyRequest: Value] = [:] }

這樣就不會報錯了。那麼下一步如何將Request的具體類轉化為AnyRequest包裝類?我們將RequestCache進一步完善:

```swift class RequestCache { private var store: [AnyRequest: Value] = [:]

/// 從 store 取
func response<R: Request>(for request: R)-> Value? where R.Output == Value {
    // 包裝
    let erasedRequest = AnyRequest(url: request.url, method: request.method)
    return store[erasedRequest]
}

// 儲存到 store 中
func saveResponse<R: Request>(_ response: Value, for request: R) where R.Output == Value {
    // 包裝
    let erasedRequest = AnyRequest(url: request.url, method: request.method)
    store[erasedRequest] = response
}

} ```

那麼我們的 Networker的改造如下:

```swift

class Networker: Networking {

private let imageCache = RequestCache<UIImage>()


func fetch<R: Request>(_ request: R) -> AnyPublisher<R.Output, Swift.Error> {
    var urlRequest = URLRequest(url: request.url)
    urlRequest.httpMethod = request.method.rawValue
    var publisher = URLSession.shared
        .dataTaskPublisher(for: urlRequest)
        .compactMap { $0.data }
        .eraseToAnyPublisher()        
    return publisher.tryMap(request.decode)
        .eraseToAnyPublisher()
}

func fetchWithCache<R: Request>(_ request: R) -> AnyPublisher<R.Output, Swift.Error> where R.Output == UIImage {
    if let response = imageCache.response(for: request) {
        return Just<R.Output>(response)
            .setFailureType(to: Swift.Error.self)
            .eraseToAnyPublisher()
    }
    return fetch(request)
        .handleEvents(receiveOutput: {
            self.imageCache.saveResponse($0, for: request)
        }).eraseToAnyPublisher()
}

} ```

最終列表的圖片的請求程式碼:

swift let request = ImageRequest(url: article.image) let networker: Networking = Networker() networker.fetchWithCache(request) .sink(receiveCompletion: { error in print(error) }, receiveValue: { image in self.articles[articleIndex].downloadedImage = image }) .store(in: &cancellables)

總結:

當我們遇到不能直接使用泛型協議進行變數定義的時候,我們可以利用一個具體實現的通用泛型類,去包裝具體實現了該泛型協議的類