[译] Swift 中强大的模式匹配

Swift 语言一个不容置疑的优势就是 switch 语句。在 switch 语句的背后是 Swift 的模式匹配,它使得代码更易读,且安全。你能够利用 switch 语句的模式匹配的可读性和优点,将其应用于代码中的其余位置。html

Swift 语言文档中指定了八种不一样的模式。在模式匹配表达式中,咱们很难知道其正确的语法。在实际状况中,你可能须要知道类型信息,来解包取得变量的值,或者只是确承认选值是非空的。使用正确的模式,能够避免笨拙地解包和未使用的变量。前端

模式匹配中有两个参与者:模式和值。值是紧跟 switch 关键字其后的表达式,或者,若是值在 switch 语句外测试的,则为 = 运算符。模式则是 case 后面的表达式。使用 Swift 语言的规则会对模式和值相互评估。截至 2018 年 7 月 15 日,参考文档中仍有一些关于如何在文章中以及在何处使用模式的一些错误,不过咱们能够经过一些实验来发现它们。[1]android

接下来,咱们先看看在 ifguard、和 while 语句中应用模式,但在此以前,让咱们用 switch 语句的一些非原生用法热下身。ios

仅匹配非空变量

若是试图匹配的值可能为空,咱们可使用可选值模式来匹配,若是不是非空的,就解包取值。在处理遗留下来的(以及一些不那么遗留)的 Objective-C 方法和函数时,这一点尤为有用。对于 Swift 4.2,IUO 的从新实现使 !? 同义。而对于 Objective-C 方法,若是没有 nullable 注解,你可能不得不处理此行为。git

下面的例子是特别微不足道的,由于这个新的行为可能对于小于 Swift 4.2 的版本不太直观。如下是 Objective-C 方法:github

- (NSString *)aLegacyObjcFunction {
    return @"I haven't been updated with modern obj-c annotations!";
}
复制代码

Swift 方法签名是:func aLegacyObjcFunction() -> String!,而且在 Swift 4.1 中,这个方法能够经过编译:正则表达式

func switchExample() -> String {
    switch aLegacyObjcFunction() {
    case "okay":
       return "fine"
    case let output:
        return output  // implicitly unwrap the optional, producing a String
    }
}
复制代码

而在 Swift 4.2 中,你会收到以下报错:“Value of optional type ‘String?’ not unwrapped; did you mean to use ‘!’ or ‘?’?”(可选类型 ‘String?’ 的值尚未解包,你是否想要使用 ‘!’ 或 ‘?’ ?)。case let output 是一个简单的变量赋值模式匹配。它会匹配 aLegacyObjcFunction 返回的 String? 类型而不会去解包取值。其中不直观的部分是,return aLegacyObjcFunction() 是能够经过编译的,由于它跳过了变量赋值(模式匹配),类型推断所以返回的类型是一个 String! 的值,这由编译器处理。咱们应该更优雅地处理它,特别是若是存在有问题的 Objective-C 函数,实际上能够返回 nilexpress

func switchExample2() -> String {
    switch aLegacyObjcFunction() {
    case "okay":
        return "fine"
    case let output?:
        return output 
    case nil:
        return "Phew, that could have been a segfault."
    }
}
复制代码

这一次,咱们故意去处理可选性的问题。请注意,咱们没必要使用 if let 来解开 aLegacyObcFunction 的返回值。空模式匹配帮咱们处理 case let output?:,其中 output 是一个 String 类型的值。swift

精确捕获自定义错误类型

在捕获自定义错误类型时,模式匹配很是有用,且富有表现力。一种常见的设计模式是,使用 enum 来定义自定义错误类型。这在 Swift 中尤为有效,由于能够容易地将关联值增添到枚举用例中,用来提供更多有关错误的详细信息。后端

这里咱们使用两种类型的类型转换模式,以及两种枚举用例模式来处理可能抛出的任何错误:

enum Error: Swift.Error {
    case badError(code: Int)
    case closeShave(explanation: String)
    case fatal
    case unknown
}

enum OtherError: Swift.Error { case base }

func makeURLRequest() throws { ... }

func getUserDetails() {
    do {
        try makeURLRequest()
    }
    // Enumeration Case Pattern: where clause
    catch Error.badError(let code) where code == 50 {
         print("\(code)") }
    // Enumeration Case Pattern: associated value
     catch Error.closeShave(let explanation) {
         print("There's an explanation! \(explanation)")
     }
     // Type Matching Pattern: variable binding
     catch let error as OtherError {
         print("This \(error) is a base error")
     }
     // Type Matching Pattern: only type check
     catch is Error {
         print("We don't want to know much more, it must be fatal or unknown")
     }
     // is Swift.Error. The compiler gives us the variable error for free here
     catch {
         print(error)
     }
}
复制代码

