[譯] 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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索