Swift編程語言學習6—— 閉包
閉包是自包括的函數代碼塊,能夠在代碼中被傳遞和使用。
Swift 中的閉包與 C 和 Objective-C 中的代碼塊(blocks)以及其它一些編程語言中的 lambdas 函數比較類似。
?
閉包能夠捕獲和存儲其所在上下文中隨意常量和變量的引用。這就是所謂的閉合並包裹著這些常量和變量。俗稱閉包。Swift 會為您管理在捕獲過程中涉及到的全部內存操作。
?
註意:
?
假設您不熟悉捕獲(capturing)這個概念也不用操心。您能夠在值捕獲 章節對其進行詳細了解。
在函數章節中介紹的全局和嵌套函數實際上也是特殊的閉包。閉包採取例如以下三種形式之中的一個:?
全局函數是一個有名字但不會捕獲不論什麽值的閉包
嵌套函數是一個有名字並能夠捕獲其封閉函數域內值的閉包
閉包表達式是一個利用輕量級語法所寫的能夠捕獲其上下文中變量或常量值的匿名閉包
Swift 的閉包表達式擁有簡潔的風格。並鼓舞在常見場景中進行語法優化,主要優化例如以下:
?
利用上下文判斷參數和返回值類型
隱式返回單表達式閉包,即單表達式閉包能夠省略returnkeyword
參數名稱縮寫
跟隨(Trailing)閉包語法
?
閉包表達式(Closure Expressions)
嵌套函數是一個在較復雜函數中方便進行命名和定義自包括代碼模塊的方式。當然,有時候撰寫小巧的沒有完整定義和命名的類函數結構也是非常實用處的。尤其是在您處理一些函數並須要將另外一些函數作為該函數的參數時。
?
閉包表達式是一種利用簡潔語法構建內聯閉包的方式。閉包表達式提供了一些語法優化。使得撰寫閉包變得簡單明了。
以下閉包表達式的樣例通過使用幾次叠代展示了sort函數定義和語法優化的方式。
每一次叠代都用更簡潔的方式描寫敘述了同樣的功能。
?
?
sort 函數(The Sort Function)
Swift 標準庫提供了sort函數,會依據您提供的基於輸出類型排序的閉包函數將已知類型數組中的值進行排序。一旦排序完畢,函數會返回一個與原數組大小同樣的新數組,該數組中包括已經正確排序的同類型元素。
?
以下的閉包表達式演示樣例使用sort函數對一個String類型的數組進行字母逆序排序,以下是初始數組值:
?
let names = ["Chris","Alex", "Ewa", "Barry", "Daniella"]
sort函數須要傳入兩個參數:
?
已知類型的數組
閉包函數,該閉包函數須要傳入與數組類型同樣的兩個值。並返回一個布爾類型值來告訴sort函數當排序結束後傳入的第一個參數排在第二個參數前面還是後面。假設第一個參數值出如今第二個參數值前面。排序閉包函數須要返回true。反之返回false。
該樣例對一個String類型的數組進行排序,因此排序閉包函數類型需為(String, String) -> Bool。
?
提供排序閉包函數的一種方式是撰寫一個符合其類型要求的普通函數,並將其作為sort函數的第二個參數傳入:
?
func backwards(s1: String, s2: String)-> Bool {
return s1 > s2
}
var reversed = sort(names, backwards)
// reversed 為 ["Ewa","Daniella", "Chris", "Barry", "Alex"]
假設第一個字符串 (s1) 大於第二個字符串 (s2),backwards函數返回true。表示在新的數組中s1應該出如今s2前。
對於字符串中的字符來說。“大於”表示 “依照字母順序較晚出現”。 這意味著字母"B"大於字母"A",字符串"Tom"大於字符串"Tim"。
其將進行字母逆序排序,"Barry"將會排在"Alex"之後。
?
然而,這是一個相當冗長的方式,本質上僅僅是寫了一個單表達式函數 (a > b)。
在以下的樣例中。利用閉合表達式語法能夠更好的構造一個內聯排序閉包。
?
?
閉包表達式語法(Closure Expression Syntax)
閉包表達式語法有例如以下一般形式:
?
{ (parameters) -> returnType in
statements
}
閉包表達式語法能夠使用常量、變量和inout類型作為參數。不提供默認值。也能夠在參數列表的最後使用可變參數。
元組也能夠作為參數和返回值。
?
以下的樣例展示了之前backwards函數相應的閉包表達式版本號的代碼:
?
reversed = sort(names, { (s1: String, s2:String) -> Bool in
return s1 > s2
})
須要註意的是內聯閉包參數和返回值類型聲明與backwards函數類型聲明同樣。
在這兩種方式中。都寫成了(s1: String, s2: String) -> Bool。 然而在內聯閉包表達式中。函數和返回值類型都寫在大括號內,而不是大括號外。
?
閉包的函數體部分由keywordin引入。該keyword表示閉包的參數和返回值類型定義已經完畢,閉包函數體即將開始。
?
由於這個閉包的函數體部分如此短以至於能夠將其改寫成一行代碼:
?
reversed = sort(names, { (s1: String, s2:String) -> Bool in return s1 > s2 } )
這說明sort函數的總體調用保持不變。一對圓括號仍然包裹住了函數中整個參數集合。而當中一個參數如今變成了內聯閉包(相比於backwards版本號的代碼)。
?
?
依據上下文判斷類型(Inferring Type From Context)
由於排序閉包函數是作為sort函數的參數進行傳入的,Swift能夠判斷其參數和返回值的類型。 sort期望第二個參數是類型為(String, String) -> Bool的函數。因此實際上String,String和Bool類型並不須要作為閉包表達式定義中的一部分。
由於全部的類型都能夠被正確判斷,返回箭頭 (->) 和環繞在參數周圍的括號也能夠被省略:
?
reversed = sort(names, { s1, s2 in returns1 > s2 } )
實際上不論什麽情況下。通過內聯閉包表達式構造的閉包作為參數傳遞給函數時,都能夠判斷出閉包的參數和返回值類型。這意味著您差點兒不須要利用完整格式構造不論什麽內聯閉包。
?
?
單表達式閉包隱式返回(Implicit Return From Single-Expression Clossures)
單行表達式閉包能夠通過隱藏returnkeyword來隱式返回單行表達式的結果,如上版本號的樣例能夠改寫為:
?
reversed = sort(names, { s1, s2 in s1 >s2 } )
在這個樣例中,sort函數的第二個參數函數類型明白了閉包必須返回一個Bool類型值。由於閉包函數體僅僅包括了一個單一表達式 (s1 > s2),該表達式返回Bool類型值,因此這裏沒有歧義,returnkeyword能夠省略。
?
?
參數名稱縮寫(Shorthand Argument Names)
Swift 自己主動為內聯函數提供了參數名稱縮寫功能,您能夠直接通過$0,$1,$2來順序調用閉包的參數。
?
假設您在閉包表達式中使用參數名稱縮寫,您能夠在閉包參數列表中省略對其的定義,而且相應參數名稱縮寫的類型會通過函數類型進行判斷。 inkeyword也同樣能夠被省略,由於此時閉包表達式全然由閉包函數體構成:
?
reversed = sort(names, { $0 > $1 } )
在這個樣例中。$0和$1表示閉包中第一個和第二個String類型的參數。
?
?
運算符函數(Operator Functions)
實際上另一種更簡短的方式來撰寫上面樣例中的閉包表達式。 Swift 的String類型定義了關於大於號 (>) 的字符串實現。其作為一個函數接受兩個String類型的參數並返回Bool類型的值。
而這正好與sort函數的第二個參數須要的函數類型相符合。
因此,您能夠簡單地傳遞一個大於號,Swift能夠自己主動判斷出您想使用大於號的字符串函數實現:
?
reversed = sort(names, >)
很多其它關於運算符表達式的內容請查看運算符函數。
?
?
跟隨閉包(Trailing Closures)
假設您須要將一個非常長的閉包表達式作為最後一個參數傳遞給函數。能夠使用跟隨閉包來增強函數的可讀性。跟隨閉包是一個書寫在函數括號之後的閉包表達式,函數支持將其作為最後一個參數調用。
?
func someFunctionThatTakesAClosure(closure:() -> ()) {
// 函數體部分
}
// 以下是不使用跟隨閉包進行函數調用
someFunctionThatTakesAClosure({
// 閉包主體部分
})
// 以下是使用跟隨閉包進行函數調用
someFunctionThatTakesAClosure() {
// 閉包主體部分
}
註意:
?
假設函數僅僅須要閉包表達式一個參數,當您使用跟隨閉包時,您甚至能夠把()省略掉。
在上例中作為sort函數參數的字符串排序閉包能夠改寫為:
?
reversed = sort(names) { $0 > $1 }
當閉包非常長以至於不能在一行中進行書寫時,跟隨閉包變得非常實用。
舉例來說。Swift 的Array類型有一個map方法,其獲取一個閉包表達式作為其唯一參數。
數組中的每個元素調用一次該閉包函數,並返回該元素所映射的值(也能夠是不同類型的值)。詳細的映射方式和返回值類型由閉包來指定。
?
當提供給數組閉包函數後,map方法將返回一個新的數組,數組中包括了與原數組一一相應的映射後的值。
?
下例介紹了怎樣在map方法中使用跟隨閉包將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]
如上代碼創建了一個數字位和他們名字映射的英文版本號字典。同一時候定義了一個準備轉換為字符串的整型數組。
?
您如今能夠通過傳遞一個跟隨閉包給numbers的map方法來創建相應的字符串版本號數組。須要註意的時調用numbers.map不須要在map後面包括不論什麽括號,由於其僅僅須要傳遞閉包表達式這一個參數,而且該閉包表達式參數通過跟隨方式進行撰寫:
?
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函數則會將字符串加入到所映射的數組中。
?
上例中跟隨閉包語法在函數後整潔封裝了詳細的閉包功能,而不再須要將整個閉包包裹在map函數的括號內。
?
?
捕獲值(Capturing Values)
閉包能夠在其定義的上下文中捕獲常量或變量。
即使定義這些常量和變量的原域已經不存在。閉包仍然能夠在閉包函數體內引用和改動這些值。
?
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類型的值。關於函數返回其它函數的內容,請查看函數類型作為返回類型。
?
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函數使用。則會被清除。
以下代碼為一個使用makeIncrementor的樣例:
?
let incrementByTen =makeIncrementor(forIncrement: 10)
該樣例定義了一個叫做incrementByTen的常量,該常量指向一個每次調用會加10的incrementor函數。調用這個函數多次能夠得到以下結果:
?
incrementByTen()
// 返回的值為10
incrementByTen()
// 返回的值為20
incrementByTen()
// 返回的值為30
假設您創建了另一個incrementor。其會有一個屬於自己的獨立的runningTotal變量的引用。以下的樣例中。incrementBySevne捕獲了一個新的runningTotal變量,該變量和incrementByTen中捕獲的變量沒有不論什麽聯系:
?
let incrementBySeven =makeIncrementor(forIncrement: 7)
incrementBySeven()
// 返回的值為7
incrementByTen()
// 返回的值為40
註意:
?
假設您閉包分配給一個類實例的屬性。而且該閉包通過指向該實例或其成員來捕獲了該實例。您將創建一個在閉包和實例間的強引用環。 Swift 使用捕獲列表來打破這樣的強引用環。很多其它信息。請參考閉包引起的循環強引用。
?
閉包是引用類型(Closures Are Reference Types)
上面的樣例中,incrementBySeven和incrementByTen是常量,可是這些常量指向的閉包仍然能夠添加其捕獲的變量值。
這是由於函數和閉包都是引用類型。
?
不管您將函數/閉包賦值給一個常量還是變量。您實際上都是將常量/變量的值設置為相應函數/閉包的引用。
上面的樣例中,incrementByTen指向閉包的引用是一個常量,而並不是閉包內容本身。
?
這也意味著假設您將閉包賦值給了兩個不同的常量/變量。兩個值都會指向同一個閉包:
?
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 返回的值為50
Swift編程語言學習6—— 閉包