Swift閉包

  • 閉包的定義

    閉包是一個捕獲上下文的常量或變量的匿名函數
func test() {
    print("test")
}

??的全局函數是一種特殊閉包不捕獲變量;

??的內嵌函數也是一個捕獲外部變量閉包

func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    // 內嵌函數,會捕獲外層函數的變量runningTotal
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
  • 閉包的寫法

閉包表達式:會自動上下文捕獲(常)變量

{ (parameters) -> returntype in
    statements
}
  
//將閉包聲明為可選類型
var closure : ((Int) -> Int)?
closure = nil

//將閉包表達式賦值給常量
let closure : (Int) -> Int
closure = {(age : Int) -> Int in
    return age
}
//或者省略 -> Int
closure = {(age : Int) in
    return age
}

// 閉包作為函數參數
func test(param : () -> Int) {
    print(param())
}
// 使用尾隨閉包
var age = 10
test { ()->Int in
   age += 1
   return age
}

使用閉包表達式好處

  1. 利用上下文推斷參數返回值類型
  2. 單一表達式可以隱式返回(省略return關鍵字)
  3. 簡寫參數名稱($0)
  4. 尾隨閉包表達式增加代碼的可讀性
  • 尾隨閉包

1.將閉包表達式作為函數的最后一個參數時,如果當前閉包表達式很長,則可以通過尾隨閉包的方式來提高代碼的可讀性

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

// 普通調用:可讀性差
test(2, 3, 4, by: {(_ item1 : Int, _ item2 : Int, _ item3 : Int) -> Bool in
    return (item1 + item2 < item3)
}

// 通過尾隨閉包調用:簡單明了
test(2, 3, 4) {(_ item1 : Int, _ item2 : Int, _ item3 : Int) -> Bool in
    return (item1 + item2 < item3)
}

2.當函數有且僅有閉包表達式作為唯一參數時,可以省略小括號直接寫閉包表達式。

應用:簡化array.sort的調用語法

var array = [5,1,3]

// 普通調用
array.sort(by: {(item1 : Int, item2 : Int) -> Bool in return item1 < item2 })
// 省略參數類型:閉包表達式可以根據上下文推斷參數類型
array.sort(by: {(item1, item2) -> Bool in return item1 < item2 })
// 閉包表達式作為唯一參數時省略(),直接{}
array.sort {(item1, item2) -> Bool in return item1 < item2 }
// 省略返回值類型:閉包表達式可以根據上下文推斷返回值類型
array.sort{(item1, item2) in return item1 < item2 }
// 省略return關鍵字:單一表達式隱式返回
array.sort{(item1, item2) in item1 < item2 }
// 用$0、$1簡寫參數
array.sort{$0 < $1 }
// 簡化語法終極版
array.sort(by: <)
  • 閉包的本質:捕獲值

首先通過之前的例子來分析閉包:

func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}

通過兩種不同的調用方式打印結果:
方式一:

print(makeIncrementer()()) //11
print(makeIncrementer()()) //11
print(makeIncrementer()()) //11

和方式二:

let makeInc = makeIncrementer()
print(makeInc()) //11
print(makeInc()) //12
print(makeInc()) //13

顯然上面兩種調用方式得到了兩種不同的結果:方式一中是每次都重新分配一個變量runningTotal,其值為10,每次調用都是在10的基礎上+1,所以得出的結果都是11;而方式二中,調用一次外層函數makeIncrementer就只分配了一個變量runningTotal = 10,函數的返回結果makeInc捕獲了變量runningTotal,意味著runningTotal就不再是一個單純的值10了,而是一個引用地址,所以每次調用就是進行引用類型的賦值操作。

SIL中分析閉包捕獲值的本質:

// makeIncrementer()
sil hidden @main.makeIncrementer() -> () -> Swift.Int : $@convention(thin) () -> @owned @callee_guaranteed () -> Int {
bb0:
  // 通過 alloc_box 在堆上分配一塊內存,存儲metadata、refCount、value,內存地址--->runningTotal
  %0 = alloc_box ${ var Int }, var, name "runningTotal" // users: %8, %7, %6, %1
  // 通過 project_box 取出%0(理解為對象)的value的地址
  %1 = project_box %0 : ${ var Int }, 0           // user: %4
  %2 = integer_literal $Builtin.Int64, 10         // user: %3
  %3 = struct $Int (%2 : $Builtin.Int64)          // user: %4
  // 將10存入%1這個地址里
  store %3 to %1 : $*Int                          // id: %4
  // function_ref incrementer #1 () in makeIncrementer()
  %5 = function_ref @incrementer #1 () -> Swift.Int in main.makeIncrementer() -> () -> Swift.Int : $@convention(thin) (@guaranteed { var Int }) -> Int // user: %7
  // 閉包調用前對%0(理解為對象)做retain操作
  strong_retain %0 : ${ var Int }                 // id: %6
  // 調用內嵌函數incrementer(閉包)
  %7 = partial_apply [callee_guaranteed] %5(%0) : $@convention(thin) (@guaranteed { var Int }) -> Int // user: %9
   // 閉包調用后對%0(理解為對象)做release操作
  strong_release %0 : ${ var Int }                // id: %8
  return %7 : $@callee_guaranteed () -> Int       // id: %9
} // end sil function 'main.makeIncrementer() -> () -> Swift.Int'
捕獲runningTotal的圖解

