毋庸置疑,Swift对枚举的实现是该语言所能提供的最受欢迎的强大功能之一。事实上,Swift枚举远远超出了基于整数的常量的简单枚举,并且支持关联值和复杂的模式匹配等,这使得它们成为解决许多不同类型问题的最佳候选者。
然而,某些类型的枚举案例可以说是很好地避免了,因为它们可能会导致我们出现一些棘手的情况,或者使我们的代码感觉不那么 "习惯"。让我们来看看一些这样的情况,以及如何使用Swift的其他语言特性来重构它们。
表示缺少一个值的情况
举个例子,假设我们正在开发一个播客应用,我们用一个枚举来实现我们应用所支持的各种类别。该枚举目前包含每个类别的情况,以及两个有点特殊的情况,用于根本没有类别的播客(none ),以及一个可以用来一次引用所有类别的类别(all )。
extension Podcast {
enum Category: String, Codable {
case none
case all
case entertainment
case technology
case news
...
}
}
然后,在实现过滤等功能时,我们可以使用上述枚举来对用户在用户界面内选择的Category 值进行模式匹配(该值被封装在Filter 模型内)。
extension Podcast {
func matches(filter: Filter) -> Bool {
switch filter.category {
case .all, category:
return name.contains(filter.string)
default:
return false
}
}
}
乍一看,上面的两段代码可能看起来非常好。但是,如果我们仔细想想,我们目前为表示没有类别而增加了一个特定的none 案例,这可以说是有点奇怪,因为Swift确实有一个内置的语言功能,是为这个目的而量身定做的--optionals。
所以,如果我们把我们的Podcast 模型的category 属性变成一个可选项,那么我们就可以完全免费地获得对缺失类别的支持--另外,我们现在可以利用Swift可选项支持的所有功能(比如if let 语句)来处理这些缺失的值。
struct Podcast {
var name: String
var category: Category?
...
}
上述变化非常有趣的一点是,我们之前在Podcast.Category 值上使用的任何详尽的switch 语句仍然可以像以前一样工作--因为事实证明,Optional 类型本身也是一个枚举,使用none 案例来表示缺少的值--这意味着像下面这个函数的代码可以完全保持不变(除了将其参数修改为一个可选项)。
func title(forCategory category: Podcast.Category?) -> String {
switch category {
case .none:
return "Uncategorized"
case .all:
return "All"
case .entertainment:
return "Entertainment"
case .technology:
return "Technology"
case .news:
return "News"
...
}
}
以上的工作要归功于Swift编译器的一点魔力,当选项被用于模式匹配的上下文时(比如switch 语句),它可以让我们既解决Optional 类型本身的情况,又解决我们自己的Podcast.Category 枚举中定义的情况,都在同一个语句中。
如果我们想的话,我们也可以用case nil ,而不是case .none ,因为在上述类型的情况下,这些功能是相同的。
特定领域的枚举
接下来,让我们把注意力转向我们的Podcast.Category 枚举的all 情况,如果我们仔细想想,这也有点奇怪。毕竟,一个播客不可能同时属于所有的类别,所以那个all 的情况确实只在过滤的范围内有意义。
因此,与其在我们的主Category 枚举中包括这种情况,不如创建一个专门的类型,专门用于过滤领域。这样一来,我们就可以实现一个相当巧妙的关注点分离,而且由于我们使用的是嵌套类型,我们可以让我们的新枚举使用相同的Category ,只是这次它将被嵌套在我们的Filter 模型中--像这样:
extension Filter {
enum Category {
case any
case uncategorized
case specific(Podcast.Category)
}
}
值得注意的是,我们也可以选择在这里使用可选的方法,用nil 代表any 或uncategorized ,但由于在这种情况下有两个潜在的候选者,我们可以说通过在这里使用专用的案例使我们的意图更加明确。
上述方法真正好的地方在于,我们现在可以使用Swift的模式匹配能力来实现我们的整个过滤逻辑--通过切换到过滤的类别,然后使用where 子句来给每个案例附加额外的逻辑。
extension Podcast {
func matches(filter: Filter) -> Bool {
switch filter.category {
case .any where category != nil,
.uncategorized where category == nil,
.specific(category):
return name.contains(filter.string)
default:
return false
}
}
}
有了上述所有的改变,我们现在可以继续从我们的主Podcast.Category 枚举中移除none 和all 案例--留给我们的是一个更直接的我们应用支持的每个类别的列表:
extension Podcast {
enum Category: String, Codable {
case entertainment
case technology
case news
...
}
}
自定义案例和自定义类型
当涉及到像Podcast.Category 这样的枚举时,(在某些时候)引入某种custom 案例是非常普遍的,它可以用来处理一次性的案例,或者通过优雅地处理未来可能在服务器端添加的案例来提供向前的兼容性。
实现的方法之一是使用一个有关联值的案例--在我们的案例中,一个String ,代表一个自定义类别的原始值,就像这样:
extension Podcast {
enum Category: Codable {
case all
case entertainment
case technology
case news
...
case custom(String)
}
}
不幸的是,虽然关联值在其他情况下非常有用,但这并不是其中之一。首先,通过添加这种情况,我们的枚举不能再被String ,这意味着我们现在必须编写自定义的编码和解码代码,以及将实例转换为原始字符串的逻辑。
因此,让我们探索另一种方法,将我们的Category 枚举转换为RawRepresentable 结构,这让我们再次利用Swift的内置逻辑来编码、解码和处理此类类型的字符串转换。
extension Podcast {
struct Category: RawRepresentable, Codable, Hashable {
var rawValue: String
}
}
由于我们现在可以自由地从我们想要的任何自定义字符串中创建Category 实例,我们可以轻松地支持自定义和未来的类别,而不需要我们编写任何额外的代码。然而,为了确保我们的代码保持向后兼容,并使我们很容易引用任何内置的、目前已知的类别--让我们也用静态API来扩展我们的新类型,这将实现所有这些事情。
extension Podcast.Category {
static var entertainment: Self {
Self(rawValue: "entertainment")
}
static var technology: Self {
Self(rawValue: "technology")
}
static var news: Self {
Self(rawValue: "news")
}
...
static func custom(_ id: String) -> Self {
Self(rawValue: id)
}
}
尽管上述变化确实需要增加一些额外的代码,但我们现在已经有了一个更灵活的设置,几乎完全向后兼容。事实上,我们需要做的唯一更新是对Category 值进行详尽的切换的代码。
例如,我们之前看了一下title 函数,它曾对这样一个值进行切换,以返回一个与给定类别相匹配的标题。由于我们不能再在编译时获得每个Category 值的详尽列表,我们现在必须使用不同的方法来计算这些标题。在这种特殊情况下,例如,我们可以把这看作是一个极好的机会,把这些字符串移到Localizable.strings 文件中,然后像这样解决我们的标题。
func title(forCategory category: Podcast.Category?) -> String {
guard let id = category?.rawValue else {
return NSLocalizedString("category-uncategorized", comment: "")
}
let key = "category-\(id)"
let string = NSLocalizedString(key, comment: "")
// Handling unknown cases by returning a capitalized version
// of their key as a fallback title:
guard string != key else {
return key.capitalized
}
return string
}
另一个选择是在Category 类型本身中解决我们的本地化标题,也许还可以添加一个可选的title 属性,这将使我们的服务器能够为我们的应用程序还不支持的自定义类别发送预本地化的标题。
自动命名的静态属性
作为一个快速的额外提示,上述基于结构的方法的一个缺点是,我们现在必须手动定义每个静态属性的底层字符串原始值,但这一点我们可以用Swift的#function 关键字来解决。由于该关键字将自动被其封装函数所调用的函数(或者,在我们的例子中,属性)的名称所取代,这将为我们提供与使用枚举时相同的自动原始值映射。
extension Podcast.Category {
static func autoNamed(_ rawValue: StaticString = #function) -> Self {
Self(rawValue: "\(rawValue)")
}
}
有了上述工具,我们现在可以简单地在我们每个内置的类别API中调用autoNamed() ,Swift会自动为我们填入这些原始值。
extension Podcast.Category {
static var entertainment: Self { autoNamed() }
static var technology: Self { autoNamed() }
static var news: Self { autoNamed() }
...
static func custom(_ id: String) -> Self {
Self(rawValue: id)
}
}
但值得注意的是,在使用基于#function 的技术时,我们必须小心一点,不要重命名任何上述静态属性,因为这样做也会改变该属性的基础原始值Category 。然而,在使用枚举时也是如此,从另一个角度看,我们现在也防止了手动定义每个原始字符串时可能发生的打字错误和其他错误。
总结
Swift 枚举是非常棒的(事实上,光是关于这个话题我就写了超过 15 篇文章),但是在某些情况下,另一种语言机制可能是我们想要构建的东西的更好选择,而且随着我们项目的成长和发展,我们总是有可能需要在几种不同的机制和方法之间切换。
谢谢你的阅读!