代码地址
在 Objective-C 和其他类 C 语言中,枚举的声明方式是有一些缺陷的,作为类型来说并不够严密。因为所有的枚举类型实际上都是整数,所以有些整数值就会没有一个与之对 应的合法枚举,更糟糕的是,它们之间是可以进行运算的,就好像它们只是数字一样。
仅仅依靠整数作为标记的枚举类型,并不满足 Swift 函数式编程中的一条核心原则:高效地利用类型排除程序缺陷。
不同于 Objective-C,枚举在 Swift 中创建了新的类型,与整数或者其他已经存在的类型没有任何关系。
enum Encoding {
case ascii
case nextstep
case japaneseEUC
case utf8
}
extension Encoding {
var nsStringEncoding: String.Encoding {
switch self {
case .ascii:
return String.Encoding.ascii
case .nextstep:
return String.Encoding.nextstep
case .japaneseEUC:
return String.Encoding.japaneseEUC
case .utf8:
return String.Encoding.utf8
}
}
}
extension Encoding {
init?(encoding: String.Encoding) {
switch encoding {
case String.Encoding.ascii:
self = .ascii
case String.Encoding.nextstep:
self = .nextstep
case String.Encoding.japaneseEUC:
self = .japaneseEUC
case String.Encoding.utf8:
self = .utf8
default:
return nil
}
}
}
extension Encoding {
var localizedName: String {
String.localizedName(of: nsStringEncoding)
}
}
可以看到 Swift 的枚举表示若干选项中的特定选项的用法。Encoding 枚举为不同的字符编码方案提供了一种安全、类型化的表示方式。
关联值
先来看一个例子,编写一个populationOfCapital函数从一个字典中查找一个国家的首都,如果找到, 它会返回该城市的人口总数。这个函数的返回类型是一个整数类型的可选值:如果所有信息都被找到的话,返回人口数;否则,返回 nil。
我们会更希望 populationOfCapital 函数返回一个 Int 或者一个 Error。利用 Swift 的枚举,就可以搞定这件事。可以重新定义 populationOfCapital 函数,使之返回一 个 PopulationResult 枚举的成员,来代替之前的 Int?。可以像下文这样定义 PopulationResult:
enum LookupError: Error {
case capitalNotFound
case populationNotFound
}
enum PopulationResult {
case success(Int)
case error(LookupError)
}
PopulationResult 的每个枚举值都具有一个关联值:枚举值 success 关联了一个整数,而 error 则关联了一个 Error。
现在实现 populationOfCapital 函数,使之返回一个 PopulationResult:
let capitals = ["China": "Beijing"]
let citys = ["Beijing": 2200]
func populationOfCapital(country: String) -> PopulationResult {
guard let capital = capitals[country] else {
return .error(LookupError.capitalNotFound)
}
guard let population = citys[capital] else {
return .error(LookupError.populationNotFound)
}
return PopulationResult.success(population)
}
函数会返回人口数或者一个 LookupError。首先检查了 capitals 字典中是否存在对应的首都名,如果不存在,就返回一个 .capitalNotFound 错误。接着,验证了 cities 字典中是否存在对应的人口数,如果不存在,则返回一个 .populationNotFound 错误。最后,如果两次查询都找到了对应的值,便返回一个 success。
可以使用一个 switch 语句来确定populationOfCapital函数是否成功:
switch populationOfCapital(country: "France") {
case let PopulationResult.success(population):
print("人口是\(population)")
case let PopulationResult.error(error):
switch error {
case LookupError.capitalNotFound:
print("首都没找到")
case LookupError.populationNotFound:
print("人口没记录")
}
}
添加泛型
想写一个与 populationOfCapital 类似的函数,只不过不是查询人口,而是查询一个国家首都的市⻓。可以简单的查询到一个国家的首都,然后在结果中使用 atMap 找到这座城 市的市⻓:
let mayors = [
"Paris": "Hidalgo",
"Madrid": "Carmena", "Amsterdam": "van der Laan", "Berlin": "Müller"
]
func mayorOfCapital(country: String) -> String? {
capitals[country].flatMap { mayors[$0] }
}
然而,使用可选值作为返回类型,依旧不会告诉我们为什么查询会失败。可以定义一个新枚举 MayorResult,来对应两种可能的情况:
enum MayorResult {
case success(String)
case error(Error)
}
可以利用这个枚举来编写另一个版本的 mayorOfCapital 函数 —— 不过为每一个新函数都引入一个枚举实在是太乏味了。更何况,MayorResult 与 PopulationResult 大同小异。两个枚举值唯一的区别就是 success 的关联值类型。所以可以定义一个新的枚举,将泛型作为 success 的关联值:
enum Result<T> {
case success(T)
case error(Error)
}
现在可以在 populationOfCapital 与 mayorOfCapital 函数中中使用同样的结果类型了:
enum LookupError1: Error {
case capitalNotFound
case populationNotFound
case mayorNotFound
}
func populationOfCapital1(country: String) -> Result<Int> {
guard let capital = capitals[country] else {
return Result.error(LookupError1.capitalNotFound)
}
guard let population = citys[capital] else {
return Result.error(LookupError1.populationNotFound)
}
return Result.success(population)
}
func mayorOfCapital1(country: String) -> Result<String> {
guard let capital = capitals[country] else {
return Result.error(LookupError1.capitalNotFound)
}
guard let mayor = mayors[capital] else {
return Result.error(LookupError1.mayorNotFound)
}
return Result.success(mayor)
}
Swift 中的错误处理
Swift 中内建的错误处理机制与上文定义的 Result 类型十分相似。它们的不同 主要有两点:
- Swift 强制使用关键字 throws 注明哪些函数和方法可能抛出错误,且必须使用 try (或 try 的变体) 来调用这些代码。如果换作 Result 类型的话,是无法在静态环境下确保 错误被处理的。
- Swift 内建错误处理机制的局限性在于,它必须借助函数的返回类型来触
发:如果我们想构建一个函数,且提供的参数包含失败情况 (比如一个回调函数),使用 throw 的方式来提供这个参数,会让一切都变得复杂起来。若是换用可选值或 Result,编写起来就没 那么繁琐,处理也会更简单。
使用 Swift 的错误处理机制重写 populationOfCapital:
func populationOfCapital2(country: String) throws -> Int {
guard let capital = capitals[country] else {
throw LookupError1.capitalNotFound
}
guard let population = citys[capital] else {
throw LookupError1.populationNotFound
}
return population
}
在 do 执行块中编写正常的流程,然后在 catch 块中 去处理所有可能的错误:
do {
let population = try populationOfCapital2(country: "France")
print("人口是\(population)")
} catch {
switch error {
case LookupError1.capitalNotFound:
print("首都没有找到")
case LookupError1.populationNotFound:
print("人口没有找到")
default:
print("其他原因导致人口查询失败")
}
}
再聊聊可选值
实际上,Swift 内建的可选值类型与 Result 类型也很像:
enum Optional<Wrapped> {
case none
case some(Wrapped)
// ……
}
可以在自己的 Result 类型中定义一些用于操作可选值的函数。通过在 Result 中重新定义 ?? 运算符,可以对 Result 进行运算:
func ??<T>(result: Result<T>, handleError: (Error) -> T) -> T {
switch result {
case .success(let value):
return value
case .error(let error):
return handleError(error)
}
}
let handelError: (Error) -> Int = {
print("发生错误:\($0)")
return 0
}
print(Result.success(100) ?? handelError)
print(Result.error(LookupError1.populationNotFound) ?? handelError)
值得注意的是没有使用 autoclosure 来标记第二个参数。实际上,在这里会显式地要求传入一个以 Error 作为参数 的函数,而该函数需要返回一个类型为 T 的值。
为什么使用枚举?
在实际开发中,可选值可能还是会比上文定义的 Result 类型更好用,原因有很多:
- 内建的语法 糖使用起来更方便
- 相对于使用自己定义的枚举,依赖一些已经存在的类型,会使你定义的接 口更容易被其他 Swift 开发者接受
- 而且有时候并不值得为 Error 专⻔费事去定义一个枚举
使用枚举去定义自己的类型,来解决具体需求。通过让类型更加严密, 可以在程序测试或运行之前,就利用 Swift 的类型检验优势,来避免许多错误。
网友评论