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

Swift Codable - How to Initialize an Optional Enum Property in a Failable Manner

我正在尝试为必须从 JSON 我的 Web 服务 return 实例化的对象采用 Codable 协议以响应 [=107] 之一=]来电。

其中一个属性是枚举类型,可选:nil表示none由enum定义的选项已被选中。

enum 常量基于 Int 并从 1 开始, 而非 0

class MyClass: Codable {

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

这是因为对应的JSON条目上的值0是为"not set"保留的;也就是说,在设置从中初始化 company 属性 时,它应该映射到 nil

Swift 的枚举初始化器 init?(rawValue:) 提供了这个开箱即用的功能:一个 Int 参数不匹配任何情况的原始值将导致初始化程序失败并且 return 为零。此外,基于 Int (和字符串)的枚举可以通过在类型定义中声明它来符合 Codable

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

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

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

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

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

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

作为解决方法,我按如下方式实现了 class:将 JSON 的整数 属性 映射到 private,我的 class 的 stored Int 属性 仅用作存储,并引入强类型 computed 属性 充当存储和我的应用程序其余部分之间的桥梁

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

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

  1. 是否不需要像我的解决方法一样需要重复的属性,并且
  2. 是否需要完全实现init(from:)and/orencode(with:),或许实现这些委托给默认行为的简化版本大部分(即不需要手动 initializing/encoding 每个 属性 的整个样板)?

附录: 还有第三个,同样不雅 的解决方案,当我第一次发布问题时并没有想到。它涉及使用人工基础 class 只是为了自动解码。我不会使用它,只是为了完整起见在这里描述一下:

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

    }
}

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

如果我没理解错的话,我想我遇到了和你类似的问题。在我的例子中,我为每个有问题的枚举写了一个包装器:

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

在搜索 DecoderDecodable 协议的文档以及具体的 JSONDecoder class 之后,我认为没有办法 来实现我一直在寻找的东西。最接近的是仅实施 init(from decoder: Decoder) 并手动执行所有必要的检查和转换。


其他想法

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

即使值 0 在 API 方面具有 "unspecified" 的特定含义,通过强制失败 init?(rawValue:) 我基本上将所有无效值混为一谈.如果由于服务器 returns(例如)-7 的某些内部错误或错误,我的代码将无法检测到它,并且会静静地将它映射到 nil,就好像它是指定的 0

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

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

    enum Company: Int {
       case unspecified = 0
       case toyota
       case ford
       case gm
    }
    

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

  2. 保留可选性,但 API return 一个 JSON 缺少键值 "company" (这样存储的 Swift 属性 保留其初始值 nil )而不是 returning 0 (我相信 JSON 确实有一个 "null" 值,但我不确定 JSONDecoder 如何处理它)

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

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

我认为现在会坚持我的解决方法,并可能在未来的某个时间采用选项 #1...

你可以试试SafeDecoder

import SafeDecoder

class MyClass: Codable {

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

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

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

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

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

我就是这样修复的,不需要实现任何初始化方法。这样,如果需要,您还可以提供默认值而不是 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 或与您期望的值不同,您 return nil (3) 或默认值 (2)。

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

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

从 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()
        }
    }
}

用法

struct Model: Codable {
    @NilOnFailCodable var val: Enum?
    enum Enum: Int, Codable {
        case holdUp = 0
        case holdDown = 1
    }
}

和例子

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

将打印

Model(_val: NilOnFailCodable<Model.Enum>(wrappedValue: nil))
nil
Model(_val: NilOnFailCodable<Model.Enum>(wrappedValue: nil))
nil

对于值“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)