【问题标题】:Codable enum with default case in Swift 4Swift 4 中默认情况下的可编码枚举
【发布时间】:2018-09-16 15:48:06
【问题描述】:

我定义了一个enum,如下:

enum Type: String, Codable {
    case text = "text"
    case image = "image"
    case document = "document"
    case profile = "profile"
    case sign = "sign"
    case inputDate = "input_date"
    case inputText = "input_text"
    case inputNumber = "input_number"
    case inputOption = "input_option"

    case unknown
}

映射一个 JSON 字符串属性。 自动序列化和反序列化工作正常,但我发现如果遇到不同的字符串,反序列化失败。

是否可以定义一个映射任何其他可用案例的unknown 案例?

这可能非常有用,因为这些数据来自 RESTFul API,未来可能会发生变化。

【问题讨论】:

  • 您可以将Type 的变量声明为可选。
  • @AndréSlotta 我已经尝试过这个解决方案,但它不起作用。我在反序列化过程中出错。
  • 你能再展示一些你的代码吗?

标签: ios swift enums swift4 codable


【解决方案1】:

您必须实现 init(from decoder: Decoder) throws 初始化程序并检查有效值:

struct SomeStruct: Codable {

    enum SomeType: String, Codable {
        case text
        case image
        case document
        case profile
        case sign
        case inputDate = "input_date"
        case inputText = "input_text"
        case inputNumber = "input_number"
        case inputOption = "input_option"

        case unknown
    }

    var someType: SomeType

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        someType = (try? values.decode(SomeType.self, forKey: .someType)) ?? .unknown
    }

}

