繁体   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