Swift 程式語言 — Closures (1)

讓我們來一起探討閉包中的奧秘吧!

Jeremy Xue
Jeremy Xue ‘s Blog
9 min readFeb 15, 2019

--

前言:

在學習程式語言的時候,常常會聽到閉包這個名詞術語,但要真的來解釋這個東西的用途時卻又好像模糊不清。因此,這邊文章希望能帶領讀者們一起來慢慢進入閉包的世界裡。

⎮ 閉包

閉包是自包含的功能區塊,它能夠在程式碼中被傳遞和使用。Swift 中的閉包類似於 C 以及 Objective-C 的區塊以及其他程式語言中的 lambdas。

閉包能夠捕獲和存儲定義在其上下文中的任何常量和變量的引用,這也就是所謂的閉合併包裹那些常量和變量,因此被稱為“閉包”,Swift能夠為你處理所有關於捕獲的內存管理的操作。

函數中引入全局函數和嵌套函數實際上是必包的特例。閉包採用下列三種形式之一:

  • 全局函數是具有名稱但不補獲任何值的閉包。
  • 嵌套函數是具有名稱的閉包,可以從其封閉函數中捕獲值。
  • 閉包表達式是一種沒有名稱的閉包,以輕量級語法編寫,可以從周圍的上下文中捕獲值。

Swift 的閉包表達式擁有乾淨、簡潔的風格,鼓勵在常見場景中實現簡潔,無累贅的語法。常見的優化有:

  • 利用上下文推斷參數和返回值的類型
  • 單表達式的閉包的隱式返回
  • 簡寫參數名稱
  • 尾隨閉包語法

⎮ 閉包表達式

嵌套函數( 在嵌套函數中引入 )是一種方便的手段,可以將自包含的程式碼區塊命名且定義為大型函數中的一部份。然而,在沒有完整聲明和名稱對情況下編寫一個類似函數的構造的簡短版本是有用的,尤其當你使用函數作為一個或多個參數的函數或方法時。

閉包表達式是一種以簡短的語法來編寫閉包的方法。閉包表達式提供了幾種語法的優化,用於以簡短方式編寫閉包,而不會減少清晰度或是意圖。下邊的閉包表達式範例透過使用幾次迭代演示 sorted(by:) 方法的精簡來展示這些優化,每一次都讓相同的功能性更加簡潔。

⎮ 排序方法

Swift 的標準庫提供了一個稱為 sorted(by:) 的方法,它會根據你提供的排序閉包對已知類型的數組的值進行排序。一旦他完成排序後,sorted(by:) 方法返回一個與舊數組相同類型和大小的數組,其中元素按正確的排序順序排列,而原始術組不會被 sorted(by:) 方式修改。

下面我們用使用這個數組來演示這些過程:

sorted(by:) 方法會接受一個閉包,該閉包會接收兩個與數組內容相同類型的兩個參數,並且返回一個 Bool 值。表示一旦被排序的第一個值是在第二個值的之前還是之後。如果是之前,那麼排序閉包需要返回 true ,反之為 false。

因為我們的數組類型為 [String] ,因此我們的排序閉包的類型為 (String, String) -> Bool 的函數。下面我們定義一個 backward 的方法:

這邊我們所要 return s1 > s2的意思是,當我們 s1 的字串大於 s2 的字串時則返回 true ,s1 在 s2 前面;反之則為 false,s1 在 s2 之後。而我們字串比較大小時實際上是在比較其字母的順序,所以用這個例子來說明,字母順序越是後面實際上就要排在越前面。

然而,這是一種相當冗長的方式來編寫基本上是一個單表達式函數( a > b )。在這個例子,最好使用閉包表達式來編寫其排序閉包。

⎮ 閉包表達式語法

閉包表達式語法具有以下的形式:

閉包表達式語法中的參數可以是輸入輸出參數,但他們不能有默認值。如果命名為可變參數,則可以使用可變參數。元組也可以用作參數類型和返回類型。

我們這邊使用上面的 reversedNames 範例來編寫一個的閉包表達式版本:

請注意,此種閉包的參數和返回類型宣吿與 backward(_:,_:) 函數的宣告相同。這兩種情況下,他都寫成 (s1:String, s2:String) -> Bool。但是,對於閉包表達式,參數和返回類型都寫在花括號 {} 內,而不是外面。

而閉包的主體的開頭由 in 關鍵字引入。這個關鍵字表示閉包的參數和返回值類型定義已經完成,並且閉包的主題即將開始。

而又因為閉包的主題簡短,甚至能使用一行程式碼表達:

範例中對於sorted(by:)方法的整體部分調用保持不變,一對圓括號仍然包裹函數的所有參數。然而,這些參數中的一個變成了現在的行內閉包。

⎮ 從上下文推斷類型

