简体   繁体   English

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

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

I have a protocol named APIRequest with an associated type named ResponseType and a decode function.我有一个名为APIRequest的协议,它有一个名为ResponseType的关联类型和一个解码 function。 This example is not complete, but I believe these are the only relevant parts for the question.这个例子并不完整,但我相信这些是问题的唯一相关部分。

There's also a struct named ArrayResponse to represent when a network response returns as an array of items of different objects (depending on the specific APIRequest 's ResponseType , as well as totalItems .还有一个名为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
}

Here's an example of a struct that adheres to the APIRequest protocol and specifies it's ResponseType as Brand , which is a Codable struct that represents brand data being returned from the server.这是一个遵循APIRequest协议并将其ResponseType指定为Brand的结构示例,这是一个Codable结构,表示从服务器返回的品牌数据。

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

The BrandRequest is used to fetch a single Brand from the server, but I can also fetch an array of Brand 's (represented by the ArrayResponse above, since Brand is one of many different types that all follow the same pattern), using BrandsRequest , which specifies it's ResponseType as an array of Brand s. BrandRequest用于从服务器获取单个Brand ,但我也可以使用BrandsRequest获取Brand的数组(由上面的ArrayResponse表示,因为 Brand 是许多不同类型中的一种,它们都遵循相同的模式),它将其ResponseType指定为Brand的数组。

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

Rather than providing a decode function in every struct that adheres to APIRequest , I've decided to make a default implementation in a protocol extension, since they all follow the same decoding.我决定在协议扩展中进行默认实现,而不是在每个结构中提供decode APIRequest ,因为它们都遵循相同的解码。

Depending on whether the ResponseType is an array (such as [Brand] , or a single item, such as Brand , I use a different version of the decode function. This works well for the single item, but for the array of items, I'd like to look into the Array, discover the type of it's Elements, and use that to check if the result.decoded() is decoded as an ArrayResponse<> of that particular type.根据ResponseType是一个数组(例如[Brand]还是单个项目,例如Brand ,我使用不同版本的decode function。这对单个项目很有效,但对于项目数组,我'想查看数组,发现它的元素类型,并使用它来检查result.decoded()是否被解码为该特定类型的ArrayResponse<>

So, for example, if I make a BrandsRequest , I'd like this top decode function which decodes the Array to return (try result.decoded() as ArrayResponse<Brand>).items with Brand being a different struct (eg Product , Customer , etc.) depending on the type of the Element in the array this function receives.因此,例如,如果我进行BrandsRequest ,我希望这个顶级decode function 解码数组以返回(try result.decoded() as ArrayResponse<Brand>).itemsBrand是不同的结构(例如ProductCustomer等)取决于此 function 接收的数组中元素的类型。 This example has some non-compiling code as my attempt to get the elementType and use it as a generic argument, but of course that does not work.这个示例有一些非编译代码作为我尝试获取elementType并将其用作通用参数的尝试,但这当然不起作用。 I also cannot simply pass Codable as the generic argument, since the compiler tells me: Value of protocol type 'Codable' (aka 'Decodable & Encodable') cannot conform to 'Decodable'; only struct/enum/class types can conform to protocols我也不能简单地将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 . Value of protocol type 'Codable' (aka 'Decodable & Encodable') cannot conform to 'Decodable'; only struct/enum/class types can conform to protocols

So my questions are:所以我的问题是:

  1. Is there a way to capture the Type of the Element in the Array to use in ArrayResponse<insert type here> ?有没有办法捕获要在ArrayResponse<insert type here>中使用的数组中元素的类型?
  2. Is there a better way to decode the network responses that return arrays of items that looks like ArrayResponse vs. single item response like Brand ?有没有更好的方法来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
    }
}

Addendum: One other approach I thought of is to change ArrayResponse<> to use T as the array type, rather than the element type:附录:我想到的另一种方法是更改ArrayResponse<>以使用 T 作为数组类型,而不是元素类型:

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

and then to simplify the array decode like so:然后像这样简化数组解码:

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

However, the compiler gives me these 2 errors: 'ArrayResponse' requires that 'Decodable & Encodable' conform to 'Encodable' and Value of protocol type 'Decodable & Encodable' cannot conform to 'Encodable'; only struct/enum/class types can conform to protocols但是,编译器给了我这两个错误: '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


Addendum 2: I can get everything working and compiling, if I add another associatedtype to APIRequest to define the type of Element within the array:附录 2:如果我向APIRequest添加另一个关联类型来定义数组中元素的类型,我可以让一切工作和编译:

protocol APIRequest {
    associatedtype ResponseType: Codable
    associatedtype ElementType: Codable

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

and then change my array decode function to use ElementType instead of Codable :然后更改我的数组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
    }
}

but then I have to supply the ElementType in each struct that conforms to APIRequest , including the single requests where it's redundant to ResponseType and not used.但是我必须在每个结构中提供符合APIRequestElementType ,包括对ResponseType冗余且未使用的单个请求。 For the array requests, it's simply the value inside the array ResponseType , which also feels repetitive:对于数组请求,它只是数组ResponseType中的值,感觉也是重复的:

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

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

The crux of my problem is that I'd like to discover the Brand type within the [Brand] array, and use it for the ArrayResponse decoding.我的问题的症结在于我想在[Brand]数组中发现Brand类型,并将其用于ArrayResponse解码。

