Swift语法 Swift5 【07 - 闭包】


  • 作者: Liwx
  • 邮箱: 1032282633@qq.com
  • 源码: 需要源码的同学, 可以在评论区留下您的邮箱

iOS Swift 语法 底层原理内存管理分析 专题:【iOS Swift5语法】

00 - 汇编
01 - 基础语法
02 - 流程控制
03 - 函数
04 - 枚举
05 - 可选项
06 - 结构体和类
07 - 闭包
08 - 属性
09 - 方法
10 - 下标
11 - 继承
12 - 初始化器init
13 - 可选项


目录

  • 01-闭包表达式
  • 02-闭包表达式的简写
  • 03-尾随闭包
  • 04-示例-数组的排序
  • 05-忽略参数
  • 06-闭包(Closure)
  • 07-注意
  • 08-自动闭包

01-闭包表达式

  • Swift通过func定义一个函数,也可以通过闭包表达式定义一个函数

  • 闭包表达式格式

{
    (参数列表) -> 返回值类型 in
    函数体代码
}

  • 使用闭包调用时, 不需要参数标签
func sum(_ v1: Int, _ v2: Int) -> Int { v1 + v2 }

var fn = {
    (v1: Int, v2: Int) -> Int in
    return v1 + v2
}
// 闭包调用不需要写参数标签
fn(10, 20)
  • 类似匿名函数
{
    (v1: Int, v2: Int) -> Int in
    return v1 + v2
}(10, 20)

02-闭包表达式的简写

func exec(v1: Int, v2: Int , fn:(Int, Int) -> Int) {
    print(fn(v1, v2))
}

// 完整写法
exec(v1: 10, v2: 20, fn: { (v1: Int, v2: Int) -> Int in
    return v1 + v2
})

// 简写1 省略参数类型
exec(v1: 10, v2: 20, fn: { (v1, v2) -> Int in
    return v1 + v2
})

// 简写2 省略 返回值, 参数类型, 小括号. 如果闭包体只有一个表达式, 省略return
exec(v1: 10, v2: 20, fn: {
    v1, v2 in v1 + v2
})

// 简写3 省略参数, 使用$0 $1表示参数
exec(v1: 10, v2: 20, fn: {
    $0 + $1
})

// 简写4 闭包参数直接写运算符
exec(v1: 10, v2: 20, fn: +)

// Xcode 自动生成 尾随闭包
exec(v1: 10, v2: 20) { (v1, v2) -> Int in
    v1 + v2
}

03-尾随闭包

  • 如果将一个很长的闭包表达式作为函数的最后一个实参, 使用尾随闭包可以增强函数的可读性
    • 尾随闭包是一个被书写在函数调用括号外面(后面)的闭包表达式

  • 尾随闭包简单使用
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
    print(fn(v1, v2))
}

// 尾随闭包
exec(v1: 10, v2: 20) {
    $0 + $1
}

  • 如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的语法,那就不需要在函数名后面写圆括号
func exec(fn: (Int, Int) -> Int) {
    print(fn(1, 2))
}

// 简写
exec(fn: { $0 + $1 })
exec() { $0 + $1 }
exec { $0 + $1 }

// Xcode 自动生成
exec { (v1, v2) -> Int in
    v1 + v2
}

04-示例-数组的排序

  • 数组排序
var arr = [10, 1, 4, 20, 99]
// 从小到大
arr.sort()  // [1, 4, 10, 20, 99]
// 自定义排序
// 从大到小
arr.sort { (v1, v2) -> Bool in
    v1 > v2
}
print(arr)  // [99, 20, 10, 4, 1]
  • Swift标准库sort函数
@inlinable public mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows

  • 函数当做闭包调用
/// 数值比较
/// - Parameters:
///   - i1: 数值1
///   - i2: 数值2
/// - Returns: true: i1排在i2前面, false: i1排在i2后面
func cmp(i1: Int, i2: Int) -> Bool {
    return i1 > i2
}

var nums = [11, 2, 18, 6, 5, 68, 45]
nums.sort(by: cmp)
print(nums) // [68, 45, 18, 11, 6, 5, 2]
  • sort函数从小到大排序几种写法
nums.sort(by: {
    (i1: Int, i2: Int) -> Bool in
    return i1 < i2
})
nums.sort { (i1, i2) -> Bool in
    i1 < i2
}
nums.sort(by: {i1, i2 in return i1 < i2})
nums.sort(by: {i1, i2 in i1 < i2})
nums.sort(by: { $0 < $1 })
nums.sort(by: <)
nums.sort() { $0 < $1 }
nums.sort { $0 < $1 }

print(nums) // [2, 5, 6, 11, 18, 45, 68]

05-忽略参数

  • 使用下划线_忽略参数
func exec(fn: (Int, Int) -> Int) {
    print(fn(1, 2))
}

exec { $0 + $1 }

//var fn = { $0 + $1 }    // 编译器无法推断出类型 error: ambiguous use of operator '+'
var fn: (Int, Int) -> Int = { $0 + $1 }
exec(fn: fn)

// 忽略参数
exec {_, _ in 10}   // 10

