【Swift學習】Swift編程之旅---閉包(十一)

  閉包是能夠在代碼中被傳遞和使用的自包含功能模塊,它很像c和oc中的block,和.net中的lambdasbas表達式。html

  閉包能夠捕獲和存儲其所在上下文中任意常量和變量的引用。 包裹着這些常量和變量的包俗稱閉包。Swift會爲您管理在捕獲過程當中涉及到的內存操做。下面是閉包的3中表現形式ios

  • 全局函數是一個有名字但不會捕獲任何值的閉包
  • 嵌套函數是一個有名字並能夠捕獲其封閉函數域內值的閉包
  • 閉包表達式是一個利用輕量級語法所寫的能夠捕獲其上下文中變量或常量值的沒有名字的閉包

  Swift的閉包表達式擁有簡潔的風格,並鼓勵在常見場景中以實現語法優化,主要優化以下: git

  •  利用上下文推斷參數和返回值類型
  •  單表達式(single-expression)閉包能夠省略 return 關鍵字
  •  參數名稱簡寫
  •  Trailing 閉包語法
 
   閉包表達式
   嵌套函數是一種在較複雜函數中方便進行命名和定義自包含代碼模塊的方式。 固然,有時候撰寫小巧的沒有完整定義和命名的類函數結構也是頗有用處的,尤爲是在處理一些函數並須要將另一些函數做爲該函數的參數時。下面閉包表達式的例子經過使用幾回迭代展現了 sort 函數定義和語法優化的方式
 
   sort方法
  
Swift 標準庫提供了 sort 函數,會根據您提供的排序閉包將已知類型數組中的值進行排序。 一旦排序完成,函數會返回一個與原數組大小相同的新數組,該數組中包含已經正確排序的同類型元素。
下面的閉包表達式示例使用 sort 函數對一個 String類型的數組進行字母逆序排序,如下是初始數組值:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

 

排序函數有兩個參數:
1. 已知類型值的數組。
2. 一個閉包,採用相同類型的數組的內容的兩個參數,並返回一個布爾值來表示是否將第一個值在排序時放到第二個值的前面或是後面。若是第一個值應該出現第二個值以前,閉包須要返回true,不然返回false。
 
該例子對一個  String 類型的數組進行降序排序,所以排序閉包需爲 (String, String) -> Bool 類型的函數。
func backwards(s1: String, _ s2: String) -> Bool { return s1 > s2 } var reversed = names.sort(backwards) // reversed is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

 

若是第一個字符串 (s1) 大於第二個字符串 (s2),backwards 函數則返回 true,表示在新的數組中 s1 應該出如今 s2 前。 字符中的 "大於" 表示 "按照字母順序後出現"。 這意味着字母 "B" 大於字母 "A", 字符串 "Tom" 大於字符串 "Tim"。 其將進行字母逆序排序,"Barry" 將會排在 "Alex" 以前,依次類推。
 
然而,這是一個至關冗長的方式,本質上只是寫了一個單表達式函數 (a > b)。 在下面的例子中,利用閉合表達式語法能夠更好的構造一個內聯排序閉包。
 
 
  閉包表達式語法
閉包表達式語法可使用常量、變量和 inout 類型做爲參數,但不提供默認值。 也能夠在參數列表的最後使用可變參數。元組也能夠做爲參數和返回值。
reversed = sort(names, { (s1: String, s2: String) -> Bool in return s1 > s2 }) 

 

須要注意的是內聯閉包參數和返回值類型聲明與 backwards 函數類型聲明相同。 在這兩種方式中,都寫成了 (s1: String, s2: String) -> Bool類型。 然而在內聯閉包表達式中,函數和返回值類型都寫在大括號內,而不是大括號外。
 
閉包的函數體部分由關鍵字 in 引入。 該關鍵字表示閉包的參數和返回值類型定義已經完成,閉包函數體即將開始。
 
