Swift 4 中默认情况下的可编码枚举

Posted

技术标签:

【中文标题】Swift 4 中默认情况下的可编码枚举【英文标题】:Codable enum with default case in Swift 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 我已经尝试过这个解决方案,但它不起作用。我在反序列化过程中出错。 你能再展示一些你的代码吗? 【参考方案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 大小写。但这是有代价的。您以某种方式需要您的案例的原始值。受this 和this 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)

【讨论】:

以上是关于Swift 4 中默认情况下的可编码枚举的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Swift 中为 SpriteKit 定义类别位掩码枚举?

在 swift 4 中使用可编码的 JSON 时出错?

swift 笔记 —— 方法(类,结构体,枚举)

Swift 4 可编码领域对象子类

Swift - 39 - 枚举类型关联默认值

swift_枚举 | 可为空类型 | 枚举关联值 | 枚举递归 | 树的概念