Swift - 具有通用字典 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 属性值提供某种包装类型。具有关联值的枚举非常适合这项工作。由于您最清楚哪些类型应该作为属性,请随时从我的示例实现中添加/删除任何案例。

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] 字典中的属性值包装到包装枚举 case 中,但首先我们需要使类型符合 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] 类型之间使用一些额外的互操作性。要在 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)
    }
}

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

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 实现,但我们得到了我们想要的。现在,您可以轻松地对已经符合 Codable 的 ~Any~ 类型进行编码,方法是在新的 MyAttrubuteValue 枚举中添加一个额外的大小写。最后要说的一点是,我们在生产中使用了与此类似的方法,到目前为止我们一直很开心。

代码太多了,这里是一个 gist

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...