您是否可以定义一个枚举来表示模型中属性的已知值,同时仍然允许从后端返回未知值?
简短回答:Yes you can!
作为我们应用程序的一部分,我们定义了一组功能标志,该应用程序用于根据一组条件来启用/禁用某些功能。这些标志作为字符串数组从后端发送回。
但是,在我们的应用程序中,我们希望将这些值定义为枚举,并将其标记为Codable
,而不是处理字符串常量的混乱情况,因此编译器会自动为我们处理实际枚举情况的编码/解码。
这是此类情况的典型枚举...
enum FeatureFlag : String, CaseIterable, Codable {
case allowsTrading
case allowsFundScreener
case allowsFundsTransfer
}
这种设计的缺点是它无法处理可能定义的值,将来无法从后端返回。
有几种方法可以解决这种情况:
1-3本身就是维护的噩梦。是的,四个更好,但是编写所有这些自定义的序列化器/反序列化器可能会非常耗时且容易出错,而且它不利于利用编译器能够自动为您完成的好处!
但是如果有五个数字呢?如果您可以让枚举itself在运行时优雅地处理未知值,而又在过程中保持无损且不必求助于可选方法,该怎么办?
这就是确切的解决方案I present below!享受!
如上所述,我们的应用程序具有一组已知的功能标志。首先,可以这样定义它们。
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
情况下,这将保留未知值,因此,如果您需要重新编码模型,则也可以通过编码器重写这些值。
[例如,如果您的旧应用读取的模型包含一个新的未知枚举大小写,然后必须重新编码该值,则不会丢失任何数据,因为它像已知大小写一样持续存在,因此尽管您自己可能会忽略它,而编码器/解码器则不会。
享受!
喜欢这个建议的solution!一个小建议,添加一些日志记录,以防系统遇到未知类型。