作者 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 里面运行。