本頁包含內容:html
閉包是自包含的函數代碼塊,能夠在代碼中被傳遞和使用。Swift 中的閉包與 C 和 Objective-C 中的代碼塊(blocks)以及其餘一些編程語言中的匿名函數比較類似。git
閉包能夠捕獲和存儲其所在上下文中任意常量和變量的引用。這就是所謂的閉合幷包裹着這些常量和變量,俗稱閉包。Swift 會爲您管理在捕獲過程當中涉及到的全部內存操做。express
注意
若是您不熟悉捕獲(capturing)這個概念也不用擔憂,您能夠在值捕獲章節對其進行詳細瞭解。編程
在函數章節中介紹的全局和嵌套函數實際上也是特殊的閉包,閉包採起以下三種形式之一:swift
Swift 的閉包表達式擁有簡潔的風格,並鼓勵在常見場景中進行語法優化,主要優化以下:api
return
關鍵字嵌套函數是一個在較複雜函數中方便進行命名和定義自包含代碼模塊的方式。固然,有時候撰寫小巧的沒有完整定義和命名的類函數結構也是頗有用處的,尤爲是在您處理一些函數並須要將另一些函數做爲該函數的參數時。數組
閉包表達式是一種利用簡潔語法構建內聯閉包的方式。閉包表達式提供了一些語法優化,使得撰寫閉包變得簡單明瞭。下面閉包表達式的例子經過使用幾回迭代展現了sort(_:)
方法定義和語法優化的方式。每一次迭代都用更簡潔的方式描述了相同的功能。閉包
Swift 標準庫提供了名爲sort
的方法,會根據您提供的用於排序的閉包函數將已知類型數組中的值進行排序。一旦排序完成,sort(_:)
方法會返回一個與原數組大小相同,包含同類型元素且元素已正確排序的新數組。原數組不會被sort(_:)
方法修改。app
下面的閉包表達式示例使用sort(_:)
方法對一個String
類型的數組進行字母逆序排序.如下是初始數組值:異步
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
sort(_:)
方法接受一個閉包,該閉包函數須要傳入與數組元素類型相同的兩個值,並返回一個布爾類型值來代表當排序結束後傳入的第一個參數排在第二個參數前面仍是後面。若是第一個參數值出如今第二個參數值前面,排序閉包函數須要返回true
,反之返回false
。
該例子對一個String
類型的數組進行排序,所以排序閉包函數類型需爲(String, String) -> Bool
。
提供排序閉包函數的一種方式是撰寫一個符合其類型要求的普通函數,並將其做爲sort(_:)
方法的參數傳入:
func backwards(s1: String, s2: String) -> Bool { return s1 > s2 } var reversed = names.sort(backwards) // reversed 爲 ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
若是第一個字符串(s1
)大於第二個字符串(s2
),backwards(_:_:)
函數返回true
,表示在新的數組中s1
應該出如今s2
前。對於字符串中的字符來講,「大於」表示「按照字母順序較晚出現」。這意味着字母"B"
大於字母"A"
,字符串"Tom"
大於字符串"Tim"
。該閉包將進行字母逆序排序,"Barry"
將會排在"Alex"
以前。
然而,這是一個至關冗長的方式,本質上只是寫了一個單表達式函數 (a > b
)。在下面的例子中,利用閉合表達式語法能夠更好地構造一個內聯排序閉包。
閉包表達式語法有以下通常形式:
{ (parameters) -> returnType in statements }
閉包表達式語法可使用常量、變量和inout
類型做爲參數,不能提供默認值。也能夠在參數列表的最後使用可變參數。元組也能夠做爲參數和返回值。
下面的例子展現了以前backwards(_:_:)
函數對應的閉包表達式版本的代碼:
reversed = names.sort({ (s1: String, s2: String) -> Bool in return s1 > s2 })
須要注意的是內聯閉包參數和返回值類型聲明與backwards(_:_:)
函數類型聲明相同。在這兩種方式中,都寫成了(s1: String, s2: String) -> Bool
。然而在內聯閉包表達式中,函數和返回值類型都寫在大括號內,而不是大括號外。
閉包的函數體部分由關鍵字in
引入。該關鍵字表示閉包的參數和返回值類型定義已經完成,閉包函數體即將開始。
因爲這個閉包的函數體部分如此短,以致於能夠將其改寫成一行代碼:
reversed = names.sort( { (s1: String, s2: String) -> Bool in return s1 > s2 } )
該例中sort(_:)
方法的總體調用保持不變,一對圓括號仍然包裹住了方法的整個參數。然而,參數如今變成了內聯閉包。
由於排序閉包函數是做爲sort(_:)
方法的參數傳入的,Swift 能夠推斷其參數和返回值的類型。sort(_:)
方法被一個字符串數組調用,所以其參數必須是(String, String) -> Bool
類型的函數。這意味着(String, String)
和Bool
類型並不須要做爲閉包表達式定義的一部分。由於全部的類型均可以被正確推斷,返回箭頭(->
)和圍繞在參數周圍的括號也能夠被省略:
reversed = names.sort( { s1, s2 in return s1 > s2 } )
實際上任何狀況下,經過內聯閉包表達式構造的閉包做爲參數傳遞給函數或方法時,均可以推斷出閉包的參數和返回值類型。 這意味着閉包做爲函數或者方法的參數時,您幾乎不須要利用完整格式構造內聯閉包。
儘管如此,您仍然能夠明確寫出有着完整格式的閉包。若是完整格式的閉包可以提升代碼的可讀性,則能夠採用完整格式的閉包。而在sort(_:)
方法這個例子裏,閉包的目的就是排序。因爲這個閉包是爲了處理字符串數組的排序,所以讀者可以推測出這個閉包是用於字符串處理的。
單行表達式閉包能夠經過省略return
關鍵字來隱式返回單行表達式的結果,如上版本的例子能夠改寫爲:
reversed = names.sort( { s1, s2 in s1 > s2 } )
在這個例子中,sort(_:)
方法的第二個參數函數類型明確了閉包必須返回一個Bool
類型值。由於閉包函數體只包含了一個單一表達式(s1 > s2
),該表達式返回Bool
類型值,所以這裏沒有歧義,return
關鍵字能夠省略。
Swift 自動爲內聯閉包提供了參數名稱縮寫功能,您能夠直接經過$0
,$1
,$2
來順序調用閉包的參數,以此類推。
若是您在閉包表達式中使用參數名稱縮寫,您能夠在閉包參數列表中省略對其的定義,而且對應參數名稱縮寫的類型會經過函數類型進行推斷。in
關鍵字也一樣能夠被省略,由於此時閉包表達式徹底由閉包函數體構成:
reversed = names.sort( { $0 > $1 } )
在這個例子中,$0
和$1
表示閉包中第一個和第二個String
類型的參數。
實際上還有一種更簡短的方式來撰寫上面例子中的閉包表達式。Swift 的String
類型定義了關於大於號(>
)的字符串實現,其做爲一個函數接受兩個String
類型的參數並返回Bool
類型的值。而這正好與sort(_:)
方法的第二個參數須要的函數類型相符合。所以,您能夠簡單地傳遞一個大於號,Swift 能夠自動推斷出您想使用大於號的字符串函數實現:
reversed = names.sort(>)
更多關於運算符表達式的內容請查看運算符函數。
若是您須要將一個很長的閉包表達式做爲最後一個參數傳遞給函數,可使用尾隨閉包來加強函數的可讀性。尾隨閉包是一個書寫在函數括號以後的閉包表達式,函數支持將其做爲最後一個參數調用:
func someFunctionThatTakesAClosure(closure: () -> Void) { // 函數體部分 } // 如下是不使用尾隨閉包進行函數調用 someFunctionThatTakesAClosure({ // 閉包主體部分 }) // 如下是使用尾隨閉包進行函數調用 someFunctionThatTakesAClosure() { // 閉包主體部分 }
在閉包表達式語法一節中做爲sort(_:)
方法參數的字符串排序閉包能夠改寫爲:
reversed = names.sort() { $0 > $1 }
若是函數只須要閉包表達式一個參數,當您使用尾隨閉包時,您甚至能夠把()
省略掉:
reversed = names.sort { $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(_:)
方法來建立對應的字符串版本數組:
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
參數被聲明爲一個變量參數(變量的具體描述請參看常量參數和變量參數),所以能夠在閉包函數體內對其進行修改,而不用再定義一個新的局部變量並將number
的值賦值給它。閉包表達式指定了返回類型爲String
,以代表存儲映射值的新數組類型爲String
。
閉包表達式在每次被調用的時候建立了一個叫作output
的字符串並返回。其使用求餘運算符(number % 10
)計算最後一位數字並利用digitNames
字典獲取所映射的字符串。
注意
字典digitNames
下標後跟着一個歎號(!
),由於字典下標返回一個可選值(optional value),代表該鍵不存在時會查找失敗。在上例中,因爲能夠肯定number % 10
老是digitNames
字典的有效下標,所以歎號能夠用於強制解包 (force-unwrap) 存儲在下標的可選類型的返回值中的String
類型的值。
從digitNames
字典中獲取的字符串被添加到output
的前部,逆序創建了一個字符串版本的數字。(在表達式number % 10
中,若是number
爲16
,則返回6
,58
返回8
,510
返回0
。)
number
變量以後除以10
。由於其是整數,在計算過程當中未除盡部分被忽略。所以16
變成了1
,58
變成了5
,510
變成了51
。
整個過程重複進行,直到number /= 10
爲0
,這時閉包會將字符串output
返回,而map(_:)
方法則會將字符串添加到所映射的數組中。
在上面的例子中,經過尾隨閉包語法,優雅地在函數後封裝了閉包的具體功能,而再也不須要將整個閉包包裹在map(_:)
方法的括號內。
閉包能夠在其被定義的上下文中捕獲常量或變量。即便定義這些常量和變量的原做用域已經不存在,閉包仍然能夠在閉包函數體內引用和修改這些值。
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
類型的值。關於函數返回其餘函數的內容,請查看函數類型做爲返回類型。
makeIncrementer(forIncrement:)
函數定義了一個初始值爲0
的整型變量runningTotal
,用來存儲當前跑步總數。該值經過incrementor
返回。
makeIncrementer(forIncrement:)
有一個Int
類型的參數,其外部參數名爲forIncrement
,內部參數名爲amount
,該參數表示每次incrementor
被調用時runningTotal
將要增長的量。
嵌套函數incrementor
用來執行實際的增長操做。該函數簡單地使runningTotal
增長amount
,並將其返回。
若是咱們單獨看這個函數,會發現看上去不一樣尋常:
func incrementor() -> Int { runningTotal += amount return runningTotal }
incrementer()
函數並無任何參數,可是在函數體內訪問了runningTotal
和amount
變量。這是由於它從外圍函數捕獲了runningTotal
和amount
變量的引用。捕獲引用保證了runningTotal
和amount
變量在調用完makeIncrementer
後不會消失,而且保證了在下一次執行incrementer
函數時,runningTotal
依舊存在。
注意
爲了優化,若是一個值是不可變的,Swift 可能會改成捕獲並保存一份對值的拷貝。
Swift 也會負責被捕獲變量的全部內存管理工做,包括釋放再也不須要的變量。
下面是一個使用makeIncrementor
的例子:
let incrementByTen = makeIncrementor(forIncrement: 10)
該例子定義了一個叫作incrementByTen
的常量,該常量指向一個每次調用會將runningTotal
變量增長10
的incrementor
函數。調用這個函數屢次能夠獲得如下結果:
incrementByTen()
// 返回的值爲10 incrementByTen() // 返回的值爲20 incrementByTen() // 返回的值爲30
若是您建立了另外一個incrementor
,它會有屬於它本身的一個全新、獨立的runningTotal
變量的引用:
let incrementBySeven = makeIncrementor(forIncrement: 7) incrementBySeven() // 返回的值爲7
再次調用原來的incrementByTen
會在原來的變量runningTotal
上繼續增長值,該變量和incrementBySeven
中捕獲的變量沒有任何聯繫:
incrementByTen()
// 返回的值爲40
注意
若是您將閉包賦值給一個類實例的屬性,而且該閉包經過訪問該實例或其成員而捕獲了該實例,您將建立一個在閉包和該實例間的循環強引用。Swift 使用捕獲列表來打破這種循環強引用。更多信息,請參考閉包引發的循環強引用。
上面的例子中,incrementBySeven
和incrementByTen
是常量,可是這些常量指向的閉包仍然能夠增長其捕獲的變量的值。這是由於函數和閉包都是引用類型。
不管您將函數或閉包賦值給一個常量仍是變量,您實際上都是將常量或變量的值設置爲對應函數或閉包的引用。上面的例子中,指向閉包的引用incrementByTen
是一個常量,而並不是閉包內容自己。
這也意味着若是您將閉包賦值給了兩個不一樣的常量或變量,兩個值都會指向同一個閉包:
let alsoIncrementByTen = incrementByTen alsoIncrementByTen() // 返回的值爲50
當一個閉包做爲參數傳到一個函數中,可是這個閉包在函數返回以後才被執行,咱們稱該閉包從函數中逃逸。當你定義接受閉包做爲參數的函數時,你能夠在參數名以前標註@noescape
,用來指明這個閉包是不容許「逃逸」出這個函數的。將閉包標註@noescape
能使編譯器知道這個閉包的生命週期(譯者注:閉包只能在函數體中被執行,不能脫離函數體執行,因此編譯器明確知道運行時的上下文),從而能夠進行一些比較激進的優化。
func someFunctionWithNoescapeClosure(@noescape closure: () -> Void) { closure() }
舉個例子,sort(_:)
方法接受一個用來進行元素比較的閉包做爲參數。這個參數被標註了@noescape
,由於它確保本身在排序結束以後就沒用了。
一種能使閉包「逃逸」出函數的方法是,將這個閉包保存在一個函數外部定義的變量中。舉個例子,不少啓動異步操做的函數接受一個閉包參數做爲 completion handler。這類函數會在異步操做開始以後馬上返回,可是閉包直到異步操做結束後纔會被調用。在這種狀況下,閉包須要「逃逸」出函數,由於閉包須要在函數返回以後被調用。例如:
var completionHandlers: [() -> Void] = [] func someFunctionWithEscapingClosure(completionHandler: () -> Void) { completionHandlers.append(completionHandler) }
someFunctionWithEscapingClosure(_:)
函數接受一個閉包做爲參數,該閉包被添加到一個函數外定義的數組中。若是你試圖將這個參數標註爲@noescape
,你將會得到一個編譯錯誤。
將閉包標註爲@noescape
使你能在閉包中隱式地引用self
。
class SomeClass { var x = 10 func doSomething() { someFunctionWithEscapingClosure { self.x = 100 } someFunctionWithNoescapeClosure { x = 200 } } } let instance = SomeClass() instance.doSomething() print(instance.x) // prints "200" completionHandlers.first?() print(instance.x) // prints "100"
自動閉包是一種自動建立的閉包,用於包裝傳遞給函數做爲參數的表達式。這種閉包不接受任何參數,當它被調用的時候,會返回被包裝在其中的表達式的值。這種便利語法讓你可以用一個普通的表達式來代替顯式的閉包,從而省略閉包的花括號。
咱們常常會調用一個接受閉包做爲參數的函數,可是不多實現那樣的函數。舉個例子來講,assert(condition:message:file:line:)
函數接受閉包做爲它的condition
參數和message
參數;它的condition
參數僅會在 debug 模式下被求值,它的message
參數僅當condition
參數爲false
時被計算求值。
自動閉包讓你可以延遲求值,由於代碼段不會被執行直到你調用這個閉包。延遲求值對於那些有反作用(Side Effect)和代價昂貴的代碼來講是頗有益處的,由於你能控制代碼何時執行。下面的代碼展現了閉包如何延時求值。
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"] print(customersInLine.count) // prints "5" let customerProvider = { customersInLine.removeAtIndex(0) } print(customersInLine.count) // prints "5" print("Now serving \(customerProvider())!") // prints "Now serving Chris!" print(customersInLine.count) // prints "4"
儘管在閉包的代碼中,customersInLine
的第一個元素被移除了,不過在閉包被調用以前,這個元素是不會被移除的。若是這個閉包永遠不被調用,那麼在閉包裏面的表達式將永遠不會執行,那意味着列表中的元素永遠不會被移除。請注意,customerProvider
的類型不是String
,而是() -> String
,一個沒有參數且返回值爲String
的函數。
將閉包做爲參數傳遞給函數時,你能得到一樣的延時求值行爲。
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"] func serveCustomer(customerProvider: () -> String) { print("Now serving \(customerProvider())!") } serveCustomer( { customersInLine.removeAtIndex(0) } ) // prints "Now serving Alex!"
serveCustomer(_:)
接受一個返回顧客名字的顯式的閉包。下面這個版本的serveCustomer(_:)
完成了相同的操做,不過它並無接受一個顯式的閉包,而是經過將參數標記爲@autoclosure
來接收一個自動閉包。如今你能夠將該函數當作接受String
類型參數的函數來調用。customerProvider
參數將自動轉化爲一個閉包,由於該參數被標記了@autoclosure
特性。
// customersInLine is ["Ewa", "Barry", "Daniella"] func serveCustomer(@autoclosure customerProvider: () -> String) { print("Now serving \(customerProvider())!") } serveCustomer(customersInLine.removeAtIndex(0)) // prints "Now serving Ewa!"
注意
過分使用autoclosures
會讓你的代碼變得難以理解。上下文和函數名應該可以清晰地代表求值是被延遲執行的。
@autoclosure
特性暗含了@noescape
特性,這個特性在非逃逸閉包一節中有描述。若是你想讓這個閉包能夠「逃逸」,則應該使用@autoclosure(escaping)
特性.
// customersInLine is ["Barry", "Daniella"] var customerProviders: [() -> String] = [] func collectCustomerProviders(@autoclosure(escaping) customerProvider: () -> String) { customerProviders.append(customerProvider) } collectCustomerProviders(customersInLine.removeAtIndex(0)) collectCustomerProviders(customersInLine.removeAtIndex(0)) print("Collected \(customerProviders.count) closures.") // prints "Collected 2 closures." for customerProvider in customerProviders { print("Now serving \(customerProvider())!") } // prints "Now serving Barry!" // prints "Now serving Daniella!"
在上面的代碼中,collectCustomerProviders(_:)
函數並無調用傳入的customerProvider
閉包,而是將閉包追加到了customerProviders
數組中。這個數組定義在函數做用域範圍外,這意味着數組內的閉包將會在函數返回以後被調用。所以,customerProvider
參數必須容許「逃逸」出函數做用域。