因為排序閉包作為參數傳遞給方法,所以 Swift 可以推斷出它們參數類型和它的返回值類型。 sorted(by:) 方法是字串數組上調用的,因此其參數類型必須為 (String, String) -> Bool 的函數。這意味著 (String, String) 和 Bool 類型不需要作為閉包表達式定義的一部份編寫。因為能夠推斷出所有類型,所以也能省略返回箭頭和參數名稱的括號。

因此我們閉包內的寫法由上面的方式,經由類型推斷省略成下面的寫法。

在閉包作為行內閉包表達式傳遞給函數或方法時,總是能夠推斷出參數類型以及返回類型。因此,當閉包用作函數或方法參數時,你永遠不需以最完整的形式編寫行內閉包。

⎮單表達式閉包的隱式返回

單表達式閉包可以透過從宣告中省略 return 關鍵字來隱式返回單個表達式的結果,如上一個範例的版本:

這裡因為 sorted(by:) 方法的參數的函數類型清楚表明閉包必須返回一個 Bool 值。因為閉包的主體包含一個返回 Bool 值的表達式 (s1 > s2),所以沒有歧義,能夠省略 return 關鍵字。

⎮簡寫參數名稱

Swift 自動為行內閉包提供簡寫的參數名稱,我們能夠透過 $0$1$2 …來引用閉包參數的值。

如果你在閉包表達式中使用這些簡寫參數名稱,那麼你可以在閉包的參數列表中忽略對其的定義,並且簡寫參數名稱的數字和類型將會從期望的函數類型中推斷出來。 in 關鍵字也能被省略,因為閉包表達式完全由它的函數體組成:

這邊的 $0$1 分別為引用閉包內的第一、二個 String 參數。

⎮運算符函數

實際上還有一種更簡短的方式來撰寫上述閉包表達式。Swift 的 String 類型定義了關於大於號 > 的特定字符串實現,讓其作為一個有兩個 String 類型參數的函數並返回一個 Bool 類型的值。這正好與 sorted(by:)方法的第二個參數需要的函數匹配。因此,簡單傳遞一個大於符號,並且 Swift 將推斷你想使用大於號特殊字符串函數實現:

最後來看看我們的閉包的演進過程,並且檢視每一次都省略了什麼,以及 Swift 所幫們推斷所補足的東西:

⎮ 尾隨閉包

如果你需要將閉包表達式作為函數的最終參數傳遞給函數,並且閉包表達式很長,則將其寫成尾隨閉包 ( trailing closure ) 是很有用的。

在函數調用的括號後編寫尾隨閉包,即使他仍然是函數的參數。使用尾隨閉包語法時,不需將閉包的參數標籤寫為函數調用的一部分。

在我們上方閉包表達式語法的字串排序閉包也能夠作為尾隨閉包編寫在 sorted(by:) 方法的括號之外:

如果閉包表達式被提供作為函數或方法的唯一參數時,而且你將該表達式作為尾隨閉包提供時,那麼在調用函數時,你不需要在函數或方法的名稱後寫上括號 ()

當你的閉包長到無法將其寫入行內閉包中,那麼尾隨閉包是最有用的方式。舉個例子,Swift 的數組有一個 map(_:) 方法,他將一個閉包表達式作為單個參數,對數組中的每個項目調用一次閉包,並該項返回一個替代映射值( 可能是某些其他類型 )。

將提供的閉包應用於每個數組元素後,map(_:) 方法返回一個包含所有映射值的新數組,其順序與原始數組中的相應值相同。

以下是如何使用帶有尾隨閉包的 map(_:) 方法將 Int 值數組轉換為 String 值數組。 [16,58,510] 轉換成 [“OneSix”,“FiveEight”,“FiveOneZero”]

我們建立了整數與其名稱的英文版本的映射字典,以及定義了一個整數數組來轉換字符串。

現在你可以使用 numbers 數組來創建一個 String 數組,方法是將閉包表達式作為尾隨閉包傳遞給數組的 map(_:) 方法:

map(_:) 方法為數組中的每個項目調用一次閉包表達式,你不需要指定閉包的輸入參數 number 的類型,因為可以從要映射的數組中推斷。

這邊閉包的處理流程為:

  1. 先將閉包中的 number 賦給變數 number ,使其在閉包中能被修改。
  2. repeat-while 迴圈執行將 number % 10 的結果轉換成英文字串,並放到原有的 output 字串之前,並將 number 的值除以 10。反覆執行,直到 number < 0 為止。
  3. 回傳 output

如此反覆執行後,我們的 strings 結果就為 numbers 所映射出來的數組了。

在上面這個例子中尾隨閉包在該函數後整潔地封裝了具體的閉包功能,而不再需要將整個閉包包在 map(:_) 方法的括號內。

後記:

那我們這次閉包的教學就到這邊結束了,閉包常常是大家對於程式中模糊的一塊,希望大家對於閉包的使用能有初步的了解,並且了解到如何使用閉包表達式語法來處理閉包,並且了解到哪些是能夠經由 Swift 推斷且能忽略的部分。

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

Hi, I’m Jeremy. [好想工作室 — iOS Developer]