具有通用 属性 的结构符合 Swift 中的 Encodable
struct with generic property conforming to Encodable in Swift
我一直在寻找一个结构,有一种通用的方法属性,其中类型在运行时定义为:
struct Dog {
let id: String
let value: ??
}
在构建 json
对象时,这可能很有用的一个简单用例。 node
可以是int
、string
、bool
、数组等,但是除了类型可以改变,对象node
一样。
经过思考并使用 protocols
失败(出现常见的 protocol 'X' can only be used as a generic constraint because it has Self or associated type requirements
错误)后,我想出了 2 种不同的解决方案,#0 使用 type erasure
和 #1 使用 type-erasure
和 generics
.
#0(类型擦除)
struct AnyDog: Encodable {
enum ValueType: Encodable {
case int(Int)
case string(String)
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .int(let value):
try container.encode(value)
case .string(let value):
try container.encode(value)
}
}
}
let id: String
let value: ValueType
init(_ dog: DogString) {
self.id = dog.id
self.value = .string(dog.value)
}
init(_ dog: DogInt) {
self.id = dog.id
self.value = .int(dog.value)
}
}
struct DogString: Encodable{
let id: String
let value: String
var toAny: AnyDog {
return AnyDog(self)
}
}
struct DogInt: Encodable {
let id: String
let value: Int
var toAny: AnyDog {
return AnyDog(self)
}
}
let dogs: [AnyDog] = [
DogString(id: "123", value: "pop").toAny,
DogInt(id: "123", value: 123).toAny,
]
do {
let data = try JSONEncoder().encode(dogs)
print(String(data: data, encoding: .utf8)!)
} catch {
print(error)
}
#1(类型擦除 + 泛型)
struct AnyDog: Encodable {
enum ValueType: Encodable {
case int(Int)
case string(String)
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .int(let value):
try container.encode(value)
case .string(let value):
try container.encode(value)
}
}
}
let id: String
let value: ValueType
}
struct Dog<T: Encodable>: Encodable{
let id: String
let value: T
var toAny: AnyDog {
switch T.self {
case is String.Type:
return AnyDog(id: id, value: .string(value as! String))
case is Int.Type:
return AnyDog(id: id, value: .int(value as! Int))
default:
preconditionFailure("Invalid Type")
}
}
}
let dogs: [AnyDog] = [
Dog<String>(id: "123", value: "pop").toAny ,
Dog<Int>(id: "123", value: 123).toAny,
]
do {
let data = try JSONEncoder().encode(dogs)
print(String(data: data, encoding: .utf8)!)
} catch {
print(error)
}
两种方法都给出了适当的结果:
[{"id":"123","value":"pop"},{"id":"123","value":123}]
即使结果相同,我坚信如果考虑更多类型,方法 #1 更 scalable
,但仍然需要在 2 个不同的区域对每个进行更改添加类型。
我相信有更好的方法可以实现这一点,但目前还没有找到。很高兴听到任何关于它的想法或建议。
编辑 #0 2020/02/08:可选值
利用 Rob 的出色回答,我现在正尝试让 value
成为可选的,如下所示:
struct Dog: Encodable {
// This is the key to the solution: bury the type of value inside a closure
let valueEncoder: (Encoder) throws -> Void
init<T: Encodable>(id: String, value: T?) {
self.valueEncoder = {
var container = [=16=].container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(value, forKey: .value)
}
}
enum CodingKeys: String, CodingKey {
case id, value
}
func encode(to encoder: Encoder) throws {
try valueEncoder(encoder)
}
}
let dogs = [
Dog(id: "123", value: 123),
Dog(id: "456", value: nil),
]
do {
let data = try JSONEncoder().encode(dogs)
print(String(data: data, encoding: .utf8)!)
} catch {
print(error)
}
此时无法再推断T
,抛出如下错误:
generic parameter 'T' could not be inferred
如果为 value
给出了 Optional
类型,我正在寻找使用 Rob 给出以下结果的答案的可能性:
[{"id":"123","value":123},{"id":"456","value":null}]
编辑 #1 2020/02/08:解决方案
好吧,我太专注于给 value
值 nil
,以至于我没有意识到 nil
没有任何类型导致推理错误。
提供可选类型使其工作:
let optString: String? = nil
let dogs = [
Dog(id: "123", value: 123),
Dog(id: "456", value: optString),
]
如果您所描述的确实是您想要的,则无需任何这些类型的橡皮擦也可以完成。你所需要的只是一个闭包。 (但这假设 Dog
真的只存在于编码中,正如你所描述的,除此之外没有任何东西需要 value
。)
struct Dog: Encodable {
// This is the key to the solution: bury the type of value inside a closure
let valueEncoder: (Encoder) throws -> Void
init<T: Encodable>(id: String, value: T) {
self.valueEncoder = {
var container = [=10=].container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(value, forKey: .value)
}
}
enum CodingKeys: String, CodingKey {
case id, value
}
func encode(to encoder: Encoder) throws {
try valueEncoder(encoder)
}
}
由于 value
仅在 valueEncoder
内部使用,世界其他地方不需要知道它的类型(Dog 甚至不需要知道它的类型)。这就是类型擦除的意义所在。它不需要制作额外的包装器类型或通用结构。
如果您想保留 DogString
和 DogInt
等类型,您也可以通过添加协议来实现:
protocol Dog: Encodable {
associatedtype Value: Encodable
var id: String { get }
var value: Value { get }
}
然后制作一个 DogEncoder 来处理编码(与上面相同,除了一个新的 init 方法):
struct DogEncoder: Encodable {
let valueEncoder: (Encoder) throws -> Void
init<D: Dog>(_ dog: D) {
self.valueEncoder = {
var container = [=12=].container(keyedBy: CodingKeys.self)
try container.encode(dog.id, forKey: .id)
try container.encode(dog.value, forKey: .value)
}
}
enum CodingKeys: String, CodingKey {
case id, value
}
func encode(to encoder: Encoder) throws {
try valueEncoder(encoder)
}
}
几种狗:
struct DogString: Dog {
let id: String
let value: String
}
struct DogInt: Dog {
let id: String
let value: Int
}
将它们放入编码器数组中:
let dogs = [
DogEncoder(DogString(id: "123", value: "pop")),
DogEncoder(DogInt(id: "123", value: 123)),
]
let data = try JSONEncoder().encode(dogs)
这是另一个可能有帮助的解决方案:
struct Dog<V: Codable>: Codable {
let id: String
let value: V
}
我一直在寻找一个结构,有一种通用的方法属性,其中类型在运行时定义为:
struct Dog {
let id: String
let value: ??
}
在构建 json
对象时,这可能很有用的一个简单用例。 node
可以是int
、string
、bool
、数组等,但是除了类型可以改变,对象node
一样。
经过思考并使用 protocols
失败(出现常见的 protocol 'X' can only be used as a generic constraint because it has Self or associated type requirements
错误)后,我想出了 2 种不同的解决方案,#0 使用 type erasure
和 #1 使用 type-erasure
和 generics
.
#0(类型擦除)
struct AnyDog: Encodable {
enum ValueType: Encodable {
case int(Int)
case string(String)
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .int(let value):
try container.encode(value)
case .string(let value):
try container.encode(value)
}
}
}
let id: String
let value: ValueType
init(_ dog: DogString) {
self.id = dog.id
self.value = .string(dog.value)
}
init(_ dog: DogInt) {
self.id = dog.id
self.value = .int(dog.value)
}
}
struct DogString: Encodable{
let id: String
let value: String
var toAny: AnyDog {
return AnyDog(self)
}
}
struct DogInt: Encodable {
let id: String
let value: Int
var toAny: AnyDog {
return AnyDog(self)
}
}
let dogs: [AnyDog] = [
DogString(id: "123", value: "pop").toAny,
DogInt(id: "123", value: 123).toAny,
]
do {
let data = try JSONEncoder().encode(dogs)
print(String(data: data, encoding: .utf8)!)
} catch {
print(error)
}
#1(类型擦除 + 泛型)
struct AnyDog: Encodable {
enum ValueType: Encodable {
case int(Int)
case string(String)
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .int(let value):
try container.encode(value)
case .string(let value):
try container.encode(value)
}
}
}
let id: String
let value: ValueType
}
struct Dog<T: Encodable>: Encodable{
let id: String
let value: T
var toAny: AnyDog {
switch T.self {
case is String.Type:
return AnyDog(id: id, value: .string(value as! String))
case is Int.Type:
return AnyDog(id: id, value: .int(value as! Int))
default:
preconditionFailure("Invalid Type")
}
}
}
let dogs: [AnyDog] = [
Dog<String>(id: "123", value: "pop").toAny ,
Dog<Int>(id: "123", value: 123).toAny,
]
do {
let data = try JSONEncoder().encode(dogs)
print(String(data: data, encoding: .utf8)!)
} catch {
print(error)
}
两种方法都给出了适当的结果:
[{"id":"123","value":"pop"},{"id":"123","value":123}]
即使结果相同,我坚信如果考虑更多类型,方法 #1 更 scalable
,但仍然需要在 2 个不同的区域对每个进行更改添加类型。
我相信有更好的方法可以实现这一点,但目前还没有找到。很高兴听到任何关于它的想法或建议。
编辑 #0 2020/02/08:可选值
利用 Rob 的出色回答,我现在正尝试让 value
成为可选的,如下所示:
struct Dog: Encodable {
// This is the key to the solution: bury the type of value inside a closure
let valueEncoder: (Encoder) throws -> Void
init<T: Encodable>(id: String, value: T?) {
self.valueEncoder = {
var container = [=16=].container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(value, forKey: .value)
}
}
enum CodingKeys: String, CodingKey {
case id, value
}
func encode(to encoder: Encoder) throws {
try valueEncoder(encoder)
}
}
let dogs = [
Dog(id: "123", value: 123),
Dog(id: "456", value: nil),
]
do {
let data = try JSONEncoder().encode(dogs)
print(String(data: data, encoding: .utf8)!)
} catch {
print(error)
}
此时无法再推断T
,抛出如下错误:
generic parameter 'T' could not be inferred
如果为 value
给出了 Optional
类型,我正在寻找使用 Rob 给出以下结果的答案的可能性:
[{"id":"123","value":123},{"id":"456","value":null}]
编辑 #1 2020/02/08:解决方案
好吧,我太专注于给 value
值 nil
,以至于我没有意识到 nil
没有任何类型导致推理错误。
提供可选类型使其工作:
let optString: String? = nil
let dogs = [
Dog(id: "123", value: 123),
Dog(id: "456", value: optString),
]
如果您所描述的确实是您想要的,则无需任何这些类型的橡皮擦也可以完成。你所需要的只是一个闭包。 (但这假设 Dog
真的只存在于编码中,正如你所描述的,除此之外没有任何东西需要 value
。)
struct Dog: Encodable {
// This is the key to the solution: bury the type of value inside a closure
let valueEncoder: (Encoder) throws -> Void
init<T: Encodable>(id: String, value: T) {
self.valueEncoder = {
var container = [=10=].container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(value, forKey: .value)
}
}
enum CodingKeys: String, CodingKey {
case id, value
}
func encode(to encoder: Encoder) throws {
try valueEncoder(encoder)
}
}
由于 value
仅在 valueEncoder
内部使用,世界其他地方不需要知道它的类型(Dog 甚至不需要知道它的类型)。这就是类型擦除的意义所在。它不需要制作额外的包装器类型或通用结构。
如果您想保留 DogString
和 DogInt
等类型,您也可以通过添加协议来实现:
protocol Dog: Encodable {
associatedtype Value: Encodable
var id: String { get }
var value: Value { get }
}
然后制作一个 DogEncoder 来处理编码(与上面相同,除了一个新的 init 方法):
struct DogEncoder: Encodable {
let valueEncoder: (Encoder) throws -> Void
init<D: Dog>(_ dog: D) {
self.valueEncoder = {
var container = [=12=].container(keyedBy: CodingKeys.self)
try container.encode(dog.id, forKey: .id)
try container.encode(dog.value, forKey: .value)
}
}
enum CodingKeys: String, CodingKey {
case id, value
}
func encode(to encoder: Encoder) throws {
try valueEncoder(encoder)
}
}
几种狗:
struct DogString: Dog {
let id: String
let value: String
}
struct DogInt: Dog {
let id: String
let value: Int
}
将它们放入编码器数组中:
let dogs = [
DogEncoder(DogString(id: "123", value: "pop")),
DogEncoder(DogInt(id: "123", value: 123)),
]
let data = try JSONEncoder().encode(dogs)
这是另一个可能有帮助的解决方案:
struct Dog<V: Codable>: Codable {
let id: String
let value: V
}