Swift學習:閉包

本篇將詳細總結介紹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函數時,傳入的閉包是表達式的形式,自動閉包省略了閉包花括號

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容