匯編模式下也可看出調用了swift_allocObject

總結:
1.綜上可知閉包捕獲值的本質就是在上分配一塊內存空間存儲值的引用
2.閉包能從上下文捕獲已被定義的(常)變量,即使它們的原作用域已經不存在,閉包仍能在其函數體內引用修改這些值;
3.每次修改的捕獲值其實修改的是堆內存中的value值;
4.每次重新執行當前函數時都會重新創建內存空間。

  • 閉包是引用類型

接下來通過分析IR文件來看一下makeInc這個變量中到底存儲的是什么?

  • 簡單的IR語法
  • 數組
<elementnumber> x <elementtype>
iN: N位(bits)的整型
// example 
alloca [24 x i8], align 8  //24個i8(1字節的整型)都是0
  • 結構體
// 名稱 = type {結構體成員,類型}
%swift.refcounted = type { %swift.type*, i64 } 
//example
%T = type {<type list>} // 類似C的結構體
  • 指針
<type> *
//example
i64*   //8字節的整型指針
  • getelementptr指令
    用于LLVM中獲得指向數組的元素和指向結構體成員的指針
<result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <id x>}*
<result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*

通過LLVM官網的例子來熟悉IR語法:

struct munger_struct {
    int f1;
    int f2;
};

void munge(struct munger_struct *P) {
    P[0].f1 = P[1].f1 + P[2].f2;
}
  • %2: 創建一塊內存空間存放數組首地址(數組中存放[結構體1的地址,結構體2的地址,結構體3的地址])--->%struct.munger struct** %2訪問二級指針

  • load %struct.munger struct*, %struct.munger struct**%2, align 8----->取出二級指針%2中的地址返回(數組的首地址

  • getelementptr inbounds %struct.munger_struct, %struct.munger_struct* %3, i64 1第一層索引i64 1,相對于自身(這里是數組)偏移1個i64大小的地址即arr[1]的地址

  • getelementptr inbounds %struct.munger_struct, %struct.munger_struct* %4, i32 0, i32 0:第一層索引i32 0相對于自身(使用的基本類型結構體)偏移0個i32大小的地址(還是arr[0]這個結構體的首地址),第二層i32 0相對于結構體中的元素偏移0個i32大小的地址(結構體中第一個元素f1的地址 *i32)

總結:
1.getelementptr后面可以有多層索引,每多一層索引,索引使用的基本類型和返回的指針類型去掉一層;
如上面的i32 0, i32 0:第一層i32 0使用的基本類型是結構體類型(所以相對于自身偏移),返回的是結構體指針類型(拿到的是結構體的地址);第二層i32 0使用的基本類型是int(結構體去掉一層),返回的是*Int(結構體指針去掉一層)。

  1. 第一層索引會改變getelementptr這個表達式的返回類型(因為是相對于自身偏移);

3.第一層索引的偏移量由第一層索引的值和第一層ty指定的基本類型決定的。

P[1].f1
P[2].f2
P[0].f1
  • 分析閉包的數據結構
  1. 只有一個捕獲值時:
    上面例子生成的IR文件中,makeIncrementer函數的實現如下:
%swift.full_boxmetadata = type { void (%swift.refcounted*)*, i8**, %swift.type, i32, i8* }
%swift.refcounted = type { %swift.type*, i64 }
%swift.type = type { i64 }
%TSi = type <{ i64 }>

define hidden swiftcc { i8*, %swift.refcounted* } @"main.makeIncrementer() -> () -> Swift.Int"() #0 {
entry:
  %runningTotal.debug = alloca %TSi*, align 8
  %0 = bitcast %TSi** %runningTotal.debug to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %0, i8 0, i64 8, i1 false)
  %1 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata, i32 0, i32 2), i64 24, i64 7) #1
  %2 = bitcast %swift.refcounted* %1 to <{ %swift.refcounted, [8 x i8] }>*
  %3 = getelementptr inbounds <{ %swift.refcounted, [8 x i8] }>, <{ %swift.refcounted, [8 x i8] }>* %2, i32 0, i32 1
  %4 = bitcast [8 x i8]* %3 to %TSi*
  store %TSi* %4, %TSi** %runningTotal.debug, align 8
  %._value = getelementptr inbounds %TSi, %TSi* %4, i32 0, i32 0
  store i64 10, i64* %._value, align 8
  %5 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %1) #1
  call void @swift_release(%swift.refcounted* %1) #1
  %6 = insertvalue { i8*, %swift.refcounted* } { i8* bitcast (i64 (%swift.refcounted*)* @"partial apply forwarder for incrementer #1 () -> Swift.Int in main.makeIncrementer() -> () -> Swift.Int" to i8*), %swift.refcounted* undef }, %swift.refcounted* %1, 1
  ret { i8*, %swift.refcounted* } %6
}

