Swift Codable - 如何以可失败的方式初始化可选的枚举属性

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)

我的问题是:有没有更好(更简单/更优雅)的方式,即:

  1. 难道不是需要复制的属性,如我的解决办法,并
  2. 难道不是要求全面落实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)


Max*_*mia 5

如果我理解正确的话,我想我也遇到了与你类似的问题。就我而言,我为每个有问题的枚举编写了一个包装器:

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


Nic*_*ari 2

Decoder在搜索了协议Decodable和具体类 的文档后JSONDecoder我相信没有办法完全实现我正在寻找的东西。最接近的是手动实施init(from decoder: Decoder)和执行所有必要的检查和转换。


额外的想法

在思考了这个问题之后,我发现了当前设计的一些问题:对于初学者来说,将0JSON 响应中的值映射到nil似乎不正确。

尽管该值0在 API 方面具有“未指定”的特定含义,但通过强制失败,init?(rawValue:)我实际上将所有无效值合并在一起。如果由于某些内部错误或错误,服务器返回(比如说)-7,我的代码将无法检测到它,并且会默默地将其映射到nil,就像它是指定的0.

因此,我认为正确的设计是:

  1. 放弃属性的可选性company,并将其定义enum为:

    enum Company: Int {
       case unspecified = 0
       case toyota
       case ford
       case gm
    }
    
    Run Code Online (Sandbox Code Playgroud)

    ...与 JSON 紧密匹配,或者,

  2. 保持可选性,但让 API 返回一个缺少键“company”值的JSON (以便存储的 Swift 属性保留其初始值nil)而不是返回0(我相信 JSON 确实有一个“null”值,但我不知道如何JSONDecoder处理)

第一个选项需要修改整个应用程序的大量代码(更改 的出现次数if let...以与 进行比较.unspecified)。

第二个选项需要修改服务器 API,这超出了我的控制范围(并且会在服务器和客户端版本之间引入迁移/向后兼容性问题)。

我认为现在会坚持我的解决方法,也许在将来的某个时候采用选项#1......