具有多个键和关联值的可编码枚举

Codable enum with multiple keys and associated values

我看到了关于如何在所有情况下都有关联值时使枚举符合 Codable 的答案,但我不清楚如何混合具有和没有关联值的情况下的枚举:

???如何针对给定案例使用同一密钥的多个变体?

???我如何 encode/decode 个没有关联值的案例?

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

使用 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

抛出更具体的错误

感谢@vadian 的精彩回答

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

这种方式在 Swift Evolution proposal for Swift 5.5 中描述了支持自动合成 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 的情况,我使用 nestedUnkeyedContainer 而不是提案中建议的 eraseAll 是新的情况我也添加了没有关联的值。

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

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

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

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

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