[英]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.