Swift - 具有通用字典变量的 Codable 结构?

Swift - Codable struct with generic Dictionary var?

我有一个 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 属性值进行某种包装类型。具有关联值的枚举非常适合这项工作。由于您最了解什么类型可以作为属性,因此请随意 add/remove 我的示例实现中的任何情况。

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() 用于 decoding/encoding,因此最终的 json 将生成常规的 json 指令。

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)
        }
    }

}

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

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.