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

时间:2018-11-06 03:14:19

标签: swift enums optional codable

我正试图为必须通过JSON实例化的对象采用Codable协议,我的Web服务会响应一个API调用返回该对象。

其中一个属性为枚举类型,并且是可选的:nil表示未选择enum定义的所有选项。

enum常量基于Int,并且从1开始,不是 0

class MyClass: Codable {

    enum Company: Int {
        case toyota = 1
        case ford
        case gm
    } 
    var company: Company?

这是因为对应的JSON条目上的值0保留用于“未设置”;也就是说,当设置从中初始化nil属性时,应将其映射到company

Swift的枚举初始值设定项init?(rawValue:)开箱即用地提供了此功能:与任何情况下的原始值都不匹配的Int参数将导致初始值设定项失败并返回nil。另外,只需在类型定义中声明基于Int(和字符串)的枚举即可使其符合Codable

enum Company: Int, Codable {
    case toyota = 1
    case ford
    case gm
} 

问题是,我的自定义类具有20多个属性,所以我真的很想避免实现init(from:)encode(to:),而要依靠自动通过提供CondingKeys自定义枚举类型获得的行为。

这导致整个类实例的初始化失败,因为似乎“合成的”初始化程序无法推断出枚举类型的不受支持的原始值应视为nil (即使尽管目标属性明确标记为可选,即Company?)。

我认为是这样,因为Decodable提供的初始化程序可以抛出,但是不能返回nil:

// This is what we have:
init(from decoder: Decoder) throws

// This is what I would want:
init?(from decoder: Decoder)

作为一种解决方法,我实现了我的类,如下所示:将JSON的integer属性映射到我的服务的类的 private stored Int属性中(仅作为存储),并引入一个强类型的 compute 属性,该属性在存储和应用程序的其余部分之间充当桥梁:

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

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

  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
        }

    }
}

...但这是一个非常丑陋的骇客。类继承层次结构不应由此类缺点决定。

5 个答案:

答案 0 :(得分:2)

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

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
}

然后像这样使用它:

class MyClass: Codable {

    enum Company: Int {
        case toyota = 1
        case ford
        case gm
    } 

    var company: NilOnFail<Company>
...

警告是,当然,无论何时需要访问company的值,都需要使用myClassInstance.company.value

答案 1 :(得分:1)

我知道我的回答晚了,但也许它会帮助其他人。

我也有 String Optional 枚举,但是如果我从后端获得一个本地枚举中未涵盖的新值,则 json 将不会被解析 - 即使枚举是可选的。

我就是这样修复的,不需要实现任何 init 方法。通过这种方式,您还可以在必要时提供默认值而不是 nil。

struct DetailView: Codable {

var title: ExtraInfo?
var message: ExtraInfo?
var action: ExtraInfo?
var imageUrl: String?

// 1
private var imagePositionRaw: String?
private var alignmentRaw: String?

// 2
var imagePosition: ImagePosition {
    ImagePosition.init(optionalRawValue: imagePositionRaw) ?? .top
}

// 3
var alignment: AlignmentType? {
    AlignmentType.init(optionalRawValue: alignmentRaw)
}

enum CodingKeys: String, CodingKey {
    case imagePositionRaw = "imagePosition",
         alignmentRaw = "alignment",
         imageUrl,
         title,
         message,
         action
}

}

(1) 您从后端获取原始值(字符串、整数 - 无论您需要什么),然后从这些原始值 (2,3) 中初始化您的枚举。

如果后端的值为 nil 或与您期望的值不同,则返回 nil (3) 或默认值 (2)。

--- 编辑以添加用于枚举初始化的扩展名:

extension RawRepresentable {
  init?(optionalRawValue: RawValue?) {
    guard let rawData = optionalRawValue else { return nil }
    self.init(rawValue: rawData)
  }
}

答案 2 :(得分:0)

在搜索了DecoderDecodable协议的文档以及具体的JSONDecoder类之后,我相信没有办法完全达到我的原意寻找。最接近的是仅实现init(from decoder: Decoder)并手动执行所有必要的检查和转换。


其他想法

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

即使值0在API方面具有“未指定”的特定含义,但通过强制出现故障的init?(rawValue:),我实际上是将所有无效值合并在一起。如果服务器因某些内部错误或错误而返回(例如)-7,则我的代码将无法检测到该错误并将其静默映射到nil,就像指定为{{ 1}}。

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

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

    enum

    ...紧密匹配JSON,或者

  2. 保留可选性,但让API返回一个JSON,该JSON 缺少键“ company”的值(以便存储的Swift属性保留其初始值enum Company: Int { case unspecified = 0 case toyota case ford case gm } ),而不是返回nil(我相信JSON确实具有“空”值,但我不确定0的处理方式)

第一个选项需要修改整个应用程序中的许多代码(将JSONDecoder的出现更改为与if let...的比较)。

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

我认为暂时仍然可以使用我的解决方法,并且将来可能会采用选项#1 ...

答案 3 :(得分:0)

您可以尝试SafeDecoder

import SafeDecoder

class MyClass: Codable {

  enum Company: Int {
    case toyota = 1
    case ford
    case gm
  }
  var company: Company?
}

然后将其解码为异常。 1,2,3以外的任何值都将自动回退为nil。

答案 4 :(得分:0)

感谢您的详细问答。你让我重新思考我解码 JSON 的方法。有类似的问题,并决定将 JSON 值解码为 Int,而不是向应该是 DTO 的内容添加逻辑。之后添加模型扩展以将值转换为枚举从使用枚举的角度来看没有区别,但看起来是一个更清晰的解决方案。