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

72

我定义了一个枚举如下:

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
@AndréSlotta 我已经尝试过这个解决方案,但它不起作用。我在反序列化过程中遇到了一个错误。 - LucaRoverelli
你能展示更多的代码吗? - André Slotta
10个回答

192

您可以扩展您的 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]

8
这应该是被接受的答案!完美运作。 - heyfrank
如果您经常使用此功能,则可以稍微通用化一些。请将“try Type”替换为“try type(of:self).init”。 - Daniel
@Daniel,有没有办法创建一个完全通用的CodableWithUnknown协议或类似的东西? - LenK
4
希望能够给我的评论点踩的人,请解释一下原因,这样我就能改正和/或改进我的回答。毫无理由地点踩没有任何意义。 - Leo Dabus
2
这真的很干净简单! - atereshkov

16
您可以放弃您的Type的原始类型,并创建一个处理关联值的未知情况。但这样做会有代价。您需要一些用于您的情况的原始值。受栈溢出答案的启发,我想出了这个优雅的解决方案来解决您的问题。
为了能够存储原始值,我们将维护另一个枚举(enum),但作为私有项:
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
    }
}

将编码和解码部分移动到扩展中:

可解码部分:

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响应进行测试。

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

添加一个解释。 - lofihelsinki

7
这里是一种基于 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"}]
 */

4

让我们从一个测试用例开始。我们期望它能通过:

    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! 实现,因为我认为它们有点脆弱,因为编译器没有类型检查。

这样更好也更常见。而且 Self.RawValue == String 可以改为 Self.RawValue: Decodable - Leo

3

您需要实现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

@LeoDabus 谢谢你的回答。我稍微修改了一下,制定了一个似乎对我有用的字符串枚举协议:

Original Answer翻译成"最初的回答"

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

3
我不会强制解包或者在那里使用do catch。如果你想将枚举类型限制为字符串,你可以这样做:protocol CaseIterableDefaultsLast: Codable & 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! } } - Leo Dabus
1
@LeoDabus 是的,那更简单。谢谢! - LenK

1
以下方法将解码所有类型的RawValue为Decodable类型(Int、String等)的枚举类型,并在失败时返回nil。这将防止由JSON响应中不存在的原始值引起的崩溃。

定义:

extension Decodable {
    static func decode<T: RawRepresentable, R, K: CodingKey>(rawValue _: R.Type, forKey key: K, decoder: Decoder) throws -> T? where T.RawValue == R, R: Decodable {
        let container = try decoder.container(keyedBy: K.self)
        guard let rawValue = try container.decodeIfPresent(R.self, forKey: key) else { return nil }
        return T(rawValue: rawValue)
    }
}

使用方法:

enum Status: Int, Decodable {
        case active = 1
        case disabled = 2
    }
    
    struct Model: Decodable {
        let id: String
        let status: Status?
        
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            id = try container.decodeIfPresent(String.self, forKey: .id)
            status = try .decode(rawValue: Int.self, forKey: .status, decoder: decoder)
        }
    }

// status: -1 reutrns nil
// status:  2 returns .disabled 

1
添加这个扩展并设置YourEnumName
extension <#YourEnumName#>: Codable {
    public init(from decoder: Decoder) throws {
        self = try <#YourEnumName#>(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
    }
}

0

您可以使用此扩展来编码/解码(此代码片段支持 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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接