bitcast: 按位轉換,轉換前后的類型的位大小必須相同;類似unsafeBitCast
%1: 通過swift_allocObject分配的內存,返回swift.refcounted*類型
%2 = bitcast %swift.refcounted* %1 to <{ %swift.refcounted, [8 x i8] }>*: 將swift.refcounted*類型按位轉換為{ %swift.refcounted, [8 x i8] }結構體類型
%3: 獲取結構體中的[8 x i8]
%4: 將[8 x i8]* 按位轉換---> %TSi* (%TSi = type <{ i64 }>)即i64*,存儲runningTotal的值
%._value: 偏移后的地址就是%4這個i64*地址,將i64類型的10存儲在runningTotal
{ i8*, %swift.refcounted* }: 最終返回的值
i8*--->void *:存儲內嵌函數的地址
%swift.refcounted*:存儲 %1({ %swift.refcounted, [8 x i8] }>*

  • 模擬閉包的數據結構
func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    func incrementer() -> Int {
        runningTotal += 1
 
        return runningTotal
    }
    return incrementer
}

/**
 %swift.refcounted = type { %swift.type*, i64 }
 %swift.type = type { i64 }
 %swift.refcounted的數據結構
 */
struct HeapObject {
    var type : UnsafePointer<Int64>
    var refCounts : Int64
}

/**
 bitcast %swift.refcounted* %1 to <{ %swift.refcounted, [8 x i8] }>*
 { %swift.refcounted, [8 x i8] }的數據結構
 T : 可傳入其它類型
 */
struct BoxValue<T> {
    var heapObj : HeapObject
    var value : T
}

/**
 ret { i8*, %swift.refcounted* } %6
 { i8*, %swift.refcounted* }的數據結構
 */
struct FuncResult<T> {
    var funcAddress : UnsafeRawPointer // 內嵌函數的地址
    var captureValue : UnsafePointer<BoxValue<T>> // 捕獲值的地址
}

// 返回類型(包裝的結構體,直接傳入函數類型的話在轉換過程中會出錯)
struct Function {
    var function : () -> Int
}

// 將Function<T>綁定到FuncResult<T>的數據結構
var makeInc = Function(function: makeIncrementer())

let funcPtr = UnsafeMutablePointer<Function>.allocate(capacity: 1)
funcPtr.initialize(to: makeInc)

let bindPtr = funcPtr.withMemoryRebound(to: FuncResult<Int>.self, capacity: 1) { p in
    return p.pointee
}

funcPtr.deinitialize(count: 1)
funcPtr.deallocate()

print(bindPtr.funcAddress) //0x0000000100002b80
print(bindPtr.captureValue.pointee.value) //10
通過終端查看內存地址

bindPtr.funcAddress內嵌函數地址對應的符號

  1. 兩個捕獲值時閉包的數據結構:
func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    var value = 6.00
    func incrementer() -> Int {
        runningTotal += 1
        value += 4.00
        return runningTotal
    }
    return incrementer
}

可知上面的閉包捕獲兩個值:Int--> runningTotal和 Double--> value,數據結構稍微有一些改變,中間多了一層封裝兩個捕獲值:

/**
<{ %swift.refcounted, %swift.refcounted*, %swift.refcounted* }>*
%swift.refcounted*: 第一個捕獲值的地址 T:第一個捕獲值的類型
%swift.refcounted*: 第二個捕獲值的地址 V:第二個捕獲值的類型
*/
struct MultiBox<T, V> {
    var refCounted : HeapObject
    var value1 :  UnsafePointer<BoxValue<T>>
    var value2 :  UnsafePointer<BoxValue<V>>
}

/**
 ret { i8*, %swift.refcounted* } %6
 { i8*, %swift.refcounted* }的數據結構
 */
struct FuncResult<T, V> {
    var funcAddress : UnsafeRawPointer // 內嵌函數的地址
    var captureValue : UnsafePointer<MultiBox<T, V>> // 捕獲值的地址
}

