swift進階十三:閉包

swift進階 學習大綱

swift進階八:閉包 & Runtime & Any等類型 中,我們介紹了閉包 捕獲變量特性根时。本節(jié)描姚,我們繼續(xù)了解閉包:

  1. 什么是閉包
  2. 閉包結(jié)構(gòu)
  3. 逃逸閉包非逃逸閉包
  4. 自動閉包
  5. 函數(shù)作形參

1. 什么是閉包

來自維基百科的解釋:

  • 在計算機科學中,閉包(Closure)夹界,又稱詞法閉包(Lexical Closure)或函數(shù)閉包(function closures)馆里,是在支持頭等函數(shù)編程語言中實現(xiàn)詞法綁定的一種技術(shù)

  • 閉包在實現(xiàn)上是一個結(jié)構(gòu)體可柿,它存儲了一個函數(shù)(通常是其入口地址)和一個關(guān)聯(lián)環(huán)境(相當于一個符號查找表

1.1 全局函數(shù)

一種特殊閉包

  • 無捕獲變量全局函數(shù)鸠踪,也屬于閉包
func test(){
     print(a)
}

1.2 內(nèi)嵌函數(shù)

也是閉包,會捕獲外部變量
incrementer內(nèi)嵌函數(shù)复斥,也是一個閉包营密,捕獲了上層函數(shù)runningTotal

func makeIncrementer() -> (()-> Int){
    var runningTotal = 10
    
    // 內(nèi)嵌函數(shù)(也是一個閉包,捕獲了runningTotal)
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    
    return incrementer
}

print(makeIncrementer())   // (()-> Int)匿名函數(shù)
print(makeIncrementer()()) // Int

打印值:

image.png

1.3 閉包表達式

  • 閉包是一個匿名函數(shù)
  • 所有代碼都在花括號內(nèi)
  • 參數(shù)返回類型in關(guān)鍵字之前
  • in之后是主體內(nèi)容
{ (參數(shù))-> 返回類型 in
    // do something
}
  • swift的閉包可以當作let 常量目锭、var 變量评汰,也可以當作參數(shù)傳遞。
// 常量
let closure1: (Int) -> Int

// 變量
var closure2 : (Int) -> Int = { (age: Int) in 
     return age
}

// 參數(shù)傳遞
func test(params: ()->Int) {
    print(params())
}

var age = 10
// 執(zhí)行(尾隨閉包)
test { () -> Int in
    age += 1
    return age
}
  • swift閉包支持可選類型:在()外使用痢虹?聲明
// 可選值:在()外使用被去?聲明
var closure: ((Int) -> Int)?

1.4 尾隨閉包

閉包表達式作為函數(shù)最后一個參數(shù)時,可通過尾隨閉包書寫方式提高代碼的可讀性

func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item: Int, _ item3: Int) -> Bool) -> Bool{
    return by(a, b, c)
}

// 常規(guī)寫法
test(10, 20, 30, by: { (_ itme1: Int, _ itme2: Int, _ itme3: Int) -> Bool in
    return itme1 + itme2 < itme3
})

// 快捷寫法(小括號提到最后一個參數(shù)前)
test(10, 20, 30) { (_ itme1: Int, _ itme2: Int, _ itme3: Int) -> Bool in
    return itme1 + itme2 < itme3
}

// 最簡潔寫法 (入?yún)⒅苯邮褂?0 $1 $2代替奖唯,單行代碼可省略return)
test(10, 20, 30) { $0 + $1 < $2 }

可以看到惨缆,最簡潔寫法看上去非常舒服語義表達清晰

閉包表達式swift語法丰捷。使用閉包表達式可以更簡潔傳遞信息坯墨。好處多多:

  • 利用上下文推斷參數(shù)返回類型
  • 單表達式可以隱式返回省略return關(guān)鍵字
  • 參數(shù)名稱可以直接使用簡寫(如$0,$1,元組的$0.0)
  • 尾隨閉包可以更簡潔的表達
var array = [1, 2, 3]

// 1. 常規(guī)寫法
array.sort(by: {(item1 : Int, item2: Int) -> Bool in return item1 < item2 })