【讨论】:

    【解决方案2】:

    您可以扩展您的 Codable 类型并在失败的情况下分配默认值:

    enum Type: String {
        case text,
             image,
             document,
             profile,
             sign,
             inputDate = "input_date",
             inputText = "input_text" ,
             inputNumber = "input_number",
             inputOption = "input_option",
             unknown
    }
    extension Type: Codable {
        public init(from decoder: Decoder) throws {
            self = try Type(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
        }
    }
    

    编辑/更新:

    Xcode 11.2 • Swift 5.1 或更高版本

    创建一个默认为 CaseIterable & Decodable 枚举的最后一种情况的协议:

    protocol CaseIterableDefaultsLast: Decodable & CaseIterable & RawRepresentable
    where RawValue: Decodable, AllCases: BidirectionalCollection { }
    
    extension CaseIterableDefaultsLast {
        init(from decoder: Decoder) throws {
            self = try Self(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? Self.allCases.last!
        }
    }
    

    游乐场测试:

    enum Type: String, CaseIterableDefaultsLast {
        case text, image, document, profile, sign, inputDate = "input_date", inputText = "input_text" , inputNumber = "input_number", inputOption = "input_option", unknown
    }
    

    let types = try! JSONDecoder().decode([Type].self , from: Data(#"["text","image","sound"]"#.utf8))  // [text, image, unknown]
    

    【讨论】:

    • 这应该是公认的答案!完美运行
    • 如果您经常使用它,则稍微通用一点。将 try Type 替换为 try type(of: self).init
    • @Daniel 有没有办法制作一个完全通用的 CodableWithUnknown 协议或类似的东西?
    • 拒绝评论解释其原因的评论将不胜感激,并允许我修复和/或改进我的答案有什么问题。没有理由的否决是没有意义的
    • 这个真的很干净很简单!
    【解决方案3】:

    您可以删除 Type 的原始类型,并设置处理关联值的 unknown 大小写。但这是有代价的。您以某种方式需要您的案例的原始值。受thisthis SO 答案的启发,我想出了这个优雅的解决方案来解决您的问题。

    为了能够存储原始值,我们将维护另一个枚举,但是是私有的:

    enum Type {
        case text
        case image
        case document
        case profile
        case sign
        case inputDate
        case inputText
        case inputNumber
        case inputOption
        case unknown(String)
    
        // Make this private
        private enum RawValues: String, Codable {
            case text = "text"
            case image = "image"
            case document = "document"
            case profile = "profile"
            case sign = "sign"
            case inputDate = "input_date"
            case inputText = "input_text"
            case inputNumber = "input_number"
            case inputOption = "input_option"
            // No such case here for the unknowns
        }
    }
    

    encoding & decoding 部分移至扩展:

    可解码部分:

    extension Type: Decodable {
        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            // As you already know your RawValues is String actually, you decode String here
            let stringForRawValues = try container.decode(String.self) 
            // This is the trick here...
            switch stringForRawValues { 
            // Now You can switch over this String with cases from RawValues since it is String
            case RawValues.text.rawValue:
                self = .text
            case RawValues.image.rawValue:
                self = .image
            case RawValues.document.rawValue:
                self = .document
            case RawValues.profile.rawValue:
                self = .profile
            case RawValues.sign.rawValue:
                self = .sign
            case RawValues.inputDate.rawValue:
                self = .inputDate
            case RawValues.inputText.rawValue:
                self = .inputText
            case RawValues.inputNumber.rawValue:
                self = .inputNumber
            case RawValues.inputOption.rawValue:
                self = .inputOption
    
            // Now handle all unknown types. You just pass the String to Type's unknown case. 
            // And this is true for every other unknowns that aren't defined in your RawValues
            default: 
                self = .unknown(stringForRawValues)
            }
        }
    }
    

    可编码部分:

    extension Type: Encodable {
        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            switch self {
            case .text:
                try container.encode(RawValues.text)
            case .image:
                try container.encode(RawValues.image)
            case .document:
                try container.encode(RawValues.document)
            case .profile:
                try container.encode(RawValues.profile)
            case .sign:
                try container.encode(RawValues.sign)
            case .inputDate:
                try container.encode(RawValues.inputDate)
            case .inputText:
                try container.encode(RawValues.inputText)
            case .inputNumber:
                try container.encode(RawValues.inputNumber)
            case .inputOption:
                try container.encode(RawValues.inputOption)
    
            case .unknown(let string): 
                // You get the actual String here from the associated value and just encode it
                try container.encode(string)
            }
        }
    }
    

    示例:

    我只是将它包装在一个容器结构中(因为我们将使用 JSONEncoder/JSONDecoder)作为:

    struct Root: Codable {
        let type: Type
    }
    

    对于未知大小写以外的值:

    let rootObject = Root(type: Type.document)
    do {
        let encodedRoot = try JSONEncoder().encode(rootObject)
        do {
            let decodedRoot = try JSONDecoder().decode(Root.self, from: encodedRoot)
            print(decodedRoot.type) // document
        } catch {
            print(error)
        }
    } catch {
        print(error)
    }
    

    对于大小写未知的值:

    let rootObject = Root(type: Type.unknown("new type"))
    do {
        let encodedRoot = try JSONEncoder().encode(rootObject)
        do {
            let decodedRoot = try JSONDecoder().decode(Root.self, from: encodedRoot)
            print(decodedRoot.type) // unknown("new type")
        } catch {
            print(error)
        }
    } catch {
        print(error)
    }
    

    我将示例与本地对象放在一起。您可以尝试使用 REST API 响应。

    【讨论】:

      【解决方案4】:

      这是基于nayem 的答案的替代方案,它通过使用内部RawValues 初始化的可选绑定提供了稍微更精简的语法:

      enum MyEnum: Codable {
      
          case a, b, c
          case other(name: String)
      
          private enum RawValue: String, Codable {
      
              case a = "a"
              case b = "b"
              case c = "c"
          }
      
          init(from decoder: Decoder) throws {
              let container = try decoder.singleValueContainer()
              let decodedString = try container.decode(String.self)
      
              if let value = RawValue(rawValue: decodedString) {
                  switch value {
                  case .a:
                      self = .a
                  case .b:
                      self = .b
                  case .c:
                      self = .c
                  }
              } else {
                  self = .other(name: decodedString)
              }
          }
      
          func encode(to encoder: Encoder) throws {
              var container = encoder.singleValueContainer()
      
              switch self {
              case .a:
                  try container.encode(RawValue.a)
              case .b:
                  try container.encode(RawValue.b)
              case .c:
                  try container.encode(RawValue.c)
              case .other(let name):
                  try container.encode(name)
              }
          }
      }
      

      如果您确定所有现有枚举案例名称都与它们所代表的基础字符串值匹配,您可以将RawValue 简化为:

      private enum RawValue: String, Codable {
      
          case a, b, c
      }
      

      ...和encode(to:) 到:

      func encode(to encoder: Encoder) throws {
          var container = encoder.singleValueContainer()
      
          if let rawValue = RawValue(rawValue: String(describing: self)) {
              try container.encode(rawValue)
          } else if case .other(let name) = self {
              try container.encode(name)
          }
      }
      

      这是一个使用此方法的实际示例,例如,您想对具有要建模为枚举的属性的 SomeValue 建模:

      struct SomeValue: Codable {
      
          enum MyEnum: Codable {
      
              case a, b, c
              case other(name: String)
      
              private enum RawValue: String, Codable {
      
                  case a = "a"
                  case b = "b"
                  case c = "letter_c"
              }
      
              init(from decoder: Decoder) throws {
                  let container = try decoder.singleValueContainer()
                  let decodedString = try container.decode(String.self)
      
                  if let value = RawValue(rawValue: decodedString) {
                      switch value {
                      case .a:
                          self = .a
                      case .b:
                          self = .b
                      case .c:
                          self = .c
                      }
                  } else {
                      self = .other(name: decodedString)
                  }
              }
      
              func encode(to encoder: Encoder) throws {
                  var container = encoder.singleValueContainer()
      
                  switch self {
                  case .a:
                      try container.encode(RawValue.a)
                  case .b:
                      try container.encode(RawValue.b)
                  case .c:
                      try container.encode(RawValue.c)
                  case .other(let name):
                      try container.encode(name)
                  }
              }
          }
      
      }
      
      let jsonData = """
      [
          { "value": "a" },
          { "value": "letter_c" },
          { "value": "c" },
          { "value": "Other value" }
      ]
      """.data(using: .utf8)!
      
      let decoder = JSONDecoder()
      
      if let values = try? decoder.decode([SomeValue].self, from: jsonData) {
          values.forEach { print($0.value) }
      
          let encoder = JSONEncoder()
      
          if let encodedJson = try? encoder.encode(values) {
              print(String(data: encodedJson, encoding: .utf8)!)
          }
      }
      
      
      /* Prints:
       a
       c
       other(name: "c")
       other(name: "Other value")
       [{"value":"a"},{"value":"letter_c"},{"value":"c"},{"value":"Other value"}]
       */
      

      【讨论】:

        【解决方案5】:

        添加此扩展并设置 YourEnumName

        extension <#YourEnumName#>: Codable {
            public init(from decoder: Decoder) throws {
                self = try <#YourEnumName#>(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
            }
        }
        

        【讨论】:

          【解决方案6】:

          @LeoDabus 感谢您的回答。我对它们进行了一些修改,以制作一个似乎对我有用的字符串枚举协议:

          protocol CodableWithUnknown: Codable {}
          extension CodableWithUnknown where Self: RawRepresentable, Self.RawValue == String {
              init(from decoder: Decoder) throws {
                  do {
                      try self = Self(rawValue: decoder.singleValueContainer().decode(RawValue.self))!
                  } catch {
                      if let unknown = Self(rawValue: "unknown") {
                          self = unknown
                      } else {
                          throw error
                      }
                  }
              }
          }
          

          【讨论】:

          • 我不会强行展开或使用 do catch 。如果您想将枚举类型限制为 String,您可以执行以下操作:protocol CaseIterableDefaultsLast: Codable &amp; CaseIterable { } extension CaseIterableDefaultsLast where Self: RawRepresentable, Self.RawValue == String, Self.AllCases: BidirectionalCollection { init(from decoder: Decoder) throws { self = try Self(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? Self.allCases.last! } }
          • @LeoDabus 是的,这更简单。谢谢!
          【解决方案7】:
          enum Type: String, Codable, Equatable {
              case image
              case document
              case unknown
          
              public init(from decoder: Decoder) throws {
                  guard let rawValue = try? decoder.singleValueContainer().decode(String.self) else {
                      self = .unknown
                      return
                  }
                  self = Type(rawValue: rawValue) ?? .unknown
              }
          }
          

          【讨论】:

          • 添加说明
          【解决方案8】:

          让我们从一个测试用例开始。我们希望这会通过:

              func testCodableEnumWithUnknown() throws {
                  enum Fruit: String, Decodable, CodableEnumWithUnknown {
                      case banana
                      case apple
          
                      case unknown
                  }
                  struct Container: Decodable {
                      let fruit: Fruit
                  }
                  let data = #"{"fruit": "orange"}"#.data(using: .utf8)!
                  let val = try JSONDecoder().decode(Container.self, from: data)
                  XCTAssert(val.fruit == .unknown)
              }
          

          我们的协议CodableEnumWithUnknown 表示支持unknown 案例,如果数据中出现未知值,解码器应使用该案例。

          然后解决办法:

          public protocol CodableEnumWithUnknown: Codable, RawRepresentable {
              static var unknown: Self { get }
          }
          
          public extension CodableEnumWithUnknown where Self: RawRepresentable, Self.RawValue == String {
          
              init(from decoder: Decoder) throws {
                  self = (try? Self(rawValue: decoder.singleValueContainer().decode(RawValue.self))) ?? Self.unknown
              }
          }
          

          诀窍是让您的枚举使用CodableEnumWithUnknown 协议实现并添加unknown 案例。

          我赞成使用其他帖子中提到的.allCases.last! 实现的上述解决方案,因为我发现它们有点脆弱,因为编译器没有对它们进行类型检查。

          【讨论】:

            【解决方案9】:

            您可以使用此扩展程序进行编码/解码 (此 sn-p 支持 Int 和 String RawValue 类型的枚举,但可以轻松扩展以适应其他类型)

            extension NSCoder {
                
                func encodeEnum<T: RawRepresentable>(_ value: T?, forKey key: String) {
                    guard let rawValue = value?.rawValue else {
                        return
                    }
                    if let s = rawValue as? String {
                        encode(s, forKey: key)
                    } else if let i = rawValue as? Int {
                        encode(i, forKey: key)
                    } else {
                        assert(false, "Unsupported type")
                    }
                }
                
                func decodeEnum<T: RawRepresentable>(forKey key: String, defaultValue: T) -> T {
                    if let s = decodeObject(forKey: key) as? String, s is T.RawValue {
                        return T(rawValue: s as! T.RawValue) ?? defaultValue
                    } else {
                        let i = decodeInteger(forKey: key)
                        if i is T.RawValue {
                            return T(rawValue: i as! T.RawValue) ?? defaultValue
                        }
                    }
                    return defaultValue
                }
                
            }
            

            比使用它

            // encode
            coder.encodeEnum(source, forKey: "source")
            // decode
            source = coder.decodeEnum(forKey: "source", defaultValue: Source.home)
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2013-04-09
              • 2016-10-10
              • 1970-01-01
              • 2018-05-14
              • 2018-11-17
              • 2017-11-18
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多