Nic*_*ari 6 enums optional swift codable
我正在尝试Codable
为一个对象采用协议,该对象必须从我的 Web 服务返回的 JSON 实例化以响应 API 调用之一。
其中一个属性是枚举类型,可选:nil
表示没有选择 定义的任何选项enum
。
该enum
常数Int
为基础的,并在开始1
,不是 0
:
class MyClass: Codable {
enum Company: Int {
case toyota = 1
case ford
case gm
}
var company: Company?
Run Code Online (Sandbox Code Playgroud)
这是因为0
相应 JSON 条目上的值是为“未设置”保留的;即nil
在设置初始化company
属性时应该映射到它。
Swift 的 enum 初始值设定项init?(rawValue:)
提供了开箱即用的此功能:Int
与任何情况下的原始值都不匹配的参数将导致初始值设定项失败并返回 nil。此外,Int
可以Codable
通过在类型定义中声明它来使基于 (和 String) 的枚举符合:
enum Company: Int, Codable {
case toyota = 1
case ford
case gm
}
Run Code Online (Sandbox Code Playgroud)
问题是,我的自定义类有 20 多个属性,所以我真的很想避免实现init(from:)
and encode(to:)
,而是依赖于通过提供CondingKeys
自定义枚举类型获得的自动行为。
这导致整个类实例的初始化失败,因为“综合”初始化程序似乎无法推断应将不支持的枚举类型的原始值视为nil
(即使目标属性明确标记为optional,即Company?
)。
我认为这是因为提供的初始化程序Decodable
可以抛出,但它不能返回 nil:
// This is what we have:
init(from decoder: Decoder) throws
// This is what I would want:
init?(from decoder: Decoder)
Run Code Online (Sandbox Code Playgroud)
作为一种变通方法,我已经实现了我的课如下:JSON的整数属性映射到一个私人,存储 Int
我的课的,只有作为存储性能,并引进一个强类型的计算属性,充当桥梁的存储和之间我的应用程序的其余部分:
class MyClass {
// (enum definition skipped, see above)
private var companyRawValue: Int = 0
public var company: Company? {
set {
self.companyRawValue = newValue?.rawValue ?? 0
// (sets to 0 if passed nil)
}
get {
return Company(rawValue: companyRawValue)
// (returns nil if raw value is 0)
}
}
enum CodingKeys: String, CodingKey {
case companyRawValue = "company"
}
// etc...
Run Code Online (Sandbox Code Playgroud)
我的问题是:有没有更好(更简单/更优雅)的方式,即:
init(from:)
和/或encode(with:)
,也许这些实施该委托的简化版本的默认行为在大多数情况下(即不需要手动初始化/编码每财产整个样板)? 附录:当我第一次发布问题时,我没有想到第三个同样不雅的解决方案。它涉及为了自动解码而使用人工基类。我不会使用它,但为了完整起见,仅在此处描述它:
// Groups all straight-forward decodable properties
//
class BaseClass: Codable {
/*
(Properties go here)
*/
enum CodingKeys: String, CodingKey {
/*
(Coding keys for above properties go here)
*/
}
// (init(from decoder: Decoder) and encode(to:) are
// automatically provided by Swift)
}
// Actually used by the app
//
class MyClass: BaseClass {
enum CodingKeys: String, CodingKey {
case company
}
var company: Company? = nil
override init(from decoder: Decoder) throws {
super.init(from: decoder)
let values = try decoder.container(keyedBy: CodingKeys.self)
if let company = try? values.decode(Company.self, forKey: .company) {
self.company = company
}
}
}
Run Code Online (Sandbox Code Playgroud)
...但这是一个非常丑陋的黑客。类继承层次结构不应该由这种类型的缺点决定。
小智 7
从 swift 5 开始,您可以使用属性包装器。https://docs.swift.org/swift-book/LanguageGuide/Properties.html
在你的情况下,主要结构将类似于:
@propertyWrapper
public struct NilOnFailCodable<ValueType>: Codable where ValueType: Codable {
public var wrappedValue: ValueType?
public init(wrappedValue: ValueType?) {
self.wrappedValue = wrappedValue
}
public init(from decoder: Decoder) throws {
self.wrappedValue = try? ValueType(from: decoder)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if let value = wrappedValue {
try container.encode(value)
} else {
try container.encodeNil()
}
}
}
Run Code Online (Sandbox Code Playgroud)
用法
struct Model: Codable {
@NilOnFailCodable var val: Enum?
enum Enum: Int, Codable {
case holdUp = 0
case holdDown = 1
}
}
Run Code Online (Sandbox Code Playgroud)
和例子
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let s = #"{"val": 2}"#
let data = s.data(using: .utf8)
let dec = decoder.decode(Model.self, from: data!)
print(dec)
let enc = encoder.encode(dec)
print(decoder.decode(Model.self, from: enc))
Run Code Online (Sandbox Code Playgroud)
会打印
Model(_val: NilOnFailCodable<Model.Enum>(wrappedValue: nil))
nil
Model(_val: NilOnFailCodable<Model.Enum>(wrappedValue: nil))
nil
Run Code Online (Sandbox Code Playgroud)
对于值“val”:1
Model(_val: NilOnFailCodable<Model.Enum>(wrappedValue: Optional(Model.Enum.holdDown)))
Optional(1)
Model(_val: NilOnFailCodable<Model.Enum>(wrappedValue: Optional(Model.Enum.holdDown)))
Optional(1)
Run Code Online (Sandbox Code Playgroud)
如果密钥“val”根本不存在,则解码将失败。添加以下代码来修复此错误:
public extension KeyedDecodingContainer {
func decode<T: Codable>(_ type: NilOnFailCodable<T>.Type, forKey key: Self.Key) throws -> NilOnFailCodable<T> {
return try decodeIfPresent(type, forKey: key) ?? NilOnFailCodable(wrappedValue: nil)
}
}
Run Code Online (Sandbox Code Playgroud)
如果我理解正确的话,我想我也遇到了与你类似的问题。就我而言,我为每个有问题的枚举编写了一个包装器:
struct NilOnFail<T>: Decodable where T: Decodable {
let value: T?
init(from decoder: Decoder) throws {
self.value = try? T(from: decoder) // Fail silently
}
// TODO: implement Encodable
}
Run Code Online (Sandbox Code Playgroud)
然后像这样使用它:
class MyClass: Codable {
enum Company: Int {
case toyota = 1
case ford
case gm
}
var company: NilOnFail<Company>
...
Run Code Online (Sandbox Code Playgroud)
company
当然,需要注意的是,无论您何时需要访问您需要使用的值myClassInstance.company.value
Decoder
在搜索了协议Decodable
和具体类 的文档后JSONDecoder
,我相信没有办法完全实现我正在寻找的东西。最接近的是手动实施init(from decoder: Decoder)
和执行所有必要的检查和转换。
在思考了这个问题之后,我发现了当前设计的一些问题:对于初学者来说,将0
JSON 响应中的值映射到nil
似乎不正确。
尽管该值0
在 API 方面具有“未指定”的特定含义,但通过强制失败,init?(rawValue:)
我实际上将所有无效值合并在一起。如果由于某些内部错误或错误,服务器返回(比如说)-7
,我的代码将无法检测到它,并且会默默地将其映射到nil
,就像它是指定的0
.
因此,我认为正确的设计是:
放弃属性的可选性company
,并将其定义enum
为:
enum Company: Int {
case unspecified = 0
case toyota
case ford
case gm
}
Run Code Online (Sandbox Code Playgroud)
...与 JSON 紧密匹配,或者,
保持可选性,但让 API 返回一个缺少键“company”值的JSON (以便存储的 Swift 属性保留其初始值nil
)而不是返回0
(我相信 JSON 确实有一个“null”值,但我不知道如何JSONDecoder
处理)
第一个选项需要修改整个应用程序的大量代码(更改 的出现次数if let...
以与 进行比较.unspecified
)。
第二个选项需要修改服务器 API,这超出了我的控制范围(并且会在服务器和客户端版本之间引入迁移/向后兼容性问题)。
我认为现在会坚持我的解决方法,也许在将来的某个时候采用选项#1......