您是否可以定义一个枚举来表示应用程序明确知道的值,但仍可以处理来自后端/解码的未知值?

问题描述 投票:2回答:2

您是否可以定义一个枚举来表示模型中属性的已知值,同时仍然允许从后端返回未知值?

简短回答:是!

作为我们应用程序的一部分,我们定义了一组功能标志,该应用程序用于根据一组条件来启用/禁用某些功能。这些标志作为字符串数组从后端发送回。

但是,在我们的应用程序中,我们希望将这些值定义为枚举,并将其标记为Codable,而不是处理字符串常量的混乱情况,因此编译器会自动为我们处理实际枚举情况的编码/解码。

这是此类情况的典型枚举...

enum FeatureFlag : String, CaseIterable, Codable {
    case allowsTrading
    case allowsFundScreener
    case allowsFundsTransfer
}

这种设计的缺点是它无法处理可能定义的值,将来无法从后端返回。

有几种方法可以解决这种情况:

  1. 放弃枚举,转到字符串常量。这容易出错,并且由于任何字符串都可以参与此逻辑,因此会破坏包含/作用域。
  2. 按原样保留枚举,并在后端更新后将应用程序传递给部署,强制对应用程序进行更新。
  3. 更新后端以处理版本控制,以仅返回该应用程序版本已知的值,从而使后端的逻辑复杂化,以了解各种前端,而不应这样做。
  4. 通过为使用此枚举的每个类/结构编写自己的编码器/解码器方法,忽略未知的当前情况列表中的任何标志,来编写未知的最常见的防御性程序。

1-3本身就是维护的噩梦。是的,四个更好,但是编写所有这些自定义的序列化器/反序列化器可能会非常耗时且容易出错,而且它不利于利用编译器能够自动为您完成的好处!

但是如果有五个数字呢?如果您可以让枚举itself在运行时优雅地处理未知值,而又在过程中保持无损且不必求助于可选方法,该怎么办?

这就是我在下面介绍的确切解决方案!享受!

swift enums codable
2个回答
3
投票

如上所述,我们的应用程序具有一组已知的功能标志。首先,可以这样定义它们。

enum FeatureFlag : String, CaseIterable, Codable {
    case allowsTrading
    case allowsFundScreener
    case allowsFundsTransfer
}

很简单。但是同样,现在,任何用类型FeatureFlag定义的值都只能处理那些特定的已知类型之一。

[现在,感谢后端的新功能,定义了一个新标志allowsSavings,并将其下推到您的应用程序。除非您手动编写了解码逻辑(或采用了可选参数),否则解码器将失败。

但是如果您不必写它们怎么办?如果枚举可以自动处理未知案件怎么办?

诀窍是定义另外一种情况,other,其关联值为String类型。这种新的情况处理解码时传递给它的所有未知类型。

这是我们更新的枚举:

enum FeatureFlag : Codable {
    case allowsTrading
    case allowsFundScreener
    case allowsFundsTransfer
    case other(String)
}

第一个问题是,由于other具有关联的值,CaseIterable无法再自动合成,因此我们必须自己手动实现。让我们在这里做...

extension FeatureFlag : CaseIterable {

    typealias AllCases = [FeatureFlag]

    static let allCases:AllCases = [
        .allowsTrading,
        .allowsFundScreener,
        .allowsFundsTransfer
    ]
}

[您会注意到我在这里特别忽略了新情况other,因为此版本的代码不知道other中拥有什么值,因此,从我们的角度来看,我们可以将其视为不存在。

出于与前面相同的原因-other案例具有关联值-我们还必须手动实现RawRepresentable,但这实际上是发生魔术的地方!

魔术酱

诀窍是实例化枚举时,您首先在allCases(基于rawValue)中搜索一个已知类型,如果找到,则使用它。

但是如果找不到匹配项,请使用新的other案例,将未知值放在其中。

同样,在通过rawValue吸气剂返回的途中,首先检查它是否为other类型,如果是,则返回相关值。否则,返回描述已知情况的字符串。

这是两者的实现:

extension FeatureFlag : RawRepresentable {

    init?(rawValue: String) {

        self = FeatureFlag.allCases.first{ $0.rawValue == rawValue }
               ??
               .other(rawValue)
    }

    var rawValue: String {

        switch self {
            case let .other(value) : return value
            default                : return String(describing:self)
        }
    }
} 

这里是相同的初始化程序,但是(穷人的)记录未知值,有助于调试后端实际发送的内容...

init?(rawValue: String) {

    guard let knownCase = FeatureFlag.allCases.first(where: { $0.rawValue == rawValue }) else {

        print("Unrecognized \(FeatureFlag.self): \(rawValue)")
        self = .other(rawValue)
        return
    }

    self = knownCase
}

注意:这里我只是将案例本身用作原始值。如果您的枚举值需要与服务器上的其他值匹配,那么您当然可以手动扩展其他情况,例如,...

var rawValue: String {

    switch self {
        case .allowsTrading       : return "ALLOWS_TRADING"
        case .allowsFundScreener  : return "ALLOWS_FUND_SCREENER"
        case .allowsFundsTransfer : return "ALLOWS_FUNDS_TRANSFER"
        case let .other(value)    : return value
    }
}

也基于原始值进行比较,因此,由于上述所有原因,所有这三个值均相等...

let a = FeatureFlag.allowsTrading
let b = FeatureFlag(rawValue: "allowsTrading")!
let c = FeatureFlag.other("allowsTrading")

let x = a == b // x is 'true'
let y = a == c // y is 'true'
let z = b == c // z is 'true'

此外,由于可表示原始值是可哈希的字符串,因此,您还可以通过简单地指定其与该协议的一致性来使此枚举Hashable(因此也可以是Equatable)。

extension FeatureFlag : Hashable {}

现在您可以将其用于集合中,也可以用作字典中的键。从上方使用“ a”,“ b”和“ c”-再次相等,您可以像这样使用它们...

var items = [FeatureFlag:Int]()

items[a] = 42
print(items[a] ?? -1) // prints 42
print(items[b] ?? -1) // prints 42
print(items[c] ?? -1) // prints 42

使用上面的代码,您现在可以将any

字符串编码或解码为该枚举类型,但是仍然可以访问您关心的已知情况,而无需在模型类型中编写任何自定义解码逻辑。当您“了解”新类型时,只需添加新案例,您就可以开始使用了!

附带好处:无损编码/解码

此方法的另一个附带好处是,在other情况下,这将保留未知值,因此,如果您需要重新编码模型,则也可以通过编码器重写这些值。

[例如,如果您的旧应用读取的模型包含一个新的未知枚举大小写,然后必须重新编码该值,则不会丢失任何数据,因为它像已知大小写一样持续存在,因此尽管您自己可能会忽略它,而编码器/解码器则不会。

享受!


1
投票

喜欢这个建议的solution!一个小建议,添加一些日志记录,以防系统遇到未知类型。

© www.soinside.com 2019 - 2024. All rights reserved.