【Swift 3.1】07 - 閉包 (Closures)

閉包 (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ù)狰右,從包含它的方法中引用runningTotalamount,這種引用是通過捕獲對runningTotalamount的指針實(shí)現(xiàn)的舆床。通過捕獲指針保證了runningTotalamountmakeIncrementer執(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)

在上面的例子中肥败,incrementBySeveincrementByTen都是常量,但是這兩個(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ò)誤的地方,歡迎指正际歼!謝謝亏娜!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蹬挺,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌它掂,老刑警劉巖巴帮,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異虐秋,居然都是意外死亡榕茧,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進(jìn)店門客给,熙熙樓的掌柜王于貴愁眉苦臉地迎上來用押,“玉大人,你說我怎么就攤上這事靶剑◎卟Γ” “怎么了池充?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長缎讼。 經(jīng)常有香客問我收夸,道長,這世上最難降的妖魔是什么血崭? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任卧惜,我火速辦了婚禮,結(jié)果婚禮上夹纫,老公的妹妹穿的比我還像新娘咽瓷。我一直安慰自己,他們只是感情好舰讹,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布茅姜。 她就那樣靜靜地躺著,像睡著了一般跺涤。 火紅的嫁衣襯著肌膚如雪匈睁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天桶错,我揣著相機(jī)與錄音航唆,去河邊找鬼。 笑死院刁,一個(gè)胖子當(dāng)著我的面吹牛糯钙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播退腥,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼任岸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了狡刘?” 一聲冷哼從身側(cè)響起享潜,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤疯趟,失蹤者是張志新(化名)和其女友劉穎匈织,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體澜驮,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡澜术,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年艺蝴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸟废。...
    茶點(diǎn)故事閱讀 38,646評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡猜敢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情缩擂,我是刑警寧澤鼠冕,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站撇叁,受9級特大地震影響供鸠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜陨闹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一楞捂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧趋厉,春花似錦寨闹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至乡数,卻和暖如春椭蹄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背净赴。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工绳矩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人玖翅。 一個(gè)月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓翼馆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親金度。 傳聞我的和親對象是個(gè)殘疾皇子应媚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評論 2 348

推薦閱讀更多精彩內(nèi)容