[英]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
数组形式返回(取决于特定的APIRequest
的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
结构,表示从服务器返回的品牌数据。
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>).items
与Brand
是不同的结构(例如Product
, Customer
等)取决于此 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
。
所以我的问题是:
ArrayResponse<insert type here>
中使用的数组中元素的类型?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
}
}
但是我必须在每个结构中提供符合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(具有关联类型的协议)都是关于向现有类型添加更多功能,目前尚不清楚是否这样做。 相反,我相信你有一个 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.