由於這個閉包的函數體部分如此短以致於能夠將其改寫成一行代碼
reversed = sort(names, { (s1: String, s2: String) -> Bool in return s1 > s2 } )

 這說明 sort 函數的總體調用保持不變,一對圓括號仍然包裹住了函數中整個參數集合。而其中一個參數如今變成了內聯閉包 。express

 
   根據上下文推斷類型
  由於排序閉包是做爲函數的參數進行傳入的,Swift能夠推斷其參數和返回值的類型。 sort 指望第二個參數是類型爲 (String, String) -> Bool 的函數,所以實際上 String, String 和 Bool 類型並不須要做爲閉包表達式定義中的一部分。 由於全部的類型均可以被正確推斷,返回箭頭 (->) 和 圍繞在參數周圍的括號也能夠被省略:
reversed = sort(names, { s1, s2 in return s1 > s2 } ) 

 

際上任何狀況下,經過內聯閉包表達式構造的閉包做爲參數傳遞給函數時,均可以推斷出閉包的參數和返回值類型,這意味着您幾乎不須要利用完整格式構造任何內聯閉包。
 
然而,你也可使用明確的類型,若是你想它避免讀者閱讀可能存在的歧義,這樣仍是值得鼓勵的。這個排序函數例子,閉包的目的是很明確的,即排序被替換,並且對讀者來講能夠安全的假設閉包可能會使用字符串值,由於它正協助一個字符串數組進行排序。
 
   單行表達式閉包能夠省略 return
單行表達式閉包能夠經過隱藏 return 關鍵字來隱式返回單行表達式的結果,如上版本的例子能夠改寫爲:
reversed = sort(names, { s1, s2 in s1 > s2 } ) 

 

在這個例子中,sort 函數的第二個參數函數類型明確了閉包必須返回一個 Bool 類型值。 由於閉包函數體只包含了一個單一表達式 (s1 > s2),該表達式返回 Bool 類型值,所以這裏沒有歧義,return關鍵字能夠省略。
 
  參數名簡寫
Swift 自動爲內聯函數提供了參數名稱簡寫功能,您能夠直接經過 $0,$1,$2等名字來引用的閉包的參數的值。
 
若是您在閉包表達式中使用參數名稱簡寫,您能夠在閉包參數列表中省略對其的定義,而且對應參數名稱簡寫的類型會經過函數類型進行推斷。 in 關鍵字也一樣能夠被省略,由於此時閉包表達式徹底由閉包函數體構成:
 
 
reversed = sort(names, { $0 > $1 } ) 

 

  運算符函數數組

實際上還有一種更簡短的方式來撰寫上面例子中的閉包表達式。 Swift的 String 類型定義了關於大於號 (>) 的字符串實現,讓其做爲一個函數接受兩個 String 類型的參數並返回 Bool 類型的值。 而這正好與 sort 函數的第二個參數須要的函數類型相符合。 所以,您能夠簡單地傳遞一個大於號,Swift能夠自動推斷出您想使用大於號的字符串函數實現:
reversed = sort(names, >) 

 

    Trailing 閉包
若是您須要將一個很長的閉包表達式做爲最後一個參數傳遞給函數,可使用 trailing 閉包來加強函數的可讀性。
 
Trailing 閉包是一個書寫在函數括號以外(以後)的閉包表達式,函數支持將其做爲最後一個參數調用。
 