在每一个 catch 上方,咱们匹配并捕获了咱们须要的尽量多的信息。下面从 switch 开始,看看咱们还能在哪里使用模式匹配。

一次性匹配

不少时候你可能想要进行一次性模式匹配。你可能只需在给定单个枚举值的状况下应用更改,并且不关心其余值。此时,优雅可读的 switch 语句忽然变成了累赘的样板文件。

咱们仅能够在非空的元组值中使用 if case 来解开它:

if case (_, let value?) = stringAndInt {
    print("The int value of the string is \(value)")
}
复制代码

上面的例子在一条语句中使用了三种模式!顶部元组模式,其中包含了一个可选模式(与上面匹配非空变量的模式没有什么不一样),还有一个鬼祟的通配符模式,_。 若是咱们使用 switch stringAndInt {...},编译器会强制咱们显式地处理全部可能的状况,或者执行 default 语句。

或者,若是 guard case 更能知足你的需求,则无需更改:

guard case (_, let value?) = stringAndInt else {
    print("We have no value, exiting early.")
    exit(0)
}
复制代码

你可使用模式来定义 while 循环和 for-in 循环的中止条件。这在范围中很是有用。正则表达式模式容许咱们避免传统的variable >= 0 && variable <= 10 构造 [2]:

var guess: Int = 0

while case 0...10 = guess  {
    print("Guess a number")
    guess = Int(readLine()!)!
}
print("You guessed a number out of the range!")
复制代码

在全部这些例子中,模式紧跟在 case 以后,值则在 = 以后。语法与此不一样的表达式中有 isasin 关键字。在这些状况下,若是将这些关键字视为 = 的替代品,那么结构是相同的。记住这一点,而且经过编译器的提示,你可使用全部 8 种模式,而无需参考语言的文档。

到目前为止,咱们在前面的例子中尚未看到用 Range 来匹配表达式模式的一些独特之处:它的模式匹配实现不是内置功能,至少不是内置于编译器中的。表达式模式使用了 Swift 标准库 ~= 操做符~= 操做符是一个自由的泛型函数,定义以下:

func ~= <T>(a: T, b: T) -> Bool where T : Equatable
复制代码

你能够看到 Swift 标准库中的 Range 类型重写了该运算符,提供了一个自定义行为,用来检查特定值是否在给定的范围内。

匹配正则表达式

下面让咱们建立一个实现 ~= 操做符的 Regex 类型。它将会是围绕 NSRegularExpression 的一个轻量级的封装器,它使用模式匹配来生成更具可读性的正则表达式代码,在使用神秘的正则表达式时,应始终感兴趣。

struct Regex: ExpressibleByStringLiteral, Equatable {

    fileprivate let expression: NSRegularExpression

    init(stringLiteral: String) {
        do {
            self.expression = try NSRegularExpression(pattern: stringLiteral, options: [])
        } catch {
            print("Failed to parse \(stringLiteral) as a regular expression")
            self.expression = try! NSRegularExpression(pattern: ".*", options: [])
        }
    }

    fileprivate func match(_ input: String) -> Bool {
        let result = expression.rangeOfFirstMatch(in: input, options: [],
                                range NSRange(input.startIndex..., in: input))
        return !NSEqualRanges(result, NSMakeRange(NSNotFound, 0))
    }
}
复制代码

这就是咱们的 Regex 结构体。它有一个 NSRegularExpression 属性。它能够初始化为字符串字面常量,其结果是,若是咱们没法传递一个有效的正则表达式,那么咱们将获得失败的消息和一个匹配全部的正则表达式。接下来,咱们实现模式匹配操做符,将其嵌套在扩展中,这样就能够清楚地知道要在何处使用该操做符。

extension Regex {
    static func ~=(pattern: Regex, value: String) -> Bool {
        return pattern.match(value)
    }
}
复制代码

咱们但愿这个结构体是开箱即用的,因此我将定义两个类常量,用来处理一些常见的正则验证需求。匹配邮箱的正则表达式是从 Matt Gallagher 的 Cocoa with Love 文章里面借用的,并检查了 RFC 2822 中定义的电子邮件地址。

