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
作为 Brand
s.
的数组
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
是不同的结构(例如 Product
、Customer
等),具体取决于此函数接收的数组中元素的类型。这个例子有一些非编译代码,因为我试图获取 elementType
并将其用作通用参数,但这当然不起作用。我也不能简单地将 Codable
作为通用参数传递,因为编译器告诉我: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
网络响应 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),而不是协议。
我有一个名为 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
作为 Brand
s.
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
是不同的结构(例如 Product
、Customer
等),具体取决于此函数接收的数组中元素的类型。这个例子有一些非编译代码,因为我试图获取 elementType
并将其用作通用参数,但这当然不起作用。我也不能简单地将 Codable
作为通用参数传递,因为编译器告诉我: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
网络响应 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),而不是协议。