Swift函数式编程七(枚举)

Posted 酒茶白开水

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift函数式编程七(枚举)相关的知识,希望对你有一定的参考价值。

代码地址

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

以上是关于Swift函数式编程七(枚举)的主要内容,如果未能解决你的问题,请参考以下文章

Swift函数式编程八(纯函数式数据结构)

Swift函数式编程八(纯函数式数据结构)

Swift函数式编程八(纯函数式数据结构)

七函数式编程

Swift 响应式编程

Swift 函数式编程实践