首页 > 解决方案 > Swift:可以发现数组中元素的类型并用于指定泛型类型参数吗?

问题描述

我有一个以APIRequest关联类型命名的协议ResponseType和一个解码函数。这个例子并不完整,但我相信这些是问题的唯一相关部分。

还有一个名为的结构ArrayResponse,用于表示网络响应何时以不同对象数组的形式返回items(取决于特定APIRequest的 'sResponseTypetotalItems.

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为 as的结构示例,BrandCodable结构表示从服务器返回的品牌数据。

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

用于从服务器BrandRequest获取单个Brand,但我也可以获取一个数组Brand(由ArrayResponse上面表示,因为 Brand 是所有遵循相同模式的许多不同类型之一),使用BrandsRequest指定它ResponseType作为s 的数组Brand

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

我决定在协议扩展中创建一个默认实现,而不是decode在每个结构中提供一个函数,因为它们都遵循相同的解码。APIRequest

根据ResponseType是数组(例如[Brand],还是单个项目,例如Brand,我使用不同版本的decode函数。这对于单个项目很好,但对于项目数组,我想研究一下数组,发现它的元素的类型,并使用它来检查是否result.decoded()被解码ArrayResponse<>为该特定类型。

因此,例如,如果我创建一个BrandsRequest,我希望这个顶级函数解码 Array以decode返回一个不同的结构(例如,等),具体取决于该函数接收的数组中 Element 的类型。这个示例有一些非编译代码作为我尝试获取并将其用作通用参数的尝试,但这当然不起作用。我也不能简单地作为通用参数传递,因为编译器告诉我:.(try result.decoded() as ArrayResponse<Brand>).itemsBrandProductCustomerelementTypeCodableValue 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返回看起来像的项目数组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


附录 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函数更改为使用ElementType而不是Codable

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

但随后我必须ElementType在每个结构中提供符合 的,包括冗余且未使用APIRequest的单个请求。ResponseType对于数组请求,它只是数组内部的值ResponseType,感觉也是重复的:

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

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

我的问题的症结在于我想发现数组中的Brand类型[Brand],并将其用于ArrayResponse解码。

标签: arraysswiftgenericsprotocols

解决方案


我怀疑这是对协议的滥用。PAT(具有关联类型的协议)都是关于向现有类型添加更多功能,目前尚不清楚是否这样做。相反,我相信你有一个泛型问题。

和以前一样,你有一个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


查看您链接的代码,是的,这是使用协议尝试重新创建类继承的一个很好的示例。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) }
        )
    }
}

精氨酸!又重复了!好吧,然后解决这个问题,并将它们放在一起:

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 }
}

然后您可以将信息挂在模型类型上,例如:

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)
    }
}

请注意,这些并不是相互排斥的。您可以同时使用这两种方法,因为这样做可以组合。不同的抽象选择不必相互干扰。

如果你这样做了,你就会开始从我的通用 Swift 演讲中开始设计,这只是另一种方式。该演讲是关于设计通用代码的方法,而不是特定的实现选择。

并且所有这些都不需要关联类型。您知道关联类型的方式可能是有意义的,即不同的符合类型以不同的方式实现协议要求。例如,Array 对下标要求的实现与 Repeated 的实现和 LazySequence 的实现有很大的不同。如果协议要求的每个实现在结构上都是相同的,那么您可能正在查看通用结构(或可能是类),而不是协议。


推荐阅读