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

Swift 您是否可以定义一个枚举来表示应用程序明确已知的值,但仍然可以处理从后端解码的未知值?,swift,enums,codable,Swift,Enums,Codable,您能否定义一个枚举来表示模型中属性的已知值,同时仍然允许从后端返回未知值 简短的回答: 作为我们应用程序的一部分,我们定义了一组功能标志,应用程序根据一组标准使用这些标志来启用/禁用某些功能。这些标志作为字符串数组从后端发送回 但是,在我们的应用程序中,我们希望将这些值定义为一个枚举(我们将其标记为可编码),而不是处理字符串常量的混乱性,这样编译器会自动为我们处理实际枚举情况的编码/解码 以下是此类场景的典型枚举 enum FeatureFlag : String, CaseIterable,

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

简短的回答:

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

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

以下是此类场景的典型枚举

enum FeatureFlag : String, CaseIterable, Codable {
    case allowsTrading
    case allowsFundScreener
    case allowsFundsTransfer
}
这种设计的缺点是它不处理将来可能定义并从后端返回的值

有几种方法可以处理这种情况:

放弃枚举并转到字符串常量。这很容易出错,并且会破坏包含/作用域,因为任何字符串都可以参与此逻辑。 按原样使用枚举,并在后端更新时强制更新应用程序,将责任推给部署。 更新后端以处理版本控制,只返回该版本应用程序已知的值,从而使后端上的逻辑复杂化,从而了解各种前端,而这些前端不应该这样做。 最常见的防御性未知数程序是为使用此枚举的每个类/结构编写自己的编码器/解码器方法,忽略当前案例列表中未知的任何标志。 从一到三都是维护的噩梦。是的,四个更好,但编写所有这些自定义序列化程序/反序列化程序可能非常耗时且容易出错,而且它无法充分利用编译器能够自动为您执行这些操作的优势

但是如果有一个数字5呢?如果您可以让枚举本身在运行时优雅地处理未知值,同时在过程中保持无损,而不必求助于选项,该怎么办


这就是确切的解决方案!享受吧

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

enum FeatureFlag : String, CaseIterable, Codable {
    case allowsTrading
    case allowsFundScreener
    case allowsFundsTransfer
}
很简单。但是,现在,使用类型FeatureFlag定义的任何值只能处理这些特定的已知类型之一

init?(rawValue: String) {
    if let item = Self.allCases.first(where: { $0.rawValue == rawValue }) {
        self = item
    } else {
        self = Self.other(rawValue)
        if #available(iOS 12.0, *) {
            os_log(.error, "Unknown FeatureFlag: %s", rawValue)
        } else {
            print("Error: Unknown FeatureFlag: \(rawValue)")
        }
    }
}
现在说,由于后端中的一个新功能,定义了一个新标志allowsSavings并将其下推到应用程序中。除非您手动编写解码逻辑或使用optionals,否则解码器将失败

但是如果你不用写呢?如果enum可以自动处理未知的情况呢

诀窍是定义一个额外的案例,另一个与String类型的关联值。这个新案例处理解码时交给它的所有未知类型

以下是我们更新的枚举:

enum FeatureFlag : Codable {
    case allowsTrading
    case allowsFundScreener
    case allowsFundsTransfer
    case other(String)
}
第一个问题是,因为other有一个关联的值,caseitrable不能再自动合成,所以我们必须自己手动实现它。让我们在这里这样做

extension FeatureFlag : CaseIterable {

    typealias AllCases = [FeatureFlag]

    static let allCases:AllCases = [
        .allowsTrading,
        .allowsFundScreener,
        .allowsFundsTransfer
    ]
}
你会注意到我在这里特别忽略了新的情况other,因为这个版本的代码不知道other中有什么值,因此从我们的角度来看,我们可以将其视为根本不存在

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

神奇的酱汁 诀窍是在实例化枚举时,首先根据rawValue在AllCase中搜索已知类型,如果找到,则使用它

但是,如果没有找到匹配项,请使用新的其他大小写,将未知值放入其中

同样,在通过rawValue getter返回的过程中,首先检查它是否是其他类型,如果是,则返回关联的值。否则,返回描述已知案例的字符串

以下是这两种方法的实现:

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)
        }
    }
} 
这是同一个初始值设定项,但使用poor man的未知值日志记录,有助于调试后端实际发送的内容

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
    }
}
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
也会根据原始值进行比较,因此由于以上所有因素,所有三个值都相等

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'
此外,由于原始可表示值是可散列的字符串,您还可以通过简单地指定其与该协议的一致性,使该枚举可散列,从而也可相等

extension FeatureFlag : Hashable {}
现在,您可以在集合中使用它,也可以将其用作字典中的键。再次使用上面的“a”、“b”和“c”,所有这些都相等,你可以这样使用它们

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
    }
}
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
有了上述内容,您现在可以将任何字符串编码或解码为thi s枚举类型,但仍然可以访问您关心的已知情况,而无需在模型类型中编写任何自定义解码逻辑。当你“知道”新类型时,只需添加新的案例,你就可以开始了

附带好处:无损编码/解码 这种方法的另一个好处是在另一种情况下保持未知值,因此如果需要重新编码模型,也可以通过编码器重新写入值

这意味着,例如,如果您的旧应用程序读入一个包含新的未知枚举案例的模型,然后必须再次对该值进行重新编码,则不会丢失任何数据,因为它会像已知案例一样持续存在,因此,尽管您自己可能会忽略它,但编码器/解码器不会

享受吧

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

init?(rawValue: String) {
    if let item = Self.allCases.first(where: { $0.rawValue == rawValue }) {
        self = item
    } else {
        self = Self.other(rawValue)
        if #available(iOS 12.0, *) {
            os_log(.error, "Unknown FeatureFlag: %s", rawValue)
        } else {
            print("Error: Unknown FeatureFlag: \(rawValue)")
        }
    }
}

我喜欢!小事情:在示例中将UserFeedback改为UserReaction修复了打字错误。谢谢实际上,将其更新为FeatureFlag,因为我认为更多的人会理解“事后定义的新值”的概念,而不是用户反应我喜欢!但它应该是“添加日志记录”,因为我在阅读“添加错误处理”时认为,关键是不要抛出错误!哈哈,好主意!哈符合事实的变了,哈哈。。。我看到你把它改回了“错误”。一切都好!顺便说一句,我刚刚更新了你的代码以引用FeatureFlag,因为我更新了这个示例,因为我认为会有更多的人跟随它,而不是UserReaction。奇怪的是,我没有把它改回去,它现在又在记录了。FeatureFlag听起来不错: