Swift閉包

閉包

  • 閉包是自包含的函數(shù)代碼塊,可以在代碼中被傳遞和使用。Swift中的閉包與CObjective-C中的代碼塊(blocks)以及其他一些編程語言中的匿名函數(shù)比較相似。
  • 閉包可以捕獲和存儲其所在上下文中任意常量和變量的引用,被稱為包裹常量和變量。 Swift會為你管理在捕獲過程中涉及到的所有內(nèi)存操作。
  • 也可以說閉包是一個捕獲了上下文的常量和變量的函數(shù)。
  • 閉包的表現(xiàn)形式:
    • 1,全局函數(shù)是一個有名字但不會捕獲任何值的閉包
    • 2,嵌套函數(shù)是一個有名字并可以捕獲其封閉函數(shù)域內(nèi)值的閉包
    • 3,閉包表達式是一個利用輕量級語法所寫的可以捕獲其上下文中變量或常量值的匿名閉包

閉包表達式

  • 完整的閉包表達式要具備:
    • 作用域{}
    • 參數(shù)和返回值
    • 函數(shù)體-(in)之后的代碼
{ (parameters) -> return type in
    statements
}
  • Swift中的閉包可以當做變量,也可以當做參數(shù)傳遞,也可以將它聲明為一個可選類型,還可以通過let聲明為一個常量,也可以作為函數(shù)的參數(shù)使用
// 聲明為一個變量
var closure : (Int) -> Int = { (age: Int) in
    return age
}

// 聲明為一個可選項
// 錯誤寫法
var closure : (Int) -> Int?
closure = nil
// 正確寫法
var closure : ((Int) -> Int)?
closure = nil

// 聲明為一個常量
let closure: (Int) -> Int

closure = {(age: Int) in
     return age
}

// 閉包作為參數(shù)
func test(param : () -> Int){
     print(param())
}
var age = 10
test { () -> Int in
    age += 1
    return age
}
  • 閉包表達式是一種利用簡潔語法構(gòu)建內(nèi)聯(lián)閉包的方式。閉包表達式提供了一些語法優(yōu)化,使得撰寫閉包變得簡單明了。下面閉包表達式的例子通過使用幾次迭代展示了sorted(by:)方法定義和語法優(yōu)化的方式。每一次迭代都用更簡潔的方式描述了相同的功能。
var array = [4, 2, 3]
array.sort(by: {(item1 : Int, item2: Int) -> Bool in return item1
< item2 })
??
array.sort{(item1 : Int, item2: Int) -> Bool in return item1 < item2 }
??
array.sort(by: {(item1, item2) -> Bool in return item1 < item2 })
??
array.sort(by: {(item1, item2) in return item1 < item2 })
??
array.sort{(item1, item2) in item1 < item2 }
??
array.sort{ return $0 < $1 } //self
??
array.sort{ $0 < $1 }
??
array.sort(by: <)

根據(jù)上下文推斷類型

  • 因為排序閉包函數(shù)是作為sorted(by:)方法的參數(shù)傳入的,Swift可以推斷其參數(shù)和返回值的類型。sorted(by:)方法被一個整型數(shù)組調(diào)用,因此其參數(shù)必須是(Int, Int) -> Bool類型的函數(shù)。這意味著(Int, Int)Bool類型并不需要作為閉包表達式定義的一部分。因為所有的類型都可以被正確推斷,返回箭頭(->)和圍繞在參數(shù)周圍的括號也可以被省略:
array.sort(by: { item1, item2 in return item1 < item2 } )
  • 實際上,通過內(nèi)聯(lián)閉包表達式構(gòu)造的閉包作為參數(shù)傳遞給函數(shù)或方法時,總是能夠推斷出閉包的參數(shù)和返回值類型。這意味著閉包作為函數(shù)或者方法的參數(shù)時,你幾乎不需要利用完整格式構(gòu)造內(nèi)聯(lián)閉包。
  • 盡管如此,你仍然可以明確寫出有著完整格式的閉包。如果完整格式的閉包能夠提高代碼的可讀性,官方也鼓勵采用完整格式的閉包。而在sorted(by:)方法這個例子里,顯然閉包的目的就是排序。由于這個閉包是為了處理整型數(shù)組的排序,因此讀者能夠推測出這個閉包是用于整型處理的。

單表達式閉包隱式返回

  • 單行表達式閉包可以通過省略return關(guān)鍵字來隱式返回單行表達式的結(jié)果,如上版本的例子可以改寫為:
array.sort(by: { item1, item2 in item1 < item2 } )

參數(shù)名稱縮寫

  • Swift自動為內(nèi)聯(lián)閉包提供了參數(shù)名稱縮寫功能,你可以直接通過$0,$1,$2來順序調(diào)用閉包的參數(shù),以此類推。
  • 如果你在閉包表達式中使用參數(shù)名稱縮寫,你可以在閉包定義中省略參數(shù)列表,并且對應(yīng)參數(shù)名稱縮寫的類型會通過函數(shù)類型進行推斷。in關(guān)鍵字也同樣可以被省略,因為此時閉包表達式完全由閉包函數(shù)體構(gòu)成:
array.sort(by: { $0 < $1 } )

運算符方法

  • 實際上還有一種更簡短的方式來編寫上面例子中的閉包表達式。
array.sort(by: <)

尾隨閉包

  • 如果你需要將一個很長的閉包表達式作為最后一個參數(shù)傳遞給函數(shù),可以使用尾隨閉包來增強函數(shù)的可讀性。尾隨閉包是一個書寫在函數(shù)括號之后的閉包表達式,函數(shù)支持將其作為最后一個參數(shù)調(diào)用。在使用尾隨閉包時,你不用寫出它的參數(shù)標簽:
func someFunctionThatTakesAClosure(closure: () -> Void) {
    // 函數(shù)體部分
}

// 以下是不使用尾隨閉包進行函數(shù)調(diào)用
someFunctionThatTakesAClosure(closure: {
    // 閉包主體部分
})

// 以下是使用尾隨閉包進行函數(shù)調(diào)用
someFunctionThatTakesAClosure() {
    // 閉包主體部分
}
  • 上文的排序方法可以簡寫
array.sort(){ $0 < $1 } 
  • 如果閉包表達式是函數(shù)或方法的唯一參數(shù),則當你使用尾隨閉包時,你甚至可以把 () 省略掉:
array.sort{ $0 < $1 } 
  • 當閉包非常長以至于不能在一行中進行書寫時,尾隨閉包變得非常有用。舉例來說,SwiftArray類型有一個map(_:)方法,這個方法獲取一個閉包表達式作為其唯一參數(shù)。該閉包函數(shù)會為數(shù)組中的每一個元素調(diào)用一次,并返回該元素所映射的值。具體的映射方式和返回值類型由閉包來指定。
  • 當提供給數(shù)組的閉包應(yīng)用于每個數(shù)組元素后,map(_:)方法將返回一個新的數(shù)組,數(shù)組中包含了與原數(shù)組中的元素一一對應(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 = digitNames[number % 10]! + output
        number /= 10
    } while number > 0
    return output
}
print(strings)

// strings 常量被推斷為字符串類型數(shù)組,即 [String]
// 其值為 ["OneSix", "FiveEight", "FiveOneZero"]

值捕獲

  • 閉包可以在其被定義的上下文中捕獲常量和變量。即使定義這些常量和變量的原作用域已經(jīng)不存在,閉包仍然可以在閉包函數(shù)體內(nèi)引用和修改這些值。
  • Swift中,可以捕獲值的閉包的最簡單形式是嵌套函數(shù),也就是定義在其他函數(shù)的函數(shù)體內(nèi)的函數(shù)。嵌套函數(shù)可以捕獲其外部函數(shù)所有的參數(shù)以及定義的常量和變量。
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 10
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

let closure = makeIncrementer(forIncrement: 10)

print(makeIncrementer(forIncrement: 10)())
print(makeIncrementer(forIncrement: 10)())
print(makeIncrementer(forIncrement: 10)())

print(closure())
print(closure())
print(closure())

// 打印結(jié)果
20
20
20
20
30
40
  • makeIncrementer返回類型為() -> Int。這意味著其返回的是一個函數(shù),而非一個簡單類型的值。該函數(shù)在每次調(diào)用時不接受參數(shù),只返回一個Int類型的值。
  • incrementer()函數(shù)并沒有任何參數(shù),但是在函數(shù)體內(nèi)訪問了runningTotalamount變量。這是因為它從外圍函數(shù)捕獲了runningTotalamount變量的引用。捕獲引用保證了runningTotalamount變量在調(diào)用完makeIncrementer后不會消失,并且保證了在下一次執(zhí)行incrementer函數(shù)時,runningTotal依舊存在。

為了優(yōu)化,如果一個值不會被閉包改變,或者在閉包創(chuàng)建后不會改變,Swift 可能會改為捕獲并保存一份對值的拷貝。
Swift 也會負責被捕獲變量的所有內(nèi)存管理工作,包括釋放不再需要的變量。

  • 我們通過sil看看發(fā)生了什么,通過alloc_box創(chuàng)建一個空間給變量runningTotal,后面還有對它的內(nèi)存管理,alloc_box就是在堆區(qū)分配一塊內(nèi)存空間存儲值,會調(diào)用swift_allocObject
    image

閉包是引用類型

  • 上面的例子中,closure是常量,但是這些常量指向的閉包仍然可以修改其捕獲的變量的值。這是因為函數(shù)和閉包都是引用類型。
  • 無論你將函數(shù)或閉包賦值給一個常量還是變量,你實際上都是將常量或變量的值設(shè)置為對應(yīng)函數(shù)或閉包的引用。上面的例子中,指向閉包的引用closure是一個常量,而并非閉包內(nèi)容本身。
  • 這也意味著如果你將閉包賦值給了兩個不同的常量或變量,兩個值都會指向同一個閉包:


    image

通過IR來分析

IR的一些簡單語法

  • 我們這里只介紹我們用得到的語法
    • 數(shù)組
    [<elementnumber> x <elementtype>]
    // example
    alloca [24 x i8], align 8 //24個i8都是0
    
    • 結(jié)構(gòu)體
    %T = type {<type list>}
    %swift.refcountd = type {%swift.type*, i64 }
    
    • 指針類型
    <type> *
    i64* // 64位的整型
    
    • getelementptr指令,LLVM中我們獲取數(shù)組和結(jié)構(gòu)體的成員,通過getelementptr,語法規(guī)則如下:
    <result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <id x>}*
    <result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
    
  • 通過一個例子來理解getelementptr指令
struct munger_struct {
    int f1;
    int f2;
};
void munge(struct munger_struct *P) {
    P[0].f1 = P[1].f1+P[2].f2;
}

struct munger_struct array[3];
  • cdmain.c目錄下,輸入命令clang -emit-llvm -S main.c -o main.ll,打開main.ll%13就是數(shù)組的首個元素,%14就是取出結(jié)構(gòu)體的第一個元素也就是P[0].f1
    image

分析上文中的closure

  • 將上文的代碼轉(zhuǎn)為IR代碼,可以看到makeIncrementer函數(shù)返回了一個結(jié)構(gòu)體,第一個元素為void *,第二個元素為%swift.refcounted*
    image
  • %swift.refcounted*的定義,它是一個結(jié)構(gòu)體指針
    image
  • 再來看下上面結(jié)構(gòu)體的賦值,可以看到第一個元素里面存的就是內(nèi)嵌函數(shù)的地址
%12 = insertvalue { i8*, %swift.refcounted* } { i8* bitcast (i64 (%swift.refcounted*)* @"$s4main15makeIncrementer12forIncrementSiycSi_tF11incrementerL_SiyFTA" to i8*), %swift.refcounted* undef }, %swift.refcounted* %8, 1
  • 第二個參數(shù)的結(jié)構(gòu)


    image
  • 知道了它們的結(jié)構(gòu)體之后,我們將它轉(zhuǎn)化為對應(yīng)的結(jié)構(gòu)體如下:
struct HeapObject{
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}
//
struct FuntionData<T>{
    var ptr: UnsafeRawPointer // 內(nèi)嵌函數(shù)的地址
    var captureValue: UnsafePointer<T>  // 捕獲值的結(jié)構(gòu)體
}

struct Box<T> {
    var refCounted: HeapObject
    var valueBox: UnsafeRawPointer
    var value: T
}

// 由于編譯器不能識別FuntionData,所以我們將它綁定到一個具體的結(jié)構(gòu)體上
struct VoidIntFun {
    var f: () ->Int
}
  • 驗證我們上面的結(jié)論是否正確。


    image
  • 在終端中查找打印的地址,可以看到它就是我們的內(nèi)嵌函數(shù)


    image
  • 通過lldb查看內(nèi)存,我們直接查看closure的內(nèi)存,可以看到并沒有打印出我們想象中的結(jié)果

    image

  • 將返回值綁定在一個結(jié)構(gòu)體上


    image

函數(shù)也是一種引用類型

  • 函數(shù)是一個獨立的代碼塊,用來執(zhí)行特定的任務(wù)。同時我們函數(shù)也可以被當做參數(shù)被傳遞,也可以賦值變量,這里我們定義一個簡單的函數(shù)來看一下:
func makeFunc(param:Int) -> Int {
    var runningTotal = 10
    return runningTotal + param
}

var m = makeFunc
  • 查看它的IR代碼,可以看到和閉包的結(jié)構(gòu)類似,只是賦值的時候第二個值為空


    image
  • 我們也可以通過定義結(jié)構(gòu)體的方式打印它


    image
  • 函數(shù)的本質(zhì)也是一個結(jié)構(gòu)體,不過這個結(jié)構(gòu)體里只保存了函數(shù)的地址

逃逸閉包

  • 當一個閉包作為參數(shù)傳到一個函數(shù)中,但是這個閉包在函數(shù)返回之后才被執(zhí)行,我們稱該閉包從函數(shù)中逃逸。當你定義接受閉包作為參數(shù)的函數(shù)時,你可以在參數(shù)名之前標注@escaping,用來指明這個閉包是允許“逃逸”出這個函數(shù)的。
  • 一種能使閉包“逃逸”出函數(shù)的方法是,將這個閉包保存在一個函數(shù)外部定義的變量中。舉個例子,很多啟動異步操作的函數(shù)接受一個閉包參數(shù)作為completion handler。這類函數(shù)會在異步操作開始之后立刻返回,但是閉包直到異步操作結(jié)束后才會被調(diào)用。在這種情況下,閉包需要“逃逸”出函數(shù),因為閉包需要在函數(shù)返回之后被調(diào)用。例如:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}
  • someFunctionWithEscapingClosure(_:)函數(shù)接受一個閉包作為參數(shù),該閉包被添加到一個函數(shù)外定義的數(shù)組中。如果你不將這個參數(shù)標記為@escaping,就會得到一個編譯錯誤。
  • 默認的閉包都是非逃逸的,函數(shù)的生命周期和閉包的生命周期是一樣的,函數(shù)結(jié)束之后閉包的生命周期也就結(jié)束了;逃逸閉包出現(xiàn)的情況一般是延遲調(diào)用閉包,或者將它作為屬性存儲。

自動閉包

  • 自動閉包是一種自動創(chuàng)建的閉包,用于包裝傳遞給函數(shù)作為參數(shù)的表達式。這種閉包不接受任何參數(shù),當它被調(diào)用的時候,會返回被包裝在其中的表達式的值。這種便利語法讓你能夠省略閉包的花括號,用一個普通的表達式來代替顯式的閉包。
  • 我們經(jīng)常會調(diào)用采用自動閉包的函數(shù),但是很少去實現(xiàn)這樣的函數(shù)。舉個例子來說,assert(condition:message:file:line:)函數(shù)接受自動閉包作為它的condition參數(shù)和message參數(shù);它的condition參數(shù)僅會在debug模式下被求值,它的message參數(shù)僅當 condition參數(shù)為false時被計算求值。
  • 自動閉包讓你能夠延遲求值,因為直到你調(diào)用這個閉包,代碼段才會被執(zhí)行。延遲求值對于那些有副作用(Side Effect)和高計算成本的代碼來說是很有益處的,因為它使得你能控制代碼的執(zhí)行時機。下面的代碼展示了閉包如何延時求值。
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"
  • 盡管在閉包的代碼中,customersInLine的第一個元素被移除了,不過在閉包被調(diào)用之前,這個元素是不會被移除的。如果這個閉包永遠不被調(diào)用,那么在閉包里面的表達式將永遠不會執(zhí)行,那意味著列表中的元素永遠不會被移除。請注意,customerProvider的類型不是String,而是 () -> String,一個沒有參數(shù)且返回值為String的函數(shù)。

過度使用 autoclosures 會讓你的代碼變得難以理解。上下文和函數(shù)名應(yīng)該能夠清晰地表明求值是被延遲執(zhí)行的。

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

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

  • 閉包是自含的函數(shù)代碼塊,可以在代碼中被傳遞和使用 閉包和swift的對比 Swift 中閉包與OC的 block ...
    皆為序幕_閱讀 1,193評論 1 9
  • 閉包是功能性自包含模塊,可以在代碼中被傳遞和使用。Swift中的閉包與 C 和 Objective-C中的 blo...
    AirZilong閱讀 352評論 0 2
  • 閉包是自包含的函數(shù)代碼塊,可以在代碼中被傳遞和使用。Swift 中的閉包與 C 和 Objective-C 中的代...
    CoderLGL閱讀 281評論 0 0
  • 閉包是自包含的函數(shù)代碼塊,可以在代碼中被傳遞和使用。Swift 中的閉包與 C 和 Objective-C 中的代...
    窮人家的孩紙閱讀 1,726評論 1 5
  • 感覺有點難懂,可以參考http://www.lxweimin.com/p/49e7a29a2698 閉包是自包含的...
    CDLOG閱讀 658評論 0 0