// 2. 省略類型 (根據(jù)上下文,可自動推斷參數(shù)類型)
array.sort(by: {(item1, item2) -> Bool in return item1 < item2 })

// 3. 省略返回類型 (根據(jù)上下文病往,可自動推斷返回類型)
array.sort(by: {(item1, item2) in return item1 < item2 })

// 4. 省略return (單行表達式捣染,可省略return)
array.sort{(item1, item2) in item1 < item2 }

// 5. 參數(shù)簡寫 (使用$0 $1,按位置順序獲取參數(shù))
array.sort{ return $0 < $1 }

// 6. 省略return (單行表達式停巷,可省略return)
array.sort{ $0 < $1 }

// 7. 使用高階函數(shù)耍攘,傳遞排序規(guī)則
array.sort(by: <)

2. 閉包的結(jié)構(gòu)

  • 我們使用變量記錄閉包榕栏,發(fā)現(xiàn)內(nèi)部屬性也被記錄了。
    (下面runningTotal被記錄少漆,多次打印時臼膏,runningTotal結(jié)果不一樣。
    如果直接調(diào)用閉包示损,會發(fā)現(xiàn)runningTotal結(jié)果一樣)
func makeIncrementer() -> (()-> Int){
    var runningTotal = 10
    
    // 內(nèi)嵌函數(shù)(也是一個閉包渗磅,捕獲了runningTotal)
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    
    return incrementer
}

// 函數(shù)變量 (存儲格式是怎樣?)
var makeInc = makeIncrementer()

print(makeInc()) // 打印: 11
print(makeInc()) // 打印: 12
print(makeInc()) // 打印: 13

print(makeIncrementer()()) // 打印: 11
print(makeIncrementer()()) // 打印: 11
print(makeIncrementer()()) // 打印: 11

Q: 把函數(shù)賦值給變量检访,變量存儲的是函數(shù)地址還是什么始鱼?

  • SIL中看不到makeInc的結(jié)構(gòu):
image.png
  • 我們再往后一層,直接查看ir代碼:

拓展IRIR語法:

image.png

Swift編譯流程

  1. swift源碼編譯為AST語法樹
    swiftc -dump-ast HTPerson.swift > ast.swift
  2. 生成SIL源碼
    swiftc -emit-sil HTPerson.swift > ./HTPerson.sil
  3. 生成IR中間代碼
    swiftc -emit-ir HTPerson.swift > ir.swift
  4. 輸出.o機器文件
    swiftc -emit-object HTPerson.swift

ir語法 ?? 官方文檔

  • 數(shù)組
[<elementnumber> x <elementtype>]  // [數(shù)組梳理 x 數(shù)組類型]
// example
alloca[24 x i8]脆贵,align8 // 24個i8都是0
  • 結(jié)構(gòu)體
%swift.refcounted = type { %swift.type*, i64 }   // { 指針類型医清,類型}
// example
%T = type {<type list>}  // 和C語言結(jié)構(gòu)體類似 
  • 指針類型
<type> *
// example
i64*    // 64位的整形
  • getelementptr指針別名
    可通過getelementptr讀取數(shù)組結(jié)構(gòu)體的成員:
<result> = getelementptr <ty>, <ty>* <ptrval> {, [inrange] <ty> <id x>}*
<result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
  • getelementptr讀取案例
    ( 創(chuàng)建一個c語言工程,main.c中加入測試代碼
// example
struct munger_struct {
        int f1;
        int f2;
};

void munge(struct munger_struct *p) { 
   p[0].f1 = p[1].f1 + p[2].f2;  // 假設(shè)P是有3個元素的數(shù)組卖氨。就可以直接通過下標讀取
}
 
struct munger_struct array[3];

int main(int argc, const char * argv[]) {
   munge(array); //調(diào)用
   return 0;
}
  • LLVM中会烙,C語言需要使用Clang輸出IR文件:
    clang -S -fobjc-arc -emit-llvm main.c
    image.png
  • 熟悉了IR語法后,回到這個代碼:
func makeIncrementer() -> (()-> Int){
    var runningTotal = 10
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
// 函數(shù)變量 (存儲格式是怎樣筒捺?)
var makeInc = makeIncrementer()
  • 輸出IR文件柏腻,分析結(jié)構(gòu):

    image.png

  • 按照IR分析的代碼,我們自定義結(jié)構(gòu)系吭,讀取函數(shù)指針地址內(nèi)部屬性值

由于無法直接讀取()-> Int函數(shù)的地址五嫂,所以利用結(jié)構(gòu)體地址就是首元素地址的特性,將函數(shù)設(shè)為結(jié)構(gòu)體第一個屬性肯尺。通過讀取結(jié)構(gòu)體指針地址沃缘,獲取到函數(shù)地址

struct FunctionData<T> {
    var pointer: UnsafeRawPointer
    var captureValue: UnsafePointer<T>
}

struct Refcounted {
    var pointer: UnsafeRawPointer
    var refCount: Int64
}

struct Type {
    var type: Int64
}

struct Box<T> {
    var refcounted: Refcounted
    var value: T  // 8字節(jié)類型,可由外部動態(tài)傳入
}

struct BoxMetadata<T> {
    var refcounted: UnsafePointer<Refcounted>
    var undefA: UnsafeRawPointer
    var type: Type
    var undefB: Int32
    var undefC: UnsafeRawPointer
}

// 由于無法直接讀取`()-> Int`函數(shù)的地址则吟,所以利用結(jié)構(gòu)體地址就是首元素地址的特性槐臀。將函數(shù)設(shè)為結(jié)構(gòu)體第一個屬性
struct VoidIntFunc {
    var f: ()->Int
}

func makeIncrementer() -> (()-> Int){
    var runningTotal = 10
    // 內(nèi)嵌函數(shù)(也是一個閉包,捕獲了runningTotal)
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    
    return incrementer
}

// 使用struct包裝的函數(shù)
var makeInc = VoidIntFunc(f: makeIncrementer())

// 讀取指針
let ptr = UnsafeMutablePointer<VoidIntFunc>.allocate(capacity: 1)

// 初始化
ptr.initialize(to: makeInc)

// 將指針綁定為FunctionData<Box<Int>>類型,返回指針
let context = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) { $0.pointee }

// 打印指針值 與 內(nèi)部屬性值
print(context.pointer)   // 打印 0x00000001000054b0
print(context.captureValue.pointee.value) // 打印  10
image.png

驗證打印的函數(shù)地址是否是makeIncrementer函數(shù):

image.png

打開終端逾滥,輸入命令:nm -p 編譯后的machO文件地址 | grep 函數(shù)地址

// 例如:
nm -p /Users/asetku/Library/Developer/Xcode/DerivedData/Demo-bhpsxmnrzusvmeaotyclgmelcxpp/Build/Products/Debug/Demo | grep 00000001000054b0
  • 可以看到峰档,該地址正是函數(shù)地址
    image.png
  • 使用xcrun swift-demangle XXXX命令,還原函數(shù)名
    image.png

所以:
函數(shù)事故引用類型寨昙,被賦值的變量記錄了函數(shù)地址
函數(shù)內(nèi)變量掀亩,會alloc開辟空間舔哪,調(diào)用前后,會retainrelease管理引用計數(shù)

拓展: 如果函數(shù)內(nèi)部多個屬性槽棍,結(jié)構(gòu)是怎樣呢捉蚤?

func makeIncrementer() -> (()-> Int){
   var aa = 10
   var bb = 20
   // 內(nèi)嵌函數(shù)(也是一個閉包抬驴,捕獲了runningTotal)
   func incrementer() -> Int {
       aa += 6
       bb += 9
       return bb
   }
   return incrementer
}
var makeInc = makeIncrementer()
  1. 基礎(chǔ)結(jié)構(gòu)沒有變化


    image.png
  2. 相比起單變量,多了一個臨時結(jié)構(gòu),把兩個變量分別用指針記錄:

    image.png

struct FunctionData<T> {
   var pointer: UnsafeRawPointer
   var captureValue: UnsafePointer<T>
}

struct Refcounted {
   var pointer: UnsafeRawPointer
   var refCount: Int64
}

struct Type {
   var type: Int64
}

struct Box<T> {
   var refcounted: Refcounted
   var value: T  // 8字節(jié)類型缆巧,可由外部動態(tài)傳入
}

// 多了一個Box2結(jié)構(gòu)布持,每個變量都是`Box`結(jié)構(gòu)的對象
struct Box2<T> {
   var refcounted: Refcounted
   var value1: UnsafePointer<Box<T>>
   var value2: UnsafePointer<Box<T>>
}

struct BoxMetadata<T> {
   var refcounted: UnsafePointer<Refcounted>
   var undefA: UnsafeRawPointer
   var type: Type
   var undefB: Int32
   var undefC: UnsafeRawPointer
}

// 由于無法直接讀取`()-> Int`函數(shù)的地址,所以利用結(jié)構(gòu)體地址就是首元素地址的特性陕悬。將函數(shù)設(shè)為結(jié)構(gòu)體第一個屬性
struct VoidIntFunc {
   var f: ()->Int
}

func makeIncrementer() -> (()-> Int){
  var aa = 10
  var bb = 20
  // 內(nèi)嵌函數(shù)(也是一個閉包题暖,捕獲了runningTotal)
  func incrementer() -> Int {
      aa += 6
      bb += 9
      return bb
  }
  return incrementer
}

// 使用struct包裝的函數(shù)
var makeInc = VoidIntFunc(f: makeIncrementer())

// 讀取指針
let ptr = UnsafeMutablePointer<VoidIntFunc>.allocate(capacity: 1)

// 初始化
ptr.initialize(to: makeInc)

// 將指針綁定為FunctionData<Box<Int>>類型,返回指針
let context = ptr.withMemoryRebound(to: FunctionData<Box2<Int>>.self, capacity: 1) { $0.pointee }

// 打印指針值 與 兩個內(nèi)部屬性值
print(context.pointer)   // 打印: 0x00000001000050c0
print(context.captureValue.pointee.value1.pointee.value) // 打幼匠:10
print(context.captureValue.pointee.value2.pointee.value) // 打与事薄: 20
image.png
  • MachO文件中校驗函數(shù)地址,確定是makeIncrementer函數(shù):
    image.png

3. 逃逸閉包與非逃逸閉包

  • 逃逸閉包
    閉包作為一個實際參數(shù)傳遞給一個函數(shù),且在函數(shù)返回之后調(diào)用拼岳,我們就說這個閉包逃逸了枝誊。
    (逃逸閉包作為函數(shù)形參時,需要使用@escaping聲明惜纸,生命周期函數(shù)叶撒。如:被外部變量持有異步延時調(diào)用)
  • 非逃逸閉包
    系統(tǒng)默認閉包參數(shù)是@nonescaping聲明, 是非逃逸閉包生命周期被調(diào)用函數(shù)保持一致耐版。

3.1 逃逸閉包

  • 閉包函數(shù)外變量持有祠够,需要@escaping聲明為逃逸閉包
  • 閉包異步線程延時調(diào)用,需要@escaping聲明為逃逸閉包
class HTPerson {
    
    var completion: ((Int)->Void)?
    
    func test1(handler: @escaping (Int)->Void) {
        
        // 1. 外部變量持有handler椭更,handler的生命周期逃到了`makeIncrementer`函數(shù)外
        self.completion = handler
    }
    
    func test2(handler: @escaping (Int)->Void) {
        
        // 2. 異步線程延時調(diào)用handler哪审,handler的生命周期逃到了`makeIncrementer`函數(shù)外
        DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
            handler(10)
        }
    }
    
}

3.2 非逃逸閉包

系統(tǒng)默認閉包為非逃逸閉包 ,編譯期自動加上@noescape聲明,生命周期函數(shù)一致虑瀑。

func test(closure: (()->())) {}
  • SIL編譯可以看到默認使用@noescape聲明閉包:
    image.png

4. 自動閉包

  • 自動閉包自動識別閉包返回值湿滓,可直接接收返回值類型數(shù)據(jù)
    需要用@autoclosure聲明舌狗,不接收任何參數(shù)叽奥,返回值是當前內(nèi)部表達式(如()->String)
// 自動閉包`@autoclosure`聲明
func testPrint(_ message: @autoclosure ()->String) {
    print(message())
}

func doSomeThing() -> String {
    return "吃了嗎?"
}
// 入?yún)鱜函數(shù)`
testPrint(doSomeThing()) 
// 入?yún)鱜字符串`
testPrint("干啥呢")
  • 可以看到痛侍,使用自動閉包時朝氓,參數(shù)可以是函數(shù),也可以是閉包返回類型(字符串).
    自動閉包可以兼容函數(shù)入?yún)?/code>類型(函數(shù)/函數(shù)返參類型

5. 函數(shù)作形參

  • 函數(shù)作為參數(shù)進行傳遞時主届,可節(jié)省計算量赵哲,在合適時期執(zhí)行

  • 普通函數(shù):
    doSomeThing是一個耗時操作君丁,計算結(jié)果傳給testPrint時枫夺,testPrint由于條件不滿足,壓根沒用到這個耗時操作結(jié)果绘闷。

func testPrint(_ condition: Bool, _ message: String) {
    if condition {
        print("錯誤信息: \(message)")
    }
    print("結(jié)束")
}

func doSomeThing() -> String {
    print("執(zhí)行了")
    // 耗時操作橡庞,從0到1000拼接成字符串
    return (0...1000).reduce("") { $0 + " \($1)"}
}

testPrint(false, doSomeThing())
image.png
  • 使用函數(shù)作為入?yún)ⅲ?br> 入?yún)?/code>直接傳入函數(shù)较坛,未滿足條件時,執(zhí)行函數(shù)扒最,避開了耗時操作
func testPrint(_ condition: Bool, _ message: ()->String) {
    if condition {
        print("錯誤信息: \(message())")
    }
    print("結(jié)束")
}

func doSomeThing() -> String {
    print("執(zhí)行了")
    // 耗時操作丑勤,從0到1000拼接成字符串
    return (0...1000).reduce("") { $0 + " \($1)"}
}

testPrint(false, doSomeThing())
image.png

注意

  • 上述只是演示函數(shù)作為入?yún)?/code>,沒有單次調(diào)用時吧趣,可延時合適時期調(diào)用法竞,避免資源提前計算
  • 但如果該資源會被多次調(diào)用再菊,還是提前計算資源節(jié)省資源爪喘。

至此,我們對閉包有了完整認識纠拔。下一節(jié)介紹Optional & Equatable & 訪問控制

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末秉剑,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子稠诲,更是在濱河造成了極大的恐慌侦鹏,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件臀叙,死亡現(xiàn)場離奇詭異略水,居然都是意外死亡,警方通過查閱死者的電腦和手機劝萤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門渊涝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人床嫌,你說我怎么就攤上這事跨释。” “怎么了厌处?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵鳖谈,是天一觀的道長。 經(jīng)常有香客問我阔涉,道長缆娃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任瑰排,我火速辦了婚禮贯要,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘椭住。我一直安慰自己郭毕,他們只是感情好,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布函荣。 她就那樣靜靜地躺著显押,像睡著了一般。 火紅的嫁衣襯著肌膚如雪傻挂。 梳的紋絲不亂的頭發(fā)上乘碑,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天,我揣著相機與錄音金拒,去河邊找鬼兽肤。 笑死,一個胖子當著我的面吹牛绪抛,可吹牛的內(nèi)容都是我干的资铡。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼幢码,長吁一口氣:“原來是場噩夢啊……” “哼笤休!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起症副,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤店雅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后贞铣,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體闹啦,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年辕坝,在試婚紗的時候發(fā)現(xiàn)自己被綠了窍奋。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡酱畅,死狀恐怖琳袄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情圣贸,我是刑警寧澤挚歧,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站吁峻,受9級特大地震影響滑负,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜用含,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一矮慕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧啄骇,春花似錦痴鳄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽螺句。三九已至,卻和暖如春橡类,著一層夾襖步出監(jiān)牢的瞬間蛇尚,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工顾画, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留取劫,地道東北人。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓研侣,卻偏偏與公主長得像谱邪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子庶诡,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353

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