深度解析 Swift, Pattern-Matching

作者 Nick Teissler 翻译者 Guanglei Liu

原文出处: https://staging-preview.bignerdranch.com/blog/pro-pattern-matching-in-swift/

毫无疑问,switch statement 是 Swift 语言强大的功能之一。使用 switch statement 来实现的 Swift 的 pattern-matching 可以使代码的可读性,和安全性大大提高。下面我就来学习一下如何在你的代码中使用 Swift 的 pattern-matching。

Swift language reference 里面记录了八种不同的 pattern。想要知道正确的书写方式会存在一定的难度。有一些很典型的用例,比如想要得知 type information, 或者 unwrap 一个 variable,再或者想判断一个 optional 是否为 nil。 使用正确的 pattern, 你可以避免使用一些无谓的 unwraps 和定义多余的 variables。

Pattern Matching 有两个重要的组成部分:pattern 和 value。value 是 switch 关键字后面的表达式。pattern 则是 case 之后的表达式。

我们将会把 pattern-matching 结合 if, guard, 还有 while statements 一起使用, 但在这之前我们先来看一下 switch statement 的基本使用方式。

判断变量是否为 nil

加入一个变量可能是 nil 的话,我们就可以用 Optional Pattern 去得到不为 nil 的值,并且还不需要 unwrap. 这个功能就非常适合处理一些 Objective-C methods。 假如 Objective-C functions 没有定义 nullable annotations 那么就必须要处理这个情况。

例如这个 Objective-C function:

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

它的 Swift signature 是 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,就会得到一个 error。Value of optional type ‘String?’ not unwrapped; did you mean to use ‘!’ or ?? 。这时候就可以使用 case let output 这个简单的 variable-assignment pattern match。它可以自动 match aLegacyObjcFunction 的返回值,并且不需要 unwrapping 这个值。 这个时候 aLegacyObjcFunction() 就可以通过编译因为它的返回值将是一个 String!。我们就可以更轻松的处理这个问题特别是当 Objective-C function 真的会返回一个 nil

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 去 unwrap aLegacyObcFunction 的返回值,因为 let output? 就会使返回值是不为空的 String

精确的捕捉自定义的 Error Types

Pattern-matching 在捕捉自定义 Error Types 时显得特别有效。常用的 design pattern 是使用 enums 来定义 Error Types。 这在 Swift 中特别有效,因为它可以给 Error Types 附加一些值,使它的信息更加丰富。

在这里我们演示两种不同的 Type-Casting Pattern 来处理 Enumeration Case Pattern

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 都提供了我们所需要的信息。

The One-Off Match

有的时候你可能只需要捕捉 enum 里面的一个 case 因为你并不在乎其他的 case。这个时候 switch statement 全部写出来有可能会显得很繁琐。

我们可以使用 if case 过滤出不为 nil 的值

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

以上的代码在一个 statement 里面使用了三个 pattern。 最上面是一个 Tuple Pattern 里面包含了一个 Optional Pattern,还有 Wildcard Pattern _

或者你也可以使用 guard case 来实现相同的效果:

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

我们也可以使用 pattern matching 来当做 while 或者 for-in loop 的一个终止条件。 这对处理 ranges 的时候特别有效。Expression Pattern 可以让我们避免使用传统的方式 例如 variable >= 0 && variable <= 10 construct[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!")

以上所有的例子都是 pattern 在 关键词 case 之后, value 在 = 之后。语法上唯一例外的时候是有 is, as 或者 in 这样的关键词的时候。 这种情况下,你可以把他们当成是 =, 这样结构就一样了。

Range matching Expression Pattern 特别的地方是它不是 compiler 自带的功能。 Expression Pattern 使用了 Swift Standard Library ~= operator~= operator 是系统自带的。

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

你可以自己 overrides 这个 operator 的特性,让它可以去检测特殊的 range。

Matching Regular Expressions

让我们用 ~= operator 来生成一个 Regex type。

它是使用 pattern-matching 来给 NSRegularExpression 生成的一个 wrapper。非常简单易懂,当我们做跟 regular expression 有关的工作的时候应该尽量使用这个方法。

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 struct 是一个 NSRegularExpression property。他可以用 String 初始化,如果 init 出错的话,会打印出 error message 并且生成一个 match-all regex 。接下来我们就来用 extension 实现 pattern-matching operator

sextension Regex {
    static func ~=(pattern: Regex, value: String) -> Bool {
        return pattern.match(value)
    }
}

接下来我们生成两个常量。 email regex 用来从 String 里面判断是否是 email address

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])+)\\])$
"""

phone numbers regex 更加简单一些

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

现在我们就可以通过 pattern matching 更简单的判断 phone 和 email

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.")
}

基本知识

虽然我们介绍了很多的 pattern matching 方法, 但是我们不应该忘记 switch 的基本使用方法。当 pattern-matching ~= operator 没有被定义的时候,Swift 会在 switch 里面使用 == operator。 这个时候就不是在使用 pattern-matching 了

以下的例子是用 switch statement 当成 demultiplexer。 这个时候的switch 就像 UITextFields 的 delegate 一样来通知具体是哪个 TextField

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 里面的一些 pattern-matching 语法。在这个基础上 一共有8种不同的 pattern-matching 可以被使用。pattern-matching 有很多的有点,也是开发人员不可缺少的工具。以上所有的例子的源代码都可以在这里找到。代码可以直接在 playground 里面运行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值