I suspect this is a misuse of protocols.我怀疑这是对协议的滥用。 PATs (protocols with associated types) are all about adding more features to existing types, and it's not clear this does that. PAT(具有关联类型的协议)都是关于向现有类型添加更多功能,目前尚不清楚是否这样做。 Instead, I believe you have a generics problem.相反,我相信你有一个 generics 问题。

As before, you have an ArrayResponse , because that's a special thing in your API:和以前一样,您有一个ArrayResponse ,因为这是您的 API 中的特殊内容:

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

Now, instead of a protocol, you need a generic struct:现在,您需要一个通用结构而不是协议:

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

And finally, you need a way to create "BrandRequest" (but really Request<Brand> ):最后,您需要一种方法来创建“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)")!)
    }
}

That said, I'd probably adjust this and create different Request extensions that attach the correct decoder (element vs array) depending on the request.也就是说,我可能会对此进行调整并创建不同的Request扩展,根据请求附加正确的解码器(元素与数组)。 The current design, based on your protocol, forces the caller at decode time to decide if there are one or more elements, but that's known when the request is created.当前的设计基于您的协议,强制调用者在解码时决定是否存在一个或多个元素,但这在创建请求时是已知的。 So I'd probably build Request more along these lines, and make Response explicitly ArrayResponse :所以我可能会更多地沿着这些思路构建 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
}

(and then assign the appropriate decoder in the init ) (然后在init中分配适当的解码器)


Looking at the code you linked, yeah, that's a pretty good example of using protocols to try to recreate class inheritance.查看您链接的代码,是的,这是使用协议尝试重新创建 class inheritance 的一个很好的示例。 The APIRequest extension is all about creating default implementations, rather than applying generic algorithms, and that usually suggests an "inherit and override" OOP mindset. APIRequest 扩展是关于创建默认实现,而不是应用通用算法,这通常暗示着“继承和覆盖”OOP 的心态。 Rather than a bunch of individual structs that conform to APIRequest, I would think this would work better as a single APIRequest generic struct (as described above).而不是一堆符合 APIRequest 的单独结构,我认为这将作为单个 APIRequest 通用结构更好地工作(如上所述)。

But you can still get there without rewriting all the original code.但是你仍然可以在不重写所有原始代码的情况下到达那里。 For example, you can make a generic "array" mapping:例如,您可以制作一个通用的“数组”映射:

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

typealias BrandsRequest = ArrayRequest<Brand>

And of course you could push that up a layer:当然,您可以将其推高一层:

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

typealias BrandRequest = ElementRequest<Brand>

And all your existing APIRequest stuff still works, but your syntax can be a lot simpler (and there's no actual requirement to create the typealiases; ElementRequest<Brand> is probably fine on its own).您现有的所有 APIRequest 内容仍然有效,但您的语法可以简单得多(并且没有实际要求创建类型别名; ElementRequest<Brand>本身可能就可以了)。


Extending some of this based on your comment, you want to add an apiPath , and I take it you're trying to figure out where to put that information.根据您的评论扩展其中的一些内容,您想添加一个apiPath ,我认为您正试图找出将这些信息放在哪里。 That fits perfectly in my Request type.这完全符合我的请求类型。 Each init is responsible for creating an URLRequest.每个init负责创建一个 URLRequest。 Any way it wants to do that is fine.它想这样做的任何方式都很好。

Simplifying things to the basics:将事情简化为基础:

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

But now we want to add User:但是现在我们要添加用户:

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

That's almost identical.这几乎是一样的。 So identical that I cut-and-pasted it.如此相同,以至于我剪切并粘贴了它。 And that tells me that it is now time to pull out reusable code (because I'm getting rid of a real duplication, not just inserting abstraction layers).这告诉我现在是提取可重用代码的时候了(因为我要摆脱真正的重复,而不仅仅是插入抽象层)。

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

But what about ArrayResponse?但是 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, Duplication again, Well: then fix that problem, and putting it all together: 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)
    }
}

Now this is just one of many ways to approach it.现在,这只是处理它的众多方法之一。 Instead of Request extension for each type, it might be nicer to have a Fetchable protocol, and put the domain there:而不是为每种类型请求扩展,拥有一个 Fetchable 协议可能会更好,并将域放在那里:

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

Then you can hang the information on the model types like:然后您可以挂起 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)
    }
}

Notice that these aren't mutually exclusive.请注意,这些并不是相互排斥的。 You can have both approaches at the same time because doing it this way composes.您可以同时使用这两种方法,因为这样做可以组合。 Different abstraction choices don't have to interfere with each other.不同的抽象选择不必相互干扰。

If you did that, you'd start to move towards the design from my Generic Swift talk , which is just another way to do it.如果你这样做了,你就会开始从我的Generic Swift 谈话中转向设计,这只是另一种方法。 That talk is about an approach to designing generic code, not a specific implementation choice.该演讲是关于设计通用代码的方法,而不是特定的实现选择。

And all without needing associated types.并且所有这些都不需要关联类型。 The way you know an associated type probably makes sense is that different conforming types implement the protocol requirements differently.您知道关联类型的方式可能是有意义的,即不同的符合类型以不同的方式实现协议要求。 For example, Array's implementation of the subscript requirement is very different than Repeated's implementation and LazySequence's implementation.例如,Array 对下标要求的实现与 Repeated 的实现和 LazySequence 的实现有很大的不同。 If every implementation of the protocol requirements would be structurally identical, then you're probably looking at a generic struct (or possibly a class), not a protocol.如果协议要求的每个实现在结构上都是相同的,那么您可能正在查看通用结构(或可能是类),而不是协议。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM