【问题标题】:Codable enum with multiple keys and associated values具有多个键和关联值的可编码枚举
【发布时间】:2019-08-25 07:13:51
【问题描述】:

当所有案例都有关联值时,我已经看到了有关如何使枚举符合 Codable 的答案,但我不清楚如何混合具有和不具有关联值的案例的枚举:

???如何在给定的情况下使用同一密钥的多个变体?

???如何编码/解码没有关联值的案例?

enum EmployeeClassification : Codable, Equatable {

case aaa
case bbb
case ccc(Int) // (year)

init?(rawValue: String?) {
    guard let val = rawValue?.lowercased() else {
        return nil
    }
    switch val {
        case "aaa", "a":
            self = .aaa
        case "bbb":
            self = .bbb
        case "ccc":
            self = .ccc(0)
        default: return nil
    }
}

// Codable
private enum CodingKeys: String, CodingKey {
    case aaa // ??? how can I accept "aaa", "AAA", and "a"?
    case bbb
    case ccc
}

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    if let value = try? container.decode(Int.self, forKey: .ccc) {
        self = .ccc(value)
        return
    }
    // ???
    // How do I decode the cases with no associated value?
}
func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    switch self {
    case .ccc(let year):
        try container.encode(year, forKey: .ccc)
    default:
        // ???
        // How do I encode cases with no associated value?
    }
}
}

【问题讨论】:

  • 任何情况下都不能有多个同名的case,但是可以从多个raw values中初始化一个case。

标签: swift enums codable


【解决方案1】:

使用init方法的假定原始字符串值作为枚举案例的(字符串)值

enum EmployeeClassification : Codable, Equatable {
    
    case aaa
    case bbb
    case ccc(Int) // (year)
    
    init?(rawValue: String?) {
        guard let val = rawValue?.lowercased() else {
            return nil
        }
        switch val {
        case "aaa", "a":
            self = .aaa
        case "bbb":
            self = .bbb
        case "ccc":
            self = .ccc(0)
        default: return nil
        }
    }
    
    // Codable
    private enum CodingKeys: String, CodingKey { case aaa, bbb, ccc }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let value = try? container.decode(Int.self, forKey: .ccc) {
            self = .ccc(value)
        } else if let aaaValue = try? container.decode(String.self, forKey: .aaa), ["aaa", "AAA", "a"].contains(aaaValue) {
            self = .aaa
        } else if let bbbValue = try? container.decode(String.self, forKey: .bbb), bbbValue == "bbb" {
            self = .bbb
        } else {
            throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "Data doesn't match"))
        }
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case .aaa: try container.encode("aaa", forKey: .aaa)
        case .bbb: try container.encode("bbb", forKey: .bbb)
        case .ccc(let year): try container.encode(year, forKey: .ccc)
        }
    }
}

解码错误非常普遍。您可以为每个 CodingKey 抛出更具体的错误

【讨论】:

  • 这太棒了,比我见过的所有尝试一些疯狂的东西的博客复杂得多。 .我有一个接近你的“解决方案”,但没有奏效。缺少的部分是 init?(rawValue: String?) 方法。
  • 很好的答案!对我有很大帮助。
【解决方案2】:

从 Swift 5.5 开始,具有关联值的枚举获得了自动符合 Codable 的能力。有关实施细节的更多详细信息,请参阅thisswift-evolution 提案。

所以,这对你的枚举来说已经足够了:

enum EmployeeClassification : Codable, Equatable {
    case aaa
    case bbb
    case ccc(Int) // (year)

不再有CodingKeys,不再有init(from:),或encode(to:)

【讨论】:

    【解决方案3】:

    感谢@vadian 的精彩回答??

    另一种方法是为任何具有关联值和(或)空案例的枚举实现自定义 Decodable / Encodable 方法 - 使用从根 container 调用的 nestedContainer 方法。

    这种方式在 Swift 5.5 的 Swift Evolution 提案中描述了对 Codable 一致性的自动综合支持,以符合具有关联值的枚举。 我从提案中得到的所有细节和下一个实现你也可以看看:https://github.com/apple/swift-evolution/blob/main/proposals/0295-codable-synthesis-for-enums-with-associated-values.md

    我还扩展了提案中的示例,以准确涵盖作者的问题。

    enum Command: Codable {
      case load(String)
      case store(key: String, Int)
      case eraseAll
    }
    

    Encodableencode(to:) 实现如下所示:

    public func encode(to encoder: Encoder) throws {
      var container = encoder.container(keyedBy: CodingKeys.self)
      switch self {
      case let .load(key):
        var nestedContainer = container.nestedUnkeyedContainer(forKey: .load)
        try nestedContainer.encode(key)
      case let .store(key, value):
        var nestedContainer = container.nestedContainer(keyedBy: StoreCodingKeys.self, forKey: .store)
        try nestedContainer.encode(key, forKey: .key)
        try nestedContainer.encode(value, forKey: .value)
      case .eraseAll:
        var nestedContainer = container.nestedUnkeyedContainer(forKey: .eraseAll)
        try nestedContainer.encodeNil()
      }
    }
    

    请注意一些修改:(1) 对于loaderaseAll 案例,我在提案中建议使用nestedUnkeyedContainereraseAll 是我也添加的没有关联值的新案例。

    Decodableinit(from:) 实现如下所示:

    public init(from decoder: Decoder) throws {
      let container = try decoder.container(keyedBy: CodingKeys.self)
      if container.allKeys.count != 1 {
        let context = DecodingError.Context(
          codingPath: container.codingPath,
          debugDescription: "Invalid number of keys found, expected one.")
        throw DecodingError.typeMismatch(Command.self, context)
      }
    
      switch container.allKeys.first.unsafelyUnwrapped {
      case .load:
        let nestedContainer = try container.nestedUnkeyedContainer(forKey: .load)
        self = .load(try nestedContainer.decode(String.self))
      case .store:
        let nestedContainer = try container.nestedContainer(keyedBy: StoreCodingKeys.self, forKey: .store)
        self = .store(
          key: try nestedContainer.decode(String.self, forKey: .key),
          value: try nestedContainer.decode(Int.self, forKey: .value))
      case .eraseAll:
        _ = try container.nestedUnkeyedContainer(forKey: .eraseAll)
        self = .eraseAll
      }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-09-24
      • 1970-01-01
      • 1970-01-01
      • 2017-08-19
      • 1970-01-01
      相关资源
      最近更新 更多