Swift Codable with Generics,保存响应中的公共数据
Swift Codable with Generics, holding common data from responses
我会尽量以最好的方式解释我想做的事情,就像我在过去几天谷歌搜索时所做的那样。
我的应用程序正在与几个不同的 API 进行通信,但让我们首先考虑来自一个 API 的响应。来自每个端点的响应包含一些'common parameters',例如状态或错误消息,以及我们最感兴趣的单个对象或对象数组,它带来了重要数据,我们可能想对其进行编码、存储,放到Realm、CoreData等
例如,单个对象的响应:
{
"status": "success",
"response_code": 200,
"messages": [
"message1",
"message2"
]
"data": {
OBJECT we're interested in.
}
}
或者,用对象数组响应:
{
"status": "success",
"response_code": 200,
"messages": [
"message1",
"message2"
]
"data": [
{
OBJECT we're interested in.
},
{
OBJECT we're interested in.
}
]
}
好的。就是这么简单,容易理解。
现在,我想写一个 "Root" 对象,它将容纳 'common parameters' 或 status
、response_code
和 messages
并有另一个, 属性 对于我们感兴趣的特定对象(或对象数组)。
继承
第一种方法是创建一个根对象,如下所示:
class Root: Codable {
let status: String
let response_code: Int
let messages: [String]?
private enum CodingKeys: String, CodingKey {
case status, response_code, messages
}
required public init(from decoder: Decoder) throws {
let container = try? decoder.container(keyedBy: CodingKeys.self)
status = try container?.decodeIfPresent(String.self, forKey: .code) ?? ""
response_code = try container?.decodeIfPresent(Int.self, forKey: .error) ?? 0
messages = try container?.decodeIfPresent([String].self, forKey: .key)
}
public func encode(to encoder: Encoder) throws {}
}
一旦我有了这个 Root 对象,我就可以创建从这个 Root 对象继承的特定对象,并将我的特定对象传递给 JSONDecoder,这样,我就有了一个很好的解决方案。但是,这个解决方案对于数组 是失败的。
也许对于某些人来说它没有,但我怎么强调我都不想制作额外的 'plural' 对象,该对象只存在于保存对象数组 ,喜欢:
class Objects: Root {
let objects: [Object]
// Code that decodes array of "Object" from "data" key
}
struct Object: Codable {
let property1
let property2
let property3
// Code that decodes all properties of Object
}
它看起来不干净,它需要单独的对象来简单地保存一个数组,它在某些情况下由于继承而在存储到 Realm 时产生问题,它 最重要的是 产生可读性较差的代码。
泛型
我的第二个想法是尝试使用泛型,所以我做了 a little something like this:
struct Root<T: Codable>: Codable {
let status: String
let response_code: Int
let messages: [String]?
let data: T?
private enum CodingKeys: String, CodingKey {
case status, response_code, messages, data
}
required public init(from decoder: Decoder) throws {
let container = try? decoder.container(keyedBy: CodingKeys.self)
status = try container?.decodeIfPresent(String.self, forKey: .code) ?? ""
response_code = try container?.decodeIfPresent(Int.self, forKey: .error) ?? 0
messages = try container?.decodeIfPresent([String].self, forKey: .key)
data = try container.decodeIfPresent(T.self, forKey: .data)
}
public func encode(to encoder: Encoder) throws {}
}
有了这个,我可以像这样将单个对象和对象数组传递给 JSON 解码器:
let decodedValue = try JSONDecoder().decode(Root<Object>.self, from: data)
// or
let decodedValue = try JSONDecoder().decode(Root<[Object]>.self, from: data)
这很不错。我可以在 Root 结构的 .data 属性 中获取我需要的结构,并按我喜欢的方式使用它,作为单个对象或对象数组。我可以轻松地存储它,随心所欲地操作它,继承带来了上例。
这个想法对我来说失败的地方是当我想在某个不确定 T 设置的地方访问 'common properties' 时。
这是对我的应用程序中实际发生的事情的简化解释,我将对其进行一些扩展以解释此通用解决方案对我不起作用的地方,最后问我的问题。
问题与疑问
如顶部所述,应用程序使用 3 个 API,所有 3 个 API 都有不同的 Root
结构,当然还有很多不同的 "sub-structs" - 到给他们起名字。我在应用程序中有一个单独的地方,单个 APIResponse
对象可以返回到应用程序的 UI 部分,我在其中从 decoded value
中提取了 1 个可读错误,decoded value
就是那个'sub-struct',是我的 "specific Objects"、Car
、Dog
、House
、Phone
、
中的任何一个
使用 Inheritance 解决方案,我可以做这样的事情:
struct APIResponse <T> {
var value: T? {
didSet {
extractErrorDescription()
}
}
var errorDescription: String? = "Oops."
func extractErrorDescription() {
if let responseValue = value as? Root1, let error = responseValue.errors.first {
self.errorDescription = error
}
else if let responseValue = value as? Root2 {
self.errorDescription = responseValue.error
}
else if let responseValue = value as? Root3 {
self.errorDescription = responseValue.message
}
}
}
但是使用 Generics 解决方案,我无法做到这一点。如果我尝试使用 Root1
或 Root2
或 Root3
编写相同的代码,如 Generics 示例所示:
func extractErrorDescription() {
if let responseValue = value as? Root1, let error = responseValue.errors.first {
self.errorDescription = error
}
}
我会收到错误提示 Generic parameter 'T' could not be inferred in cast to 'Root1'
,在这里,我试图提取错误,但我不知道哪个子结构被传递给了 Root1。是 Root1<Dog>
还是 Root1<Phone>
还是 Root1<Car>
- 我不知道怎么弄清楚,我显然需要知道才能确定值是 Root1
还是Root2
或 Root3
。
我正在寻找的解决方案是一种可以让我区分 Root
对象与上面显示的泛型解决方案的解决方案,或者是一种允许我在一些完全不同的架构中解码的解决方案方式,记住我写的所有内容,尤其是避免 'plural' objects
的能力
*如果JSON没有通过JSON验证器,请无视,手写就是为了这个问题
**如果编写的代码没有运行,请忽略,这更多的是一个架构问题,而不是如何编译一些代码。
您在这里寻找的是协议。
protocol ErrorProviding {
var error: String? { get }
}
我有意将 errorDescription
更改为 error
,因为这似乎是您的根类型中的内容(但您绝对可以在此处重命名)。
然后 APIResponse 要求:
struct APIResponse<T: ErrorProviding> {
var value: T?
var error: String? { value?.error }
}
然后每个具有特殊处理的根类型实现协议:
extension Root1: ErrorProviding {
var error: String? { errors.first }
}
但是已经具有正确形状的简单根类型可以声明符合性,不需要额外的实现。
extension Root2: ErrorProviding {}
假设您想要的不仅仅是 error
,您可以将其设为 APIPayload
而不是 ErrorProviding
并添加任何其他常见要求。
附带说明一下,如果您只使用 Decodable 而不是使用带有空 encode
方法的 Codable,您的代码会更简单。如果一个类型不能真正被编码,它就不应该符合 Encodable。
我会尽量以最好的方式解释我想做的事情,就像我在过去几天谷歌搜索时所做的那样。
我的应用程序正在与几个不同的 API 进行通信,但让我们首先考虑来自一个 API 的响应。来自每个端点的响应包含一些'common parameters',例如状态或错误消息,以及我们最感兴趣的单个对象或对象数组,它带来了重要数据,我们可能想对其进行编码、存储,放到Realm、CoreData等
例如,单个对象的响应:
{
"status": "success",
"response_code": 200,
"messages": [
"message1",
"message2"
]
"data": {
OBJECT we're interested in.
}
}
或者,用对象数组响应:
{
"status": "success",
"response_code": 200,
"messages": [
"message1",
"message2"
]
"data": [
{
OBJECT we're interested in.
},
{
OBJECT we're interested in.
}
]
}
好的。就是这么简单,容易理解。
现在,我想写一个 "Root" 对象,它将容纳 'common parameters' 或 status
、response_code
和 messages
并有另一个, 属性 对于我们感兴趣的特定对象(或对象数组)。
继承
第一种方法是创建一个根对象,如下所示:
class Root: Codable {
let status: String
let response_code: Int
let messages: [String]?
private enum CodingKeys: String, CodingKey {
case status, response_code, messages
}
required public init(from decoder: Decoder) throws {
let container = try? decoder.container(keyedBy: CodingKeys.self)
status = try container?.decodeIfPresent(String.self, forKey: .code) ?? ""
response_code = try container?.decodeIfPresent(Int.self, forKey: .error) ?? 0
messages = try container?.decodeIfPresent([String].self, forKey: .key)
}
public func encode(to encoder: Encoder) throws {}
}
一旦我有了这个 Root 对象,我就可以创建从这个 Root 对象继承的特定对象,并将我的特定对象传递给 JSONDecoder,这样,我就有了一个很好的解决方案。但是,这个解决方案对于数组 是失败的。 也许对于某些人来说它没有,但我怎么强调我都不想制作额外的 'plural' 对象,该对象只存在于保存对象数组 ,喜欢:
class Objects: Root {
let objects: [Object]
// Code that decodes array of "Object" from "data" key
}
struct Object: Codable {
let property1
let property2
let property3
// Code that decodes all properties of Object
}
它看起来不干净,它需要单独的对象来简单地保存一个数组,它在某些情况下由于继承而在存储到 Realm 时产生问题,它 最重要的是 产生可读性较差的代码。
泛型
我的第二个想法是尝试使用泛型,所以我做了 a little something like this:
struct Root<T: Codable>: Codable {
let status: String
let response_code: Int
let messages: [String]?
let data: T?
private enum CodingKeys: String, CodingKey {
case status, response_code, messages, data
}
required public init(from decoder: Decoder) throws {
let container = try? decoder.container(keyedBy: CodingKeys.self)
status = try container?.decodeIfPresent(String.self, forKey: .code) ?? ""
response_code = try container?.decodeIfPresent(Int.self, forKey: .error) ?? 0
messages = try container?.decodeIfPresent([String].self, forKey: .key)
data = try container.decodeIfPresent(T.self, forKey: .data)
}
public func encode(to encoder: Encoder) throws {}
}
有了这个,我可以像这样将单个对象和对象数组传递给 JSON 解码器:
let decodedValue = try JSONDecoder().decode(Root<Object>.self, from: data)
// or
let decodedValue = try JSONDecoder().decode(Root<[Object]>.self, from: data)
这很不错。我可以在 Root 结构的 .data 属性 中获取我需要的结构,并按我喜欢的方式使用它,作为单个对象或对象数组。我可以轻松地存储它,随心所欲地操作它,继承带来了上例。
这个想法对我来说失败的地方是当我想在某个不确定 T 设置的地方访问 'common properties' 时。
这是对我的应用程序中实际发生的事情的简化解释,我将对其进行一些扩展以解释此通用解决方案对我不起作用的地方,最后问我的问题。
问题与疑问
如顶部所述,应用程序使用 3 个 API,所有 3 个 API 都有不同的 Root
结构,当然还有很多不同的 "sub-structs" - 到给他们起名字。我在应用程序中有一个单独的地方,单个 APIResponse
对象可以返回到应用程序的 UI 部分,我在其中从 decoded value
中提取了 1 个可读错误,decoded value
就是那个'sub-struct',是我的 "specific Objects"、Car
、Dog
、House
、Phone
、
中的任何一个
使用 Inheritance 解决方案,我可以做这样的事情:
struct APIResponse <T> {
var value: T? {
didSet {
extractErrorDescription()
}
}
var errorDescription: String? = "Oops."
func extractErrorDescription() {
if let responseValue = value as? Root1, let error = responseValue.errors.first {
self.errorDescription = error
}
else if let responseValue = value as? Root2 {
self.errorDescription = responseValue.error
}
else if let responseValue = value as? Root3 {
self.errorDescription = responseValue.message
}
}
}
但是使用 Generics 解决方案,我无法做到这一点。如果我尝试使用 Root1
或 Root2
或 Root3
编写相同的代码,如 Generics 示例所示:
func extractErrorDescription() {
if let responseValue = value as? Root1, let error = responseValue.errors.first {
self.errorDescription = error
}
}
我会收到错误提示 Generic parameter 'T' could not be inferred in cast to 'Root1'
,在这里,我试图提取错误,但我不知道哪个子结构被传递给了 Root1。是 Root1<Dog>
还是 Root1<Phone>
还是 Root1<Car>
- 我不知道怎么弄清楚,我显然需要知道才能确定值是 Root1
还是Root2
或 Root3
。
我正在寻找的解决方案是一种可以让我区分 Root
对象与上面显示的泛型解决方案的解决方案,或者是一种允许我在一些完全不同的架构中解码的解决方案方式,记住我写的所有内容,尤其是避免 'plural' objects
*如果JSON没有通过JSON验证器,请无视,手写就是为了这个问题
**如果编写的代码没有运行,请忽略,这更多的是一个架构问题,而不是如何编译一些代码。
您在这里寻找的是协议。
protocol ErrorProviding {
var error: String? { get }
}
我有意将 errorDescription
更改为 error
,因为这似乎是您的根类型中的内容(但您绝对可以在此处重命名)。
然后 APIResponse 要求:
struct APIResponse<T: ErrorProviding> {
var value: T?
var error: String? { value?.error }
}
然后每个具有特殊处理的根类型实现协议:
extension Root1: ErrorProviding {
var error: String? { errors.first }
}
但是已经具有正确形状的简单根类型可以声明符合性,不需要额外的实现。
extension Root2: ErrorProviding {}
假设您想要的不仅仅是 error
,您可以将其设为 APIPayload
而不是 ErrorProviding
并添加任何其他常见要求。
附带说明一下,如果您只使用 Decodable 而不是使用带有空 encode
方法的 Codable,您的代码会更简单。如果一个类型不能真正被编码,它就不应该符合 Encodable。