閉包 (Closures)
自從蘋果2014年發(fā)布Swift,到現(xiàn)在已經(jīng)兩年多了竿裂,而Swift也來到了3.1版本达箍。去年利用工作之余,共花了兩個(gè)多月的時(shí)間把官方的Swift編程指南看完×淳拢現(xiàn)在整理一下筆記,回顧一下以前的知識煮落。有需要的同學(xué)可以去看官方文檔>>敞峭。
閉包是具有一定功能的代碼塊。Swift的閉包類似于C和OC中的block和其他編程語言的匿名函數(shù)蝉仇。
全局和嵌套方法實(shí)際上都是屬于閉包的特殊情況旋讹。閉包有以下三種形式:
- 全局方法:有一個(gè)名字,不會不會捕獲任何值
- 嵌套方法:有一個(gè)名字轿衔,可以從包含這個(gè)嵌套方法的方法內(nèi)部捕獲值
- 閉包語句:沒有名字沉迹,可以從包含它的上下文捕獲值
Swift的閉包語句經(jīng)過了一系列的優(yōu)化變得非常簡單、整潔和清晰:
- 根據(jù)上下文推斷參數(shù)和返回值的類型
- 一個(gè)閉包有隱藏的返回值
- 簡略的參數(shù)名
- 后置閉包語法
閉包表達(dá)式 (Closure Expressions)
排序方法 (Sorted Methods)
Swift標(biāo)準(zhǔn)庫中有一個(gè)方法sorted(by:)
害驹,可以用來對一個(gè)數(shù)組的值排序鞭呕。當(dāng)排序執(zhí)行完成時(shí),返回一個(gè)排好序的數(shù)組宛官,并且不會修改原數(shù)組葫松。sorted(by:)
方法接收一個(gè)具有兩個(gè)與數(shù)組元素類型相同的參數(shù)、并返回一個(gè)布爾值的閉包底洗,閉包的返回值說明了是正序還是倒序腋么。
例如下面這個(gè)例子,對一個(gè)[String]
類型的數(shù)組枷恕,排序閉包需要一個(gè)(String, String) -> Bool
的方法:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reverseNaems = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
然而党晋,上面這種寫法不夠簡潔,我們可以使用閉包表達(dá)式語法來寫徐块。
閉包表達(dá)式語法 (Closure Expression Syntax)
閉包表達(dá)式語法的通用形式如下:
{ (parameters) -> return type in
statements
}
閉包表達(dá)式語法的參數(shù)可以是in-out參數(shù)未玻,但是不能有默認(rèn)值,可以使用可變參數(shù)胡控,多元組也可以作為參數(shù)和返回值扳剿。
backward(_:_:)
的閉包表達(dá)式語法:
reverseNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
根據(jù)上下文推斷類型 (Inferring Type From Context)
因?yàn)榉诸愰]包當(dāng)做參數(shù)被傳入一個(gè)方法,Swift能推斷參數(shù)的類型和返回值的類型昼激。因?yàn)閰?shù)和返回值的類型都能被推斷出來庇绽,所以參數(shù)的類型和返回值類型都可以忽略锡搜,寫成:
reversNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
隱藏Return的單個(gè)表達(dá)式閉包 (Implicit Returns from Single-Expression Closures)
單個(gè)表達(dá)式閉包可以通過刪除return
關(guān)鍵字來隱式地返回單個(gè)表達(dá)式的結(jié)果:
reveresNames = names.sorted(by: s1, s2 in s1 > s2)
簡略參數(shù)名 (Shorthand Argument Names)
Swift可以自動(dòng)提供簡略參數(shù)名給單行閉包,這些簡略參數(shù)名是被用來引用于閉包的參數(shù)瞧掺,例如$0
耕餐、$1
和$2
等等。
如果在閉包中使用這種形式辟狈,可以在定義包時(shí)把參數(shù)省略肠缔,in
關(guān)鍵字也可以省略:
reverseNames = names.sorted(by: { $0 > $1 })
$0
和$1
是閉包的第一和第二個(gè)參數(shù)。
運(yùn)算符方法 (Operator Methods)
其實(shí)上面的閉包還可以用更簡短的方式來實(shí)現(xiàn)哼转。Swift的String
類型把>
定義為一個(gè)具有兩個(gè)String
類型并返回布爾值得方法明未。這剛好符合sorted(by:)
方法需要的閉包參數(shù)。所以上面的例子可以簡寫成:
reverseNames = names.sorted(by: >)
后置閉包 (Trailing Closures)
如果我們需要傳入一個(gè)很長的閉包作為參數(shù)壹蔓,并且這個(gè)參數(shù)是最后一個(gè)參數(shù)趟妥,后置閉包是非常有用的。當(dāng)使用后置閉包語法時(shí)佣蓉,不需要寫參數(shù)的標(biāo)簽:
func someFunctionThatTakesAClosure(closure: () -> Void) {
// function body goes here
}
// 調(diào)用時(shí)沒有使用后置閉包
someFunctionThatTakesAClosure(closure: {
// closure's body goes here
})
// 調(diào)用時(shí)使用后置閉包
someFunctionThatTakesAClosure() {
// trailing closure's body goes here
}
上面提到的把名字倒序排列的例子中披摄,可以使用后置閉包語法寫成:
reverseNames = names.sorted() { $0 > $1 }
如果方法的參數(shù)中只有一個(gè)方法類型的參數(shù),在使用后置閉包語法時(shí)勇凭,可以把()
省略:
reverseNames = names.sorted { $0 > $1 }
當(dāng)閉包非常長而且不能用一行代碼寫完時(shí)行疏,后置閉包是非常有用的。例如套像,Swift的Array
類型有一個(gè)map(_:)
方法,這個(gè)方法需要一個(gè)閉包表達(dá)式作為它唯一的參數(shù)终息。這個(gè)閉包會被數(shù)組中的每個(gè)元素調(diào)用一次夺巩,并返回一個(gè)與元素相關(guān)的值。map(_:)
方法執(zhí)行完后周崭,會返回一個(gè)新的數(shù)組柳譬,數(shù)組包含著所有與各個(gè)元素對應(yīng)的值,并且順序與原素組相同续镇。
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]
let strings = numbers.map { (number) -> String in
var number = number
var output = ""
repeat {
output = digitName[number % 10]! + output
number /= 10
} while number > 0
}
// strings is inferred to be of type [String]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]
map(_:)
方法會讓數(shù)組的每一個(gè)元素調(diào)用一次閉包美澳。在閉包中,我們無需指定參數(shù)number
的類型摸航,因?yàn)?code>number的類型可以從數(shù)組的元素類型中推斷出來制跟。
number
變量使用閉包的number
參數(shù)參數(shù)來初始化,以保證閉包后面的代碼中能修改number
的值酱虎,但是閉包參數(shù)number
還是屬于常量雨膨。
捕獲值 (Capturing Values)
閉包可以從包含這個(gè)閉包的方法中捕獲這個(gè)方法內(nèi)部定義的常量或變量,然后閉包可以修改這些常量或者變量读串,即使這些常量和變量不再存在聊记。
在Swift中撒妈,最簡單的能捕獲值的閉包形式就是嵌套方法。一個(gè)嵌套方法可以捕獲包含嵌套方法的方法參數(shù)和內(nèi)部定義的常量或者變量排监。
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
incrementer()
方法沒有參數(shù)狰右,從包含它的方法中引用runningTotal
和amount
,這種引用是通過捕獲對runningTotal
和amount
的指針實(shí)現(xiàn)的舆床。通過捕獲指針保證了runningTotal
和amount
在makeIncrementer
執(zhí)行完之后不會消失棋蚌,并且在下一次調(diào)用makeIncrementer
時(shí)還可以使用。
注意:因?yàn)閮?yōu)化峭弟,如果一個(gè)值在閉包中沒有被修改附鸽,Swift會捕獲和存儲這個(gè)值的副本。Swift還會處理內(nèi)存管理問題瞒瘸,當(dāng)這些變量不再使用時(shí)坷备,Swift會把他們銷毀。
下面是使用makeIncrementer
的一個(gè)例子:
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30
如果創(chuàng)建第二個(gè)incrementer情臭,它會有自己的一個(gè)對runningTotal
的引用:
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7
然后再調(diào)用之前的incrementByTen
省撑,runningTotal
的值會繼續(xù)往上增加,并且不會對incrementBySeven
引用的runningTotal
造成影響俯在。
注意:如果把一個(gè)閉包賦值給一個(gè)類對實(shí)例的屬性竟秫,這個(gè)閉包又通過引用這個(gè)類對象的實(shí)例或者成員來捕獲值,這將會造成在閉包和類實(shí)例之間的循環(huán)引用跷乐。
閉包是引用類型 (Closures Are Reference Types)
在上面的例子中肥败,incrementBySeve
和incrementByTen
都是常量,但是這兩個(gè)常量引用的閉包還可以讓runningTotal
繼續(xù)增加愕提。這是因?yàn)榉椒ê烷]包都是引用類型馒稍。
不管在什么時(shí)候,把方法和閉包賦值給變量和常量浅侨,實(shí)際上常量或者變量指向了方法和閉包纽谒。那么這就意味著如果把一個(gè)閉包賦值給不同的變量或者常量,這些常量和變量實(shí)際上是引用這同一個(gè)閉包:
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen
// returns a value of 50
逃逸閉包 (Escaping Closures)
當(dāng)一個(gè)閉包作為參數(shù)傳給一個(gè)方法時(shí)如输,但是在方法返回之后才調(diào)用鼓黔,那么這個(gè)閉包被稱為逃逸閉包。在聲明方法時(shí)不见,在方法參數(shù)類型之前使用@escaping
來說明這個(gè)閉包允許“逃脫”澳化。
一個(gè)閉包能“逃脫”的一種方式,就是把這個(gè)閉包存儲在方法之外定義的變量中稳吮。例如肆捕,很多方法開啟一個(gè)異步操作,并用一個(gè)閉包參數(shù)作為一個(gè)completion handler盖高。在異步操作開始之后慎陵,馬上返回眼虱。但是閉包在異步操作完成之前并不會調(diào)用,閉包需要“逃脫”席纽,在后面調(diào)用:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
在這個(gè)方法中捏悬,如果不加上@escaping
,將會編譯錯(cuò)誤润梯。
使用@escaping
標(biāo)記一個(gè)閉包过牙,意味著在閉包中需要明確的引用self
。
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"
completionHandlers.first?()
print(instance.x)
// Prints "100"
自動(dòng)閉包 (Autoclosures)
一個(gè)閉包自動(dòng)創(chuàng)建并被包裝成一個(gè)表達(dá)式纺铭,然后作為參數(shù)傳入方法寇钉,這個(gè)閉包就被稱為自動(dòng)閉包。
一個(gè)自動(dòng)閉包可以延遲執(zhí)行舶赔,因?yàn)殚]包中的代碼在被調(diào)用之前并不會執(zhí)行扫倡。延遲執(zhí)行在一些有副作用或者計(jì)算昂貴的代碼中非常有用,因?yàn)槲覀兛梢钥刂拼a何時(shí)執(zhí)行:
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Prints "5"
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"
即使customersInLine
的第一個(gè)元素在閉包中被刪除了竟纳,但是這個(gè)元素在閉包執(zhí)行之前并不會被刪除撵溃。注意: customerProvider
不是String
類型,而是() -> String
锥累。
把一個(gè)閉包作為參數(shù)傳給方法也是一樣:
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"
使用@autoclosure
:
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"
注意:過度使用自動(dòng)閉包會降低代碼的可讀性缘挑。
如果一個(gè)自動(dòng)閉包想要“逃脫”,同時(shí)使用@autoclosure
和@escaping
桶略。
// customersInLine is ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 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!"
第七部分完语淘。下個(gè)部分:【Swift 3.1】08 - 枚舉 (Enumerations)
如果有錯(cuò)誤的地方,歡迎指正际歼!謝謝亏娜!