Swift:是否可以发现数组中元素的类型并将其用于指定泛型类型参数?

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

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

还有一个名为 ArrayResponse 的结构,用于表示网络响应 return 为 items 不同对象的数组(取决于特定的 APIRequest' s ResponseType, 以及 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 结构,代表品牌数据return来自服务器。

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

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

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

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

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

因此,例如,如果我创建一个 BrandsRequest,我想要这个顶级 decode 函数将数组解码为 return (try result.decoded() as ArrayResponse<Brand>).items Brand 是不同的结构(例如 ProductCustomer 等),具体取决于此函数接收的数组中元素的类型。这个例子有一些非编译代码,因为我试图获取 elementType 并将其用作通用参数,但这当然不起作用。我也不能简单地将 Codable 作为通用参数传递,因为编译器告诉我: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 网络响应 return 看起来像 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
    }
}

但是,编译器给我以下 2 个错误: '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
    }
}

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

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

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

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

我怀疑这是对协议的滥用。 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 扩展,根据请求附加正确的解码器(元素与数组)。当前的设计基于您的协议,强制调用者在解码时决定是否存在一个或多个元素,但在创建请求时就知道了。所以我可能会沿着这些思路构建更多请求,并明确地 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 继承的一个很好的例子。 APIRequest 扩展是关于创建默认实现,而不是应用通用算法,这通常暗示着 "inherit and override" OOP 思维方式。而不是一堆符合 APIRequest 的单独结构,我认为这会更好地作为单个 APIRequest 通用结构(如上所述)。

但是你仍然可以在不重写所有原始代码的情况下到达那里。例如,您可以制作一个通用的 "array" 映射:

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: [=16=]) }
        )
    }
}

但现在我们要添加用户:

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: [=17=]) }
        )
    }
}

这几乎是一样的。如此相似以至于我剪切并粘贴了它。这告诉我现在 是提取可重用代码的时候了(因为我正在摆脱真正的重复,而不仅仅是插入抽象层)。

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: [=18=]) }
        )
    }
}

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: [=19=]) }
        )
    }
}

阿格!再次复制!好吧,那就解决这个问题,然后把它们放在一起:

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: [=20=]) })
    }

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

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

如果你这样做了,你就会开始转向我的 Generic Swift talk 的设计,这只是另一种方法。那次谈话是关于设计通用代码的方法,而不是具体的实现选择。

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