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 & 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 中默认情况下的可编码枚举的主要内容,如果未能解决你的问题,请参考以下文章