【问题标题】:Swift - Codable struct with generic Dictionary var?Swift - 具有通用 Dictionary var 的可编码结构?
【发布时间】:2021-01-26 21:29:00
【问题描述】:

我有一个 Swift struct,看起来像这样:

struct MyStruct: Codable {

  var id: String
  var name: String
  var createdDate: Date

}

为此,我想添加另一个属性:[String:Any] 字典。结果如下所示:

struct MyStruct: Codable {

  var id: String
  var name: String
  var createdDate: Date

  var attributes: [String:Any] = [:]
}

最后,我希望能够将我的 MyStruct 实例序列化为 JSON 字符串,反之亦然。但是,当我去构建时,我收到一条错误消息,

Type 'MyStruct' does not conform to protocol 'Codable'
Type 'MyStruct' does not conform to protocol 'Decodable'

显然是 attributes var 使我的构建绊倒了,但我不确定如何才能获得预期的结果。知道如何编码我的struct 来支持这一点吗?

【问题讨论】:

  • 您无法对 Any 进行编码/解码,因此您需要将其替换为真实类型。
  • Any 和泛型是两个完全不同的东西。
  • 当您说这是[String: Any] 时,这表明这里可以接受 any 类型,并且您希望对其进行编码和解码。例如,我可以将 UIViewController 或 CBPeripheral 放入该字典中(它甚至没有公共 init)。对于“属性”字典来说,这些似乎不合适。我怀疑你有一些对属性有意义的事情的小清单,所以你需要为此创建一个类型。
  • @vadian 我并不是指“泛型”中的泛型。我的意思是,我可以在该字典中有多种不同的数据类型。也许它有字符串。也许是整数。也许是另一个对象。我希望能够将该字典与 JSON 进行编码/解码。
  • 如果您的意思是“任何适合 JSON 的类型”,请查看 stackoverflow.com/a/65902852/97337 以进行讨论。

标签: json swift codable decodable encodable


【解决方案1】:

既然 cmets 已经指出 Any 类型与泛型无关,让我直接进入解决方案。

首先需要为 Any 属性值提供某种包装类型。具有相关值的枚举非常适合这项工作。由于您最清楚应该将哪些类型作为属性,请随时从我的示例实现中添加/删除任何案例。

enum MyAttrubuteValue {
    case string(String)
    case date(Date)
    case data(Data)
    case bool(Bool)
    case double(Double)
    case int(Int)
    case float(Float)
}

我们稍后会将[String: Any] 字典中的属性值包装到包装器枚举案例中,但首先我们需要使类型符合Codable 协议。我使用singleValueContainer() 进行解码/编码,因此最终的 json 将产生一个常规的 json dicts。

extension MyAttrubuteValue: Codable {

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let string = try? container.decode(String.self) {
            self = .string(string)
        } else if let date = try? container.decode(Date.self) {
            self = .date(date)
        } else if let data = try? container.decode(Data.self) {
            self = .data(data)
        } else if let bool = try? container.decode(Bool.self) {
            self = .bool(bool)
        } else if let double = try? container.decode(Double.self) {
            self = .double(double)
        } else if let int = try? container.decode(Int.self) {
            self = .int(int)
        } else if let float = try? container.decode(Float.self) {
            self = .float(float)
        } else {
            fatalError()
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .string(let string):
            try? container.encode(string)
        case .date(let date):
            try? container.encode(date)
        case .data(let data):
            try? container.encode(data)
        case .bool(let bool):
            try? container.encode(bool)
        case .double(let double):
            try? container.encode(double)
        case .int(let int):
            try? container.encode(int)
        case .float(let float):
            try? container.encode(float)
        }
    }

}

现在我们可以开始了,但是在解码/编码属性之前,我们可以在[String: Any] 和[String: MyAttrubuteValue] 类型之间使用一些额外的互操作性。要在 Any 和 MyAttrubuteValue 之间轻松映射,请添加以下内容:

extension MyAttrubuteValue {

    var value: Any {
        switch self {
        case .string(let value):
            return value
        case .date(let value):
            return value
        case .data(let value):
            return value
        case .bool(let value):
            return value
        case .double(let value):
            return value
        case .int(let value):
            return value
        case .float(let value):
            return value
        }
    }

    init?(_ value: Any) {
        if let string = value as? String {
            self = .string(string)
        } else if let date = value as? Date {
            self = .date(date)
        } else if let data = value as? Data {
            self = .data(data)
        } else if let bool = value as? Bool {
            self = .bool(bool)
        } else if let double = value as? Double {
            self = .double(double)
        } else if let int = value as? Int {
            self = .int(int)
        } else if let float = value as? Float {
            self = .float(float)
        } else {
            return nil
        }
    }

}

现在,通过快速的value 访问和新的init,我们可以轻松地映射值。我们还确保辅助属性仅适用于具体类型的字典,即我们正在使用的字典。

extension Dictionary where Key == String, Value == Any {
    var encodable: [Key: MyAttrubuteValue] {
        compactMapValues(MyAttrubuteValue.init)
    }
}

extension Dictionary where Key == String, Value == MyAttrubuteValue {
    var any: [Key: Any] {
        mapValues(\.value)
    }
}

现在是最后一部分,MyStruct 的自定义 Codable 实现

extension MyStruct: Codable {

    enum CodingKeys: String, CodingKey {
        case id = "id"
        case name = "name"
        case createdDate = "createdDate"
        case attributes = "attributes"
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(name, forKey: .name)
        try container.encode(createdDate, forKey: .createdDate)
        try container.encode(attributes.encodable, forKey: .attributes)
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(String.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
        createdDate = try container.decode(Date.self, forKey: .createdDate)
        attributes = try container.decode(
            [String: MyAttrubuteValue].self, forKey: .attributes
        ).any
    }

}

这个解决方案相当长,但同时也很简单。我们失去了自动的Codable 实现,但我们得到了我们想要的。现在,您可以通过在新的 MyAttrubuteValue 枚举中添加一个额外的大小写来轻松地对已经符合 Codable 的 ~Any~ 类型进行编码。最后要说的一件事是,我们在生产中使用了与此类似的方法,到目前为止我们很高兴。

代码很多,这里是gist。

【讨论】:

    猜你喜欢
    • 2018-12-03
    • 2019-10-11
    • 2021-09-23
    • 2018-05-11
    • 1970-01-01
    • 2020-03-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多