本篇將詳細總結介紹Swift閉包的用法;
閉包是自包含的函數代碼塊,可以在代碼中被傳遞和使用。Swift中的閉包與C和 Objective-C中的代碼塊(blocks)以及其他一些編程語言中的匿名函數比較相似。
主要內容:
1.閉包表達式
2.閉包的使用與優化
3.值捕獲
4.逃逸閉包
5.自動閉包
一、閉包表達式
Swift閉包的三種存在形式:
1.全局函數是一個有名字但不會捕獲任何值的閉包
2.嵌套函數是一個有名字并可以捕獲其封閉函數域內值的閉包
3.閉包表達式是一個利用輕量級語法所寫的可以捕獲其上下文中變量或常量值的匿名閉包
閉包表達式的語法一般有如下的一般形式:
{ (parameters) -> returnType in
statements
}
說明:
1.閉包的外層是一個大括號,先寫的參數和返回值,然后操作部分之前使用in;
2.閉包就相當于OC中的block, 也可以看做是匿名函數;
3.閉包表達式參數可以是in-out參數,但不能設定默認值;
4.閉包的函數體部分由關鍵字in引入,該關鍵字表示閉包參數和返回值類型已經完成,閉包函數體開始;
二、閉包的使用與優化
下面,我們使用Swift標準庫中的sorted(by:)方法來測試閉包的使用。sorted(by:)方法允許外部傳入一個用于排序的閉包函數將已知類型數組中的值進行排序,完成排序之后,該方法會返回一個與原數組大小相同,包含同類型元素已正確排序的新數組:
//定義一個整型數組
var someInts: [Int] = [5,9,7,0,1,3]
//定義一個排序函數
func biggerNumFirst(num1:Int, num2:Int) -> Bool{
return num1 > num2
}
//普通用法:將biggerNumFirst函數傳入sorted函數,實現排序
var sortInts = someInts.sorted(by: biggerNumFirst)
print(sortInts) //[9, 7, 5, 3, 1, 0]
//閉包用法:為sorted函數參數傳入一個閉包,實現排序
sortInts = someInts.sorted(by:{ (a:Int, b:Int) -> Bool in
return a > b
})
print(sortInts) //[9, 7, 5, 3, 1, 0]
注意:因為閉包不會在其他地方調用,所以不使用外部參數名
閉包使用起來十分靈活,我們可以在某些特定情況下對齊進行優化,下面是對上述閉包的優化:
2.1.根據上下文推斷類型,省略參數類型與括號
由于排序閉包函數是作為sorted(by:)方法的參數傳入的,Swift可以推斷其類型和返回值類型。所以sorted(by:)方法被一個Int類型的數組調用,其參數必定是(Int,Int)->Bool類型的函數。最后,根據上下文推斷類型,我們可以省略參數類型和參數周圍的括號。
sortInts = someInts.sorted(by: {a,b in
return a > b
})
print(sortInts)
2.2.對于不會發生歧義的閉包,可將其寫成一行
sortInts = someInts.sorted(by:{a,b in return a > b})
print(sortInts)
2.3.單行閉包表達式,省略return關鍵字
省略return關鍵字的條件:
sorted(by:)方法的參數類型明確了閉包必須返回一個Bool類型值
單行閉包表達式中,其返回值類型沒有歧義
sortInts = someInts.sorted(by: {a,b in a > b})
print(sortInts)
2.4.使用參數名縮寫(不推薦使用)
Swift 自動為內聯閉包提供了參數名稱縮寫功能,你可以直接通過$0,$1,$2 來順序調用閉包的參數,以此類推。
如果我們在閉包表達式中使用參數名稱縮寫, 我們就可以在閉包定義中省略參數列表,并且對應參數名稱縮寫的類型會通過函數類型進行推斷。in關鍵字也同樣可以被省略,因為此時閉包表達式完全由閉包函數體構成:
sortInts = someInts.sorted(by: {$0>$1})
print(sortInts)
2.5.使用運算符簡化閉包(不推薦使用)
Swift的Int類型定義了關于大于號(>)的字符串實現,其作為一個函數接受兩個Int類型的參數并返回Bool類型的值。而這正好與sorted(by:)方法的參數需要的函數類型相符合。可以使用大于號來代替閉包
sortInts = someInts.sorted(by: >)
print(sortInts)
2.6.尾隨閉包,解決長閉包的書寫問題
如果你需要將一個很長的閉包表達式作為最后一個參數傳遞給函數,可以使用尾隨閉包來增強函數的可讀性。
尾隨閉包的寫法:將閉包書寫在函數括號之后,函數會支持將其作為最后一個參數調用,使用尾隨閉包,不需要寫出它的參數標簽。
func someFunctionThatTakesAClosure(closure: () -> Void) {
//函數體部分
closure(); //調用閉包
}
//不使用尾隨閉包進行函數調用
someFunctionThatTakesAClosure(closure: {
//閉包主體部分
})
//使用尾隨閉包進行函數調用
someFunctionThatTakesAClosure() {
//閉包主體部分
}
//注意:如果閉包表達式是函數或方法的唯一參數,則當你使用尾隨閉包時,你甚至可以把 () 省略掉:
someFunctionThatTakesAClosure {
print("Hello World!") //打印:Hello World!
}
總結Swift閉包主要的四種優化方法:
1.利用上下文推斷參數和返回值類型,省略參數類型與括號
2.隱式返回單表達式閉包,即單表達式閉包可以省略return關鍵字
3.參數名稱縮寫
4.尾隨閉包語法
三、值捕獲
閉包可以在其被定義的上下文中捕獲常量或變量。即使定義這些常量和變量的原作用域已經不存在,閉包仍然可以在閉包函數體內引用和修改這些值。Swift會為你管理在捕獲過程中涉及到的所有內存操作。
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
代碼分析:
1.makeIncrementer函數以amount為參數,以()->Int作為返回值類型,其函數體中還嵌套了另一個函數incrementer。
2.如果我們把incrementer單獨拿出來,會發現其中runingTotal和amount變量都無法使用,因為這兩個變量的引用是incrementer從外部捕獲的。
3.Swift會負責被捕獲變量的所有內存管理工作,包括對捕獲的一份值拷貝,也包括釋放不再需要的變量。
現在再來測試makeIncrementer函數的使用:
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen(); //10
incrementByTen(); //20
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven() //7
incrementBySeven(); //14
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen() //30
代碼分析:
1.incrementByTen與incrementBySeven,是通過makeIncrementer函數傳入不同的增量參數amount而創建的;
2.兩個函數都有屬于各自的引用,其中的runningTotal變量都是從makeIncrementer中捕獲的,但是已經各自沒有關系;
3.函數和閉包都是引用類型,將其賦值給變量或者常量,都只是操作的它們的引用,而不會改變閉包或者函數本身;
四、逃逸閉包
當一個閉包作為參數傳到一個函數中,但是這個閉包在函數返回之后才被執行,我們稱該閉包從函數中逃逸。
逃逸閉包:在定義接受閉包作為參數的函數時,我們需要在參數名之前標注@escaping,以此表明這個閉包是允許"逃逸"出這個函數的。
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
//代碼1:執行閉包,不需要添加@escaping
//completionHandler();
//代碼2:函數外部對閉包進行了操作
completionHandlers.append(completionHandler)
}
代碼分析:
someFunctionWithEscapingClosure(_:) 函數接受一個閉包作為參數,該閉包被添加到一個函數外定義的數組中。如果不將這個參數標記為@escaping,就會得到一個編譯錯誤。
4.1.逃逸閉包的使用
逃逸閉包和非逃逸閉包在使用上有所不同。將一個閉包標記為@escaping(即逃逸閉包)后,在調用這個閉包時就必須在閉包中顯式地引用 self。一個示例如下:
//定義一個帶有非逃逸閉包參數的函數
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
//定義一個可以使用閉包的類
class SomeClass {
var x = 10
func doSomething() {
//調用逃逸閉包:必須在閉包中顯式引用self
someFunctionWithEscapingClosure { self.x = 100 }
//調用非逃逸閉包:可以隱式引用self
someFunctionWithNonescapingClosure { x = 200 }
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x) //打印出 "200”
五、自動閉包
自動閉包:一種自動創建的閉包,用與包裝傳遞給函數作為參數的表達式;自動閉包的特點:
1.自動閉包不接受任何參數;
2.自動閉包被調用的時候,會返回被包裝在其中的表達式的值;
3.自動閉包是用一個普通的表達式來代替顯式的閉包,能夠省略閉包的花括號;
其實,我們經常調用采用自動閉包的函數,但是卻少去實現這樣的函數,assert函數就是其中之一:
assert(condition:, message:)
assert函數中的condition參數可以接受自動閉包作為值,condition參數僅會在debug模式下被求值,在condidtion被調用返回值為false時,message參數將被使用。
5.1.自動閉包的基本使用
自動閉包能夠實現延遲求值,直到調用這個閉包時,代碼才會被執行。這對于有副作用或者高計算成本的代碼來說是有益處的;下面的代碼展示了自動閉包實現延時求值的具體做法:
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count) //打印出 “5"
//自動閉包不接受參數,只是一個表達式
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count) //打印出 “5"
//調用自動閉包
print("Now serving \(customerProvider())!") // Prints "Now serving Chris!"
print(customersInLine.count) //打印出 "4”
代碼分析:閉包實現了移除第一元素的功能,但是在閉包被調用之前,這個元素是不會被移除的。這就實現了延遲的作用
5.2.自動閉包在函數中的使用
現在將閉包作為參數傳遞給一個函數,同樣可以實現延時求值行為。下面的serve函數接受了一個閉包參數(具有刪除第一個元素且返回這個元素的功能)。
//customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
//以閉包的形式傳入參數
serve(customer: { customersInLine.remove(at: 0) } ) //打印出"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)) //打印出"Now serving Ewa!\n"
注意:
過度使用 autoclosures 會讓你的代碼變得難以理解。上下文和函數名應該能夠清晰地表明求值是被延遲執行的。
5.3.可"逃逸"的自動閉包
一個自動閉包可以“逃逸”,這時候應該同時使用 @autoclosure 和 @escaping 屬性,下面舉例說明:
// customersInLine is ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
customerProviders.append(customerProvider)
}
//調用collectCustomerProviders,向數組中追加閉包
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))
print("Collected \(customerProviders.count) closures.") //打印 "Collected 2 closures."
//循環數組中閉包,并且執行
for customerProvider in customerProviders {
print("Now serving \(customerProvider())!")
}
// 打印 "Now serving Barry!"
// 打印 "Now serving Daniella!”
代碼分析:
作為逃逸閉包:
collectCustomerProviders函數中,閉包customerProvider被追加到customerProviders中,而這個數據是定義在函數作用域范圍之外的,這意味數組內的閉包能夠在函數返回之后被調用,所以customerProvider必須允許
"逃逸"出函數作用域。
作為自動閉包:
調用collectCustomerProviders函數時,傳入的閉包是表達式的形式,自動閉包省略了閉包花括號