若是你在 Swift 中使用正则表达式,那么你不能就简单地从 Stack Overflow 关于 Regex 帖子中直接复制代码。Swift 字符串定义转义序列,如换行符(\n),制表符(\t),和 unicode 标量(\u{1F4A9})。这与正则表达式的语法相冲突,由于正则表达式含有大量的反斜杠和全部类型的括号。像 Python,则有方便的原始字符串语法。原始字符串将按逐字逐句地获取每一个字符,而且不会解析转义序列,所以能够以“纯净的”形式插入正则表达式。在 Swift 中,字符串中任何单独的反斜杠都表示转义序列,所以对于编译器来讲,若是想要接受大多数的正则表达式,就须要转义序列以及一些其余特殊字符。这里有一个小尝试,尝试在 Swift 中使用原始字符串,但最后失败了。随着 Swift 继续成为一种多平台,多用途的语言,人们可能会对这个功能从新产生兴趣。在此以前,现有复杂的匹配邮件的正则表达式,变成了这个 ASCII 的艺术怪物:

static let email: Regex = """ ^(?:[a-z0-9!#$%\\&'*+/=?\\^_`{|}~-]+(?:\\.[a-z0-9!#$%\\&'*+/=?\\^_`{|}~-]+)*|\"(?:[\\x01-\\x08\ \\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@\ (?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0\ -4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?\ :[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7\ f])+)\\])$ """
复制代码

咱们可使用一个更简单的表达式来匹配电话号码,借用 Stack Overflow 以及如前面所述的双转义:

static let phone: Regex = "^(\\+\\d{1,2}\\s)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$"
复制代码

如今,咱们可使用方便、易读的模式语法来识别电话号码或电子邮件:

let input = Bool.random() ? "nerd@bignerdranch.com" : "(770) 817-6373"
switch input {
    case Regex.email:
        print("Send \(input) and email!")
    case Regex.phone:
        print("Give Big Nerd Ranch a call at \(input)")
    default:
        print("An unknown format.")
}
复制代码

你可能想知道为何看不到上面的 ~= 操做符。由于它是 Expression Pattern 的一个实现细节,且是隐式使用的。

牢记这些基础知识!

有了全部这些奇特的模式,咱们不该该忘记使用经典 switch 语句的方法。当模式匹配 ~= 操做符未定义时,Swift 在 switch 语句中会使用 == 操做符。重申一下,咱们如今再也不处于模式匹配的范畴。

如下是一个例子。这里的 switch 语句用来作一个给委托回调的分离器。它对 NSObject 子类的 textField 变量执行了 switch 语句。所以,等式被定义为了标识比较,它会检查两个变量的指针值是否相等。举个例子,以一个对象做为三个 UITextField 对象的委托。每一个文本字段都须要以不一样的方式验证其文本。当用户编辑文本时,委托为每一个文本字段接收相同的回调,

func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
    switch textField {
        case emailTextField:
            return validateEmail()
        case phoneTextField:
            return validatePhone()
        case passwordTextField:
            return validatePassword()
        default:
            preconditionFailure("Unaccounted for Text Field")
    }
}
复制代码

而且能够不一样地验证每一个文本字段。

结论

咱们查阅了 Swift 中可用的一些模式,并检查了模式匹配语法的结构。有了这些知识,全部 8 种模式均可供使用!模式具备许多优势,它是每一个 Swift 开发者的工具箱中不可或缺的一部分。这篇文章还有未涵盖到的内容,例如编译器检查穷举逻辑的细节以及结合 where 语句的一些模式。

感谢 Erica Sadun 在她的博客文章 Afternoon Whoa 中向我介绍了 guard case 语法,它是这篇文章的灵感来源。

这篇文章中的全部例子均可以在 gist 中找到。代码能够在 Playground 运行,也能够根据你的须要进行挑选。

[1] 该指南要求使用具备关联值的枚举,“对应的枚举用例模式必须指定一个元组模式,其中包含每一个关联值的一个元素。”若是您不须要关联的值,只需包含没有任何关联值的enum状况就能够编译和匹配。

另外一个小的更正是,自定义表达式操做符 ~= 可能 “仅出如今 switch 语句大小写标签中”。在上述例子中,咱们也在一个 if 语句中使用到它。Swift 语法正确地说明了上述两种用法,这个小错误只在本文中。

[2] readLine 方法不适用于 Playground。若是要运行此示例,请从 macOS 命令行应用中尝试。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索