func someFunctionThatTakesAClosure(closure: () -> ()) { // 函數體部分  } // 如下是不使用 trailing 閉包進行函數調用   someFunctionThatTakesAClosure({ // 閉包主體部分  }) // 如下是使用 trailing 閉包進行函數調用   someFunctionThatTakesAClosure() { // 閉包主體部分  } 

 注意:若是函數只須要閉包表達式一個參數,當您使用 trailing 閉包時,您甚至能夠把 () 省略掉。安全

在上例中做爲 sort 函數參數的字符串排序閉包能夠改寫爲:閉包

reversed = sort(names) { $0 > $1 } 
當閉包很是長以致於不能在一行中進行書寫時,Trailing 閉包就變得很是有用。 舉例來講,Swift 的  Array 類型有一個 map 方法,其獲取一個閉包表達式做爲其惟一參數。 數組中的每個元素調用一次該閉包函數,並返回該元素所映射的值(也能夠是不一樣類型的值)。 具體的映射方式和返回值類型由閉包來指定。
 
當提供給數組閉包函數後,map 方法將返回一個新的數組,數組中包含了與原數組一一對應的映射後的值。
 
下例介紹瞭如何在 map 方法中使用 trailing 閉包將  Int 類型數組 [16,58,510] 轉換爲包含對應  String 類型的數組 ["OneSix", "FiveEight", "FiveOneZero"]:
let digitNames = [ 0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four", 5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine" ] let numbers = [16, 58, 510] 

 

上面的代碼建立了整數數字到他們的英文名字之間映射字典。 同時定義了一個準備轉換爲字符串的整型數組。
 
您如今能夠經過傳遞一個 trailing 閉包給 numbers 的 map 方法來建立對應的字符串版本數組。 須要注意的時調用 numbers.map 不須要在 map 後面包含任何括號,由於只須要傳遞閉包表達式這一個參數,而且該閉包表達式參數經過 trailing 方式進行撰寫:
 
let strings = numbers.map { (var number) -> String in var output = "" while number > 0 { output = digitNames[number % 10]! + output number /= 10 } return output } // strings 常量被推斷爲字符串類型數組,即 String[] // 其值爲 ["OneSix", "FiveEight", "FiveOneZero"] 

 

map 在數組中爲每個元素調用了閉包表達式。 您不須要指定閉包的輸入參數 number 的類型,由於能夠經過要映射的數組類型進行推斷。
 
閉包 number 參數被聲明爲一個變量參數 ,所以能夠在閉包函數體內對其進行修改。 閉包表達式制定了返回值類型爲  String,以代表存儲映射值的新數組類型爲 String
 
 
閉包表達式在每次被調用的時候建立了一個字符串並返回。 其使用求餘運算符 (number % 10) 計算最後一位數字並利用 digitNames 字典獲取所映射的字符串。
注意: 
字典 digitNames 下標後跟着一個歎號 (!),由於字典下標返回一個可選值 (optional value),代表即便該 key不存在也不會查找失敗。 在上例中,它保證了 number % 10 能夠老是做爲一個 digitNames 字典的有效下標 key。 所以歎號能夠用於強展開 (force-unwrap) 存儲在可選下標項中的 String 類型值。
 
 
從 digitNames 字典中獲取的字符串被添加到輸出的前部,逆序創建了一個字符串版本的數字。 (在表達式 number % 10中,若是number爲16,則返回6,58返回8,510返回0)。
 
number 變量以後除以10。 由於其是整數,在計算過程當中未除盡部分被忽略。 所以 16變成了1,58變成了5,510變成了51。
 
整個過程重複進行,直到 number /= 10 爲0,這時閉包會將字符串輸出,而map函數則會將字符串添加到所映射的數組中。
 
上例中 trailing 閉包語法在函數後整潔封裝了具體的閉包功能,而再也不須要將整個閉包包裹在 map 函數的括號內。
 
 
捕獲 (Caputure)

閉包能夠在其定義的上下文中捕獲常量或變量。 即便定義這些常量和變量的原做用域已經不存在,閉包仍然能夠在閉包函數體內引用和修改這些值。
 
Swift最簡單的閉包形式是嵌套函數,也就是定義在其餘函數體內的函數。 嵌套函數能夠捕獲其外部函數全部的參數以及定義的常量和變量。
 
下例爲一個叫作 makeIncrementor 的函數,其包含了一個叫作 incrementor 嵌套函數。 嵌套函數 incrementor 從上下文中捕獲了兩個值,runningTotal 和 amount。 以後 makeIncrementor 將 incrementor 做爲閉包返回。 每次調用 incrementor 時,其會以 amount 做爲增量增長 runningTotal 的值。
func makeIncrementor(forIncrement amount: Int) -> () -> Int { var runningTotal = 0 func incrementor() -> Int { runningTotal += amount return runningTotal } return incrementor } 

 

makeIncrementor 返回類型爲 () -> Int。 這意味着其返回的是一個函數,而不是一個簡單類型值。 該函數在每次調用時不接受參數只返回一個  Int 類型的值。 關於函數返回其餘函數的內容,請查看 Function Types as Return Types
 
makeIncrementor 函數定義了一個整型變量 runningTotal (初始爲0) 用來存儲當前增長總數。 該值經過 incrementor 返回。
 
makeIncrementor 有一個  Int 類型的參數,其外部命名爲 forIncrement, 內部命名爲 amount,表示每次 incrementor 被調用時 runningTotal 將要增長的量。
 
incrementor 函數用來執行實際的增長操做。 該函數簡單地使 runningTotal 增長 amount,並將其返回。
 
若是咱們單獨看這個函數,會發現看上去不一樣尋常:
func incrementor() -> Int { runningTotal += amount return runningTotal } 

 

incrementor 函數並無獲取任何參數,可是在函數體內訪問了 runningTotal 和 amount 變量。這是由於其經過捕獲在包含它的函數體內已經存在的 runningTotal 和 amount 變量而實現。
 
因爲沒有修改 amount 變量,incrementor 實際上捕獲並存儲了該變量的一個副本,而該副本隨着 incrementor 一同被存儲。
 
然而,由於每次調用該函數的時候都會修改 runningTotal 的值,incrementor 捕獲了當前 runningTotal 變量的引用,而不是僅僅複製該變量的初始值。捕獲一個引用保證了當 makeIncrementor 結束時候並不會消失,也保證了當下一次執行 incrementor 函數時,runningTotal 能夠繼續增長。
注意:
Swift 會決定捕獲引用仍是拷貝值。 您不須要標註 amount 或者 runningTotal 來聲明在嵌入的 incrementor 函數中的使用方式。 Swift 同時也處理 runingTotal 變量的內存管理操做,若是再也不被 incrementor 函數使用,則會被清除。
 
let incrementByTen = makeIncrementor(forIncrement: 10) 

 該例子定義了一個叫作 incrementByTen 的常量,該常量指向一個每次調用會加10的 incrementor 函數。 調用這個函數屢次能夠獲得如下結果:app

incrementByTen() // 返回的值爲10  incrementByTen() // 返回的值爲20  incrementByTen() // 返回的值爲30 

 若是您建立了另外一個 incrementor,其會有一個屬於本身的獨立的 runningTotal 變量的引用。 下面的例子中,incrementBySevne 捕獲了一個新的 runningTotal 變量,該變量和 incrementByTen 中捕獲的變量沒有任何聯繫:函數

 
let incrementBySeven = makeIncrementor(forIncrement: 7) incrementBySeven() // 返回的值爲7  incrementByTen() // 返回的值爲40 

 若是您閉包分配給一個類實例的屬性,而且該閉包經過指向該實例或其成員來捕獲了該實例,您將建立一個在閉包和實例間的強引用環。 Swift 使用捕獲列表來打破這種強引用環優化

 
 
 
閉包是引用類型

上面的例子中,incrementBySeven 和 incrementByTen 是常量,可是這些常量指向的閉包仍然能夠增長其捕獲的變量值。 這是由於函數和閉包都是引用類型。
 
不管您將函數/閉包賦值給一個常量仍是變量,您實際上都是將常量/變量的值設置爲對應函數/閉包的引用。 上面的例子中,incrementByTen 指向閉包的引用是一個常量,而並不是閉包內容自己。
 
這也意味着若是您將閉包賦值給了兩個不一樣的常量/變量,兩個值都會指向同一個閉包:
let alsoIncrementByTen = incrementByTen alsoIncrementByTen() // 返回的值爲50 
相關文章
相關標籤/搜索