簡體   English   中英

Swift:可以發現數組中元素的類型並用於指定泛型類型參數嗎?

[英]Swift: Can the type of an Element in an Array be discovered and used to specify the generic type argument?

我有一個名為APIRequest的協議,它有一個名為ResponseType的關聯類型和一個解碼 function。 這個例子並不完整,但我相信這些是問題的唯一相關部分。

還有一個名為ArrayResponse的結構來表示網絡響應何時以不同對象的items數組形式返回(取決於特定的APIRequestResponseType以及totalItems

protocol APIRequest {
    associatedtype ResponseType: Codable

    /// Decodes the request result into the ResponseType
    func decode(_: Result<Data, APIError>) throws -> ResponseType
}

struct ArrayResponse<T>: Codable where T: Codable {
    let items: [T]
    let totalItems: Int
}

這是一個遵循APIRequest協議並將其ResponseType指定為Brand的結構示例,這是一個Codable結構,表示從服務器返回的品牌數據。

struct BrandRequest: APIRequest {
    typealias ResponseType = Brand
}
struct Brand: Codable {
    var brandID: Int
    var brandName: String?
}

BrandRequest用於從服務器獲取單個Brand ,但我也可以使用BrandsRequest獲取Brand的數組(由上面的ArrayResponse表示,因為 Brand 是許多不同類型中的一種,它們都遵循相同的模式),它將其ResponseType指定為Brand的數組。

struct BrandsRequest: APIRequest {
    typealias ResponseType = [Brand]
}

我決定在協議擴展中進行默認實現,而不是在每個結構中提供decode APIRequest ,因為它們都遵循相同的解碼。

根據ResponseType是一個數組(例如[Brand]還是單個項目,例如Brand ,我使用不同版本的decode function。這對單個項目很有效,但對於項目數組,我'想查看數組,發現它的元素類型,並使用它來檢查result.decoded()是否被解碼為該特定類型的ArrayResponse<>

因此,例如,如果我進行BrandsRequest ,我希望這個頂級decode function 解碼數組以返回(try result.decoded() as ArrayResponse<Brand>).itemsBrand是不同的結構(例如ProductCustomer等)取決於此 function 接收的數組中元素的類型。 這個示例有一些非編譯代碼作為我嘗試獲取elementType並將其用作通用參數的嘗試,但這當然不起作用。 我也不能簡單地將Codable作為通用參數傳遞,因為編譯器告訴我: Value of protocol type 'Codable' (aka 'Decodable & Encodable') cannot conform to 'Decodable'; only struct/enum/class types can conform to protocols Value of protocol type 'Codable' (aka 'Decodable & Encodable') cannot conform to 'Decodable'; only struct/enum/class types can conform to protocols

所以我的問題是:

  1. 有沒有辦法捕獲要在ArrayResponse<insert type here>中使用的數組中元素的類型?
  2. 有沒有更好的方法來decode返回 arrays 的項目的網絡響應,這些項目看起來像ArrayResponse與單個項目響應像Brand
extension APIRequest where ResponseType == Array<Codable> {
    func decode(_ result: Result<Data, APIError>) throws -> ResponseType {
        let elementType = type(of: ResponseType.Element.self)
        print(elementType)

        return (try result.decoded() as ArrayResponse<elementType>).items
    }
}

extension APIRequest {
    func decode(_ result: Result<Data, APIError>) throws -> ResponseType {
        return try result.decoded() as ResponseType
    }
}

附錄:我想到的另一種方法是更改ArrayResponse<>以使用 T 作為數組類型,而不是元素類型:

struct ArrayResponse<T>: Codable where T: Codable {
    let items: T
    let totalItems: Int
}

然后像這樣簡化數組解碼:

extension APIRequest where ResponseType == Array<Codable> {
    func decode(_ result: Result<Data, APIError>) throws -> ResponseType {
        return (try result.decoded() as ArrayResponse<ResponseType>).items
    }
}

但是,編譯器給了我這兩個錯誤: 'ArrayResponse' requires that 'Decodable & Encodable' conform to 'Encodable' Value of protocol type 'Decodable & Encodable' cannot conform to 'Encodable'; only struct/enum/class types can conform to protocols Value of protocol type 'Decodable & Encodable' cannot conform to 'Encodable'; only struct/enum/class types can conform to protocols


附錄 2:如果我向APIRequest添加另一個關聯類型來定義數組中元素的類型,我可以讓一切工作和編譯:

protocol APIRequest {
    associatedtype ResponseType: Codable
    associatedtype ElementType: Codable

    /// Decodes the request result into the ResponseType
    func decode(_: Result<Data, APIError>) throws -> ResponseType
}

然后更改我的數組decode function 以使用ElementType而不是Codable

extension APIRequest where ResponseType == Array<ElementType> {
    func decode(_ result: Result<Data, APIError>) throws -> ResponseType {
        return (try result.decoded() as ArrayResponse<ResponseType>).items
    }
}

但是我必須在每個結構中提供符合APIRequestElementType ,包括對ResponseType冗余且未使用的單個請求。 對於數組請求,它只是數組ResponseType中的值,感覺也是重復的:

struct BrandRequest: APIRequest {
    typealias ResponseType = Brand
    typealias ElementType = Brand
}

struct BrandsRequest: APIRequest {
    typealias ResponseType = [Brand]
    typealias ElementType = Brand
}

我的問題的症結在於我想在[Brand]數組中發現Brand類型,並將其用於ArrayResponse解碼。

我懷疑這是對協議的濫用。 PAT(具有關聯類型的協議)都是關於向現有類型添加更多功能,目前尚不清楚是否這樣做。 相反,我相信你有一個 generics 問題。

和以前一樣,您有一個ArrayResponse ,因為這是您的 API 中的特殊內容:

struct ArrayResponse<Element: Codable>: Codable {
    let items: [Element]
    let totalItems: Int
}

現在,您需要一個通用結構而不是協議:

struct Request<Response: Codable> {
    // You need some way to fetch this, so I'm going to assume there's an URLRequest
    // hiding in here.
    let urlRequest: URLRequest

    // Decode single values
    func decode(_ result: Result<Data, APIError>) throws -> Response {
        return try JSONDecoder().decode(Response.self, from: result.get())
    }

    // Decode Arrays. This would be nice to put in a constrained extension instead of here,
    // but that's not currently possible in Swift
    func decode(_ result: Result<Data, APIError>) throws -> ArrayResponse<Response> {
        return try JSONDecoder().decode(ArrayResponse<Response>.self, from: result.get())
    }
}

最后,您需要一種方法來創建“BrandRequest”(但實際上是Request<Brand> ):

struct Brand: Codable {
    var brandID: Int
    var brandName: String?
}

// You want "BrandRequest", but that's just a particular URLRequest for Request<Brand>.
// I'm going to make something up for the API:
extension Request where Response == Brand {
    init(brandName: String) {
        self.urlRequest = URLRequest(url: URL(string: "https://example.com/api/v1/brands/(\brandName)")!)
    }
}

也就是說,我可能會對此進行調整並創建不同的Request擴展,根據請求附加正確的解碼器(元素與數組)。 當前的設計基於您的協議,強制調用者在解碼時決定是否存在一個或多個元素,但這在創建請求時是已知的。 所以我可能會更多地沿着這些思路構建 Request ,並明確地制作Response ArrayResponse

struct Request<Response: Codable> {
    // You need some way to fetch this, so I'm going to assume there's an URLRequest
    // hiding in here.
    let urlRequest: URLRequest
    let decoder: (Result<Data, APIError>) throws -> Response
}

(然后在init中分配適當的解碼器)


查看您鏈接的代碼,是的,這是使用協議嘗試重新創建 class inheritance 的一個很好的示例。 APIRequest 擴展是關於創建默認實現,而不是應用通用算法,這通常暗示着“繼承和覆蓋”OOP 的心態。 而不是一堆符合 APIRequest 的單獨結構,我認為這將作為單個 APIRequest 通用結構更好地工作(如上所述)。

但是你仍然可以在不重寫所有原始代碼的情況下到達那里。 例如,您可以制作一個通用的“數組”映射:

struct ArrayRequest<Element: Codable>: APIRequest {
    typealias ResponseType = [Element]
    typealias ElementType = Element
}

typealias BrandsRequest = ArrayRequest<Brand>

當然,您可以將其推高一層:

struct ElementRequest<Element: Codable>: APIRequest {
    typealias ResponseType = Element
    typealias ElementType = Element
}

typealias BrandRequest = ElementRequest<Brand>

您現有的所有 APIRequest 內容仍然有效,但您的語法可以簡單得多(並且沒有實際要求創建類型別名; ElementRequest<Brand>本身可能就可以了)。


根據您的評論擴展其中的一些內容,您想添加一個apiPath ,我認為您正試圖找出將這些信息放在哪里。 這完全符合我的請求類型。 每個init負責創建一個 URLRequest。 它想這樣做的任何方式都很好。

將事情簡化為基礎:

struct Brand: Codable {
    var brandID: Int
    var brandName: String?
}

struct Request<Response: Codable> {
    let urlRequest: URLRequest
    let parser: (Data) throws -> Response
}

extension Request where Response == Brand {
    init(brandName: String) {
        self.init(
            urlRequest: URLRequest(url: URL(string: "https://example.com/api/v1/brands/\(brandName)")!),
            parser: { try JSONDecoder().decode(Brand.self, from: $0) }
        )
    }
}

但是現在我們要添加用戶:

struct User: Codable {}

extension Request where Response == User {
    init(userName: String) {
        self.init(urlRequest: URLRequest(url: URL(string: "https://example.com/api/v1/users/\(userName)")!),
                  parser: { try JSONDecoder().decode(User.self, from: $0) }
        )
    }
}

這幾乎是一樣的。 如此相同,以至於我剪切並粘貼了它。 這告訴我現在是提取可重用代碼的時候了(因為我要擺脫真正的重復,而不僅僅是插入抽象層)。

extension Request {
    init(domain: String, id: String) {
        self.init(urlRequest: URLRequest(url: URL(string: "https://example.com/api/v1/\(domain)/\(id)")!),
                  parser: { try JSONDecoder().decode(Response.self, from: $0) }
        )
    }
}

extension Request where Response == Brand {
    init(brandName: String) {
        self.init(domain: "brands", id: brandName)
    }
}

extension Request where Response == User {
    init(userName: String) {
        self.init(domain: "users", id: userName)
    }
}

但是 ArrayResponse 呢?

extension Request {
    init<Element: Codable>(domain: String) where Response == ArrayResponse<Element> {
        self.init(urlRequest: URLRequest(url: URL(string: "https://example.com/api/v1/\(domain)")!),
                  parser: { try JSONDecoder().decode(Response.self, from: $0) }
        )
    }
}

Arg,再次重復,好吧:然后解決這個問題,並將它們放在一起:

extension Request {
    static var baseURL: URL { URL(string: "https://example.com/api/v1")! }

    init(path: String) {
        self.init(urlRequest: URLRequest(url: Request.baseURL.appendingPathComponent(path)),
                  parser: { try JSONDecoder().decode(Response.self, from: $0) })
    }

    init(domain: String, id: String) {
        self.init(path: "\(domain)/\(id)")
    }

    init<Element: Codable>(domain: String) where Response == ArrayResponse<Element> {
        self.init(path: domain)
    }
}

extension Request where Response == Brand {
    init(brandName: String) {
        self.init(domain: "brands", id: brandName)
    }
}

extension Request where Response == User {
    init(userName: String) {
        self.init(domain: "users", id: userName)
    }
}

現在,這只是處理它的眾多方法之一。 而不是為每種類型請求擴展,擁有一個 Fetchable 協議可能會更好,並將域放在那里:

protocol Fetchable: Codable {
    static var domain: String { get }
}

然后您可以掛起 model 類型的信息,例如:

extension User: Fetchable {
    static let domain = "users"
}

extension ArrayResponse: Fetchable where T: Fetchable {
    static var domain: String { T.domain }
}

extension Request where Response: Fetchable {
    init(id: String) {
        self.init(domain: Response.domain, id: id)
    }

    init<Element: Fetchable>() where Response == ArrayResponse<Element> {
        self.init(domain: Response.domain)
    }
}

請注意,這些並不是相互排斥的。 您可以同時使用這兩種方法,因為這樣做可以組合。 不同的抽象選擇不必相互干擾。

如果你這樣做了,你就會開始從我的Generic Swift 談話中轉向設計,這只是另一種方法。 該演講是關於設計通用代碼的方法,而不是特定的實現選擇。

並且所有這些都不需要關聯類型。 您知道關聯類型的方式可能是有意義的,即不同的符合類型以不同的方式實現協議要求。 例如,Array 對下標要求的實現與 Repeated 的實現和 LazySequence 的實現有很大的不同。 如果協議要求的每個實現在結構上都是相同的,那么您可能正在查看通用結構(或可能是類),而不是協議。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM