Swift函数式编程七(枚举)

2021-07-22  本文已影响0人  酒茶白开水

代码地址

在 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 的错误处理机制重写 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 的类型检验优势,来避免许多错误。

上一篇 下一篇

猜你喜欢

热点阅读