06-闭包(Closure)

  • 关于闭包的定义
    • 一个函数和他所捕获的变量/常量环境组合起来,称为闭包
      • 一般指定义在函数内部函数
      • 一般它捕获的是外层函数局部变量/常量

func fn() -> () -> () {
    var a = 10
    
    func fn1() {
        // 捕获的是外层函数的局部变量/常量
        a = 10
    }
    return fn1
}

  • 汇编分析函数内存布局
func sum(_ v1: Int, _ v2: Int) -> Int { v1 + v2 }
var fn3 = sum // 函数地址占用16字节,前8个字节存放函数地址, 后8个字节存放0
print(fn3(10, 20))
 // rcx存放函数地址: 0x100002524 + 0x146c = 0x100003990
 0x10000251d <+6125>: leaq   0x146c(%rip), %rcx        ; _7_闭包.sum(Swift.Int, Swift.Int) -> Swift.Int at main.swift:292
 0x100002524 <+6132>: movq   %rcx, 0x2f15(%rip)        ; _7_闭包.fn3 : (Swift.Int, Swift.Int) -> Swift.Int
 0x10000252b <+6139>: movq   $0x0, 0x2f12(%rip)        ; _7_闭包.fn3 : (Swift.Int, Swift.Int) -> Swift.Int + 4

  • 汇编分析swift_allocObject(n)函数
    • swift_allocObject(n) // 至少需要n个字节,实际返回不一定是n个字节
    • lldb: bt命令打印函数调用栈
class Test {
    // 8, 8
    var test1: Int = 0  // 8
    var test2: Int = 0  // 8
}
var test = Test()   // 断点调试, 内部会调用__allocating_init
 07-闭包`Test.__allocating_init():
 ->  0x1000038d0 <+0>:  pushq  %rbp
     0x1000038d1 <+1>:  movq   %rsp, %rbp
     0x1000038d4 <+4>:  pushq  %r13
     0x1000038d6 <+6>:  pushq  %rax
     0x1000038d7 <+7>:  movl   $0x20, %esi              ; 分配32个字节
     0x1000038dc <+12>: movl   $0x7, %edx
     0x1000038e1 <+17>: movq   %r13, %rdi
     0x1000038e4 <+20>: callq  0x100003c1e               ; symbol stub for: swift_allocObject
     0x1000038e9 <+25>: movq   %rax, %r13
     0x1000038ec <+28>: callq  0x100003900               ; _7_闭包.Test.init() -> _7_闭包.Test at main.swift:94
     0x1000038f1 <+33>: addq   $0x8, %rsp
     0x1000038f5 <+37>: popq   %r13
     0x1000038f7 <+39>: popq   %rbp
     0x1000038f8 <+40>: retq

  • 使用bt指令查看函数调用栈
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
  * frame #0: 0x00007fff68bf6ce0 libsystem_malloc.dylib`malloc
    frame #1: 0x00007fff6835cca9 libswiftCore.dylib`swift_slowAlloc + 25
    frame #2: 0x00007fff6835cd27 libswiftCore.dylib`swift_allocObject + 39
    frame #3: 0x0000000100001a5d 07-闭包`main at main.swift:203:36
    frame #4: 0x00007fff68a40cc9 libdyld.dylib`start + 1

  • 汇编窥探闭包的本质

    • 内层函数没有捕获外层函数的局部变量/常量
typealias Fn = (Int) -> Int
 func getFn() -> Fn {
     // 局部变量
     var num = 0
     func plus(_ i: Int) -> Int {
         return i
     }
     
     return plus  
 }
  
 var fn = getFn()   // 返回的是函数地址,16字节, 低8字节: plus函数地址,高8字节:0
 fn(1)   // 1
 fn(2)   // 3
 fn(3)   // 6
 fn(4)   // 10
image.png
image.png
image.png

  • 内层函数有捕获外层函数的局部变量/常量

    • 闭包存储和类类似: 前8字节: 闭包信息
    • 如果捕获了外层函数的局部变量/常量, 则底层会调用: swift_allocObject 函数, 分配堆空间来存储需要捕获的变量/常量
    • 函数指针 地址16个字节,前8个字节: 不是plus函数地址,是间接调用plus的函数地址; 有捕获参数: num 后8个字节为 堆空间的地址, 没捕获: 后8个字节为 0
typealias Fn = (Int) -> Int
func getFn() -> Fn {
    // 局部变量
    var num = 0     // num占用24字节, 8: 类型 8: 引用计数 8: num值
    
    // 如果plus捕获了局部变量/常量, 可以认为plus函数传递了两个参数,i, 和num堆空间的地址值(用于plus内部访问堆空间num值)
    func plus(_ i: Int) -> Int {
        // 捕获的是外层函数的局部变量/常量
        num += i
        return num  // 断点调试, 观察num值变化
    }
    
    // 如果有捕获的是外层函数的局部变量/常量, 在return之前会将局部变量num数据保存到分配的堆空间中
    return plus     // 断点调试, 目的是要在swift_allocObject执行后返回分配的堆空间地址, 之后一行语句打断点
}   // 返回的plus和num形成了闭包

// 函数指针fn 地址16个字节,前8个字节: 不是plus函数地址,是间接调用plus的函数地址, 有捕获参数: num 后8个字节: 堆空间的地址, 没捕获: 后8个字节: 0
var fn = getFn() // 返回的是函数地址,16字节, 低8字节: 间接调用plus函数地址,高8字节:闭包地址
print(MemoryLayout.stride(ofValue: fn1))    // 16

fn(1)   // 1   0x0000000100004078 0x0000000000000002 0x0000000000000001 0x0000000000000000
fn(2)   // 3   0x0000000100004078 0x0000000000000002 0x0000000000000003 0x0000000000000000
fn(3)   // 6   0x0000000100004078 0x0000000000000002 0x0000000000000006 0x0000000000000000
fn(4)   // 10  0x0000000100004078 0x0000000000000002 0x000000000000000a 0x0000000000000000
image.png
image.png
  • 闭包捕获一个参数的内存布局
    D8FB8159-0DC8-47B0-9B55-32D5293FB403.png

  • 可以把闭包想象成是一个类的实例对象
    • 内存在堆空间
    • 捕获的局部变量/常量就是对象的成员(存储属性)
    • 组成闭包的函数就是类内部定义的方法
// 闭包类似于以下类实例对象
class Closure {
    var num = 0
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }
}

var cs1 = Closure()
var cs2 = Closure()
cs1.plus(1) // 1
cs2.plus(2) // 2
cs1.plus(3) // 4
cs2.plus(4) // 6
cs1.plus(5) // 9
cs2.plus(6) // 12
  • 窥探闭包捕获多个参数

typealias Fn = (Int) -> Int
func getFn() -> Fn {
    // 局部变量
    var num = 0     // num占用24字节, 8: 类型 8: 引用计数 8: num值
    var count = 0
    
    // 如果plus捕获了局部变量/常量, 可以认为plus函数传递了两个参数,i, 和num堆空间的地址值(用于plus内部访问堆空间num值)
    func plus(_ i: Int) -> Int {
        // 捕获的是外层函数的局部变量/常量
        num += i
        count += 1
        return num  // 断点调试, 观察num值变化
    }
    
    // 如果有捕获的是外层函数的局部变量/常量, 在return之前会将局部变量num数据保存到分配的堆空间中
    return plus     // 断点调试, 目的是要在swift_allocObject执行后返回分配的堆空间地址, 之后一行语句打断点
}   // 返回的plus和num形成了闭包

// 闭包地址16位,前8个字节: 不是plus函数地址,是间接调用plus的函数地址, 有捕获参数: num 后8个字节: 堆空间的地址, 没捕获: 后8个字节: 0

var fn = getFn()
fn(1)   // 1
fn(2)   // 3
fn(3)   // 6
fn(4)   // 10
image.png
0E79C50B-E1AF-4356-AF15-DDA93AB96608.png
  • 闭包捕获多个参数的内存布局
    EBF109E8-74CC-4833-A9F1-576426C6E48F.png

07-注意

  • 如果返回值是函数类型, 那么参数的修饰要保持统一
// 返回值是函数类型为(inout Int) -> Void
func add(_ num: Int) -> (inout Int) -> Void {
    func plus(v: inout Int) {  // 参数也必须修饰为inout Int类型 
        v += num
    }
    return plus
}

var num = 5
add(20)(&num)  // plus参数类型是inout Int, 调用是需要加上&符号
print(num)  // 25

08-自动闭包

  • 非自动闭包
// 如果第1个数大于0,返回第1个数,否则返回第2个数
func getFirstPositive(_ v1: Int, _ v2: Int) -> Int {
    return v1 > 0 ? v1 : v2
}
getFirstPositive(10, 20)    // 10
getFirstPositive(-2, 20)    // 20
getFirstPositive(0, -4)     // -4
// 改成函数类型的参数,可以让v2延迟加载
func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int {
    print("getFirstPositive1")
    return v1 > 0 ? v1 : v2()
}
getFirstPositive(-4, {20})  // 20
getFirstPositive(-4) {20}   // 20
  • 自动闭包的使用
    • @autoclosure 会自动将n封装成闭包{ n }
    • @autoclosure 只支持() -> T格式的参数
    • @autoclosure 并非只支持最后一个参数
    • 空合并运算符?? 使用了@autoclosure技术
    • 有@autoclosure,无@autoclosure,构成了函数重载
    • 为了避免与期望冲突,使用了@autoclosure的地方最好明确注释清楚: 这个值会被推迟执行
// 非自动闭包
func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int {
    print("getFirstPositive1")
    return v1 > 0 ? v1 : v2()
}
// 自动闭包写法  函数重载
func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
    print("getFirstPositive2")
    return v1 > 0 ? v1 : v2()
}
getFirstPositive(-4, 20)    // 无需加大括号,@autoclosure 会自动将20封装成闭包{ 20 }
getFirstPositive(-4, {30})  // 调用getFirstPositive(_ v1: Int, _ v2: () -> Int),  30

iOS Swift 语法 底层原理内存管理分析 专题:【iOS Swift5语法】

下一篇: 08 - 属性
上一篇: 06 - 结构体和类


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343