print(bindPtr.funcAddress) //0x0000000100002750
print(bindPtr.captureValue.pointee.value1.pointee.value) //10
print(bindPtr.captureValue.pointee.value2.pointee.value) //6.0

總結:

  • 閉包捕獲值的原理就是在上開辟內存空間,存儲捕獲值
  • 修改捕獲值時就是去堆上修改
  • 閉包是引用類型(地址傳遞),閉包的底層結構-->結構體(函數的地址 + 捕獲變量的值)
  • 閉包不捕獲值時(函數)也是一個引用類型(地址傳遞),底層結構還是一個結構體(函數地址 + null),只保存了函數地址。
func makeIncrementer(inc : Int) -> Int {
    var runningTotal = 10
    runningTotal += inc
    return runningTotal
}

var makeInc = makeIncrementer
 
struct FuncData {
    var funcAddress : UnsafeRawPointer
    var captureValue : UnsafeRawPointer?
}

struct Func {
    var function : (Int) -> Int
}

var f = Func(function: makeIncrementer(inc:))
let ptr = UnsafeMutablePointer<Func>.allocate(capacity: 1)
ptr.initialize(to: f)

let funcPtr = ptr.withMemoryRebound(to: FuncData.self, capacity: 1){
    $0.pointee
}

ptr.deinitialize(count: 1)
ptr.deallocate()

print(funcPtr.funcAddress) //0x00000001000030a0
print(funcPtr.captureValue) //nil

/**
cat address 0x0000000100003000
&0x0000000100003000,  FirstSwiftTest.makeIncrementer(inc: Swift.Int) -> Swift.Int <+0> , ($s14FirstSwiftTest15makeIncrementer3incS2i_tF)FirstSwiftTest.__TEXT.__text
*/
  • 逃逸閉包

1.定義
  • 將閉包作為實際參數傳遞給函數
  • 該閉包是在函數返回后調用
    • 異步延遲調用
    • 存儲,需要時再調用
2. 聲明:逃逸閉包前+@escaping
var completionHandler : ((Int)->())?

// 先存儲再調用
func makeIncrementer(amount : Int, handler : @escaping (Int) -> ()) {
    var runningTotal = 10
    runningTotal += amount
    // 必須顯式引用self,目的是提醒可能有循環引用,自己注意解決
    self.completi
onHandler = handler
}

// 異步延遲調用
func async_makeIncrementer(amount : Int, handler : @escaping (Int) -> ()) {
    var runningTotal = 10
    runningTotal += amount
    
    DispatchQueue.global().asyncAfter(wallDeadline: .now() + 0.1) {
        handler(runningTotal)
    }
}

重點:逃逸閉包的生命周期一定要比函數的生命周期

3.區別非逃逸閉包
  • 非逃逸閉包和逃逸閉包的前提都是:作為函數的參數

  • 非逃逸閉包在sil中都是@noescape修飾

  • 函數體內執行,生命周期函數生命周期一致

  • 非逃逸閉包會產生循環引用(作用域在函數作用域內)

  • 編譯器對非逃逸閉包優化:省略掉內存管理retain、release

  • 逃逸閉包耗資源,不要濫用

  • 逃逸閉包必須顯式引用self,提醒可能存在循環引用需要解決。

  • 自動閉包

    • 使用 @autoclosure關鍵字聲明自動閉包
    • 接收任何參數返回值是當前內部表達式的值
func debugErrorMsg(_ condition : Bool, _ message : @autoclosure () -> String) {
    if condition {
        print(message())
    }
}

debugErrorMsg(true, "NetWork Error Occured")

上面例子中,實際就是將傳入的字符串"NetWork Error Occured"放入到一個閉包表達式中,在調用的時候返回。即:

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

推薦閱讀更多精彩內容

  • Swift 閉包 [TOC] 前言 我個人覺得在看這篇文章前,先了解一下Swift 函數[https://swif...
    just東東閱讀 737評論 0 2
  • 前言 本篇文章主要講解Swift中又一個相當重要的知識點 ?? 閉包,首先會介紹閉包的概念,包含與OC中Block的...
    深圳_你要的昵稱閱讀 492評論 0 9
  • 前情提要 Swift的閉包和OC的Block是一回事,是一種特殊的函數-帶有自動變量的匿名函數。 分別從語法和原理...
    Jacob6666閱讀 424評論 0 0
  • 什么是閉包 維基百科中的解釋:在計算機科學中,閉包(Closure),又稱詞法閉包(Lexical Closure...
    帥駝駝閱讀 442評論 0 3
  • 本文主要分析閉包以及閉包捕獲變量的原理 閉包 閉包是一個捕獲了全局上下文的常量或者變量的函數,通俗來講,閉包可以是...
    輝輝歲月閱讀 332評論 0 0