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听起来不错: