Swift 中的闭包

闭包是自包含的函数代码块,可以在代码中被传递和使用。Swift 中的闭包与 CObjective-C 中的代码块 (blocks) 以及其他一些编程语言中的匿名函数比较相似。
闭包可以捕获和存储其所在上下文中任意常量和变量的引用,被称为包裹常量和变量。

闭包表达式

Swift 中可以通过 func 关键字 定义一个函数

func sum(_ v1: Int, _ v2: Int) -> Int {
    return v1 + v2
}

sum 函数调用:

sum(10, 20)  // 30

也可以用过闭包表达式定义一个函数

var fn = { (v1: Int, v2: Int) -> Int in
    return v1 + v2
}

闭包表达式调用:

fn(10, 20) // 30

Swift 中函数是一类公民,可以作为参数返回值,跟 IntArrayClass 等没有区别:

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

观察 exec 函数,这是有三个参数的无返回值函数:

  • 第1个参数:v1,类型为 Int
  • 第2个参数:v2,类型为 Int
  • 第1个参数:fn,类型为 (Int, Int) -> Int)
  • 无返回值

调用 exec 函数:

exec(v1: 10, v2: 20, fn: { (v1: Int, v2: Int) -> Int in
    // 注意:这里的 v1 和 v2 是 fn 闭包表达式内的参数,跟 exec 函数的 v1、v2 没有任何关系
    return v1 + v2
})

闭包的精简写法

上面 exec 的调用虽然很容易理解,但看上去有些冗长:参数类型满天飞fn 的参数 v1v2exec本身的前两个参数容易混淆。
强大的 Swift 编译器允许我们对其做一些精简,下面一步步做介绍:

  • 由于在定义 exec 的时候已经明确了 fn 的两个参数类型Int和返回类型Int,所以可以做如下简化:

    // 只需要使用 in 关键字 将参数和函数体做区隔即可,省略了v1、v2的类型以及返回值
    exec(v1: 10, v2: 20, fn: { v1, v2 in
        return v1 + v2
    })
    
  • 就跟函数一样,闭包函数体的单行表达式可以省略 return

    // 省略了函数体的 return 关键字
     exec(v1: 10, v2: 20, fn: { v1, v2 in
         v1 + v2
     })
    
  • Swift 中可以使用 $0$1 来分别表示第0个参数第1个参数

  exec(v1: 10, v2: 20, fn: {
       $0 + $1
  })
  • 甚至于你可以省略$0$1(这种方式过分了,不推荐😓)
  // 直接使用一个 + ,表示第0个参数和第1个参数直接是加号运算符
  exec(v1: 10, v2: 20, fn: +)

尾随闭包

如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数,将这个闭包替换成为尾随闭包的形式很有用。
尾随闭包是一个书写在函数圆括号之后的闭包表达式,函数支持将其作为最后一个参数调用。
在使用尾随闭包时,你不用写出它的参数标签。

还用之前的 exec 函数举例

  • 不使用尾随闭包:
exec(v1: 10, v2: 20, fn: { (v1: Int, v2: Int) -> Int in
    return v1 + v2
})
  • 使用尾随闭包:
    exec(v1: 10, v2: 20) { (v1: Int, v2: Int) -> Int in
       return v1 + v2
    }
    

尾随闭包不仅仅省略了 fn 形参,而且将{函数体}挪到了()外面让整个函数调用更加的直观易读。同样可以对其进行精简:
swift // 省略闭包参数类型和返回值 exec(v1: 10, v2: 20) { v1, v2 in // 省略 return v1 + v2 }
然后:
swift exec(v1: 10, v2: 20) { // 直接使用 $0、$1 表示第0和第1个参数 $0 + $1 }
这个表达式就非常的简洁优雅了!👍🏻 注意,下面的表达式是不允许的:
swift // 尾随闭包不允许省略$0、$1 exec(v1: 10, v2: 20) { + }

闭包的值捕获

闭包可以理解为函数以及其捕获的上下文中的变量或常量的总和

看下面这个函数:

func getFn() -> (Int) -> Int {
    var num = 0
    func plus (_ i: Int) -> Int {
        num = num + i
        return num
    }
    return plus
}

getFn函数没有参数,返回值为(Int) -> Int(一个参数为Int返回值为Int的函数)。

getFn函数体内定义了一个Int类型的变量num,又定义了一个plus函数,并将其作为getFn函数的返回值返回。

plus函数对num变量进行了捕获,构成了闭包。

思考如下代码的输出:

var f = getFn()
print(f(1))
print(f(2))
print(f(3))
print(f(4))

结果是哪一组?

1         1
2   or    3
3         6
4         10

正如前面提到的函数以及其捕获的上下文中的变量或常量的总和,当调用getFn()时,返回的不仅仅是plus函数同时也包括num变量组成的闭包整体!理解这个概念非常重要,因此getFn()返回的其实就是下面的代码片段:

var num = 0
func plus (_ i: Int) -> Int {
    num = num + i
    return num
}

因此f(1)``f(2)``f(3)``f(4)访问的是同一个num,或者说同一块变量内存

num 初始值为 0
f(1)就等价于 num = num + 1
f(2)就等价于 num = num + 2
f(3)就等价于 num = num + 3
f(4)就等价于 num = num + 4

所以结果为:

1
3
6
10

再思考这种情况:

var f1 = getFn()
print(f1(1))   // 1
print(f1(2))   // 3
print(f1(3))   // 6

var f2 = getFn()
print(f2(1))   // 1
print(f2(2))   // 3
print(f2(3))   // 6

很显然每创建一个getFn函数引用(没错,函数和闭包都是引用类型),Swift 都会为所捕获的num申请一份新的堆空间内存,来保证所有的f1访问的都是同一块内存地址,所有的f2访问的也都是同一块内存地址,但f1f2访问的num堆地址不是同一块!

自动闭包

观察下面这个函数:

// 如果第1个参数大于0则返回之,否则返回第2个参数
func getFirstPositiveNumber(n1: Int, n2: Int) -> Int {
    return n1 > 0 ? n1 : n2
}

调用getFirstPositiveNumber

func getDoubleOfNumber(_ v: Int) -> Int {
    return v * 2
}
getFirstPositiveNumber(n1: 10, n2: 20)  // 10
getFirstPositiveNumber(n1: -10, n2: 20) // 20

上述函数看似很简单,但有一个隐患可以优化:

如果n1 > 0,那么n2是什么根本不重要了,可是编译器还是需要花费开销去"关心"n2。你可能会不以为然,心理嘀咕『不就一个Int,至于么?"』

那下面这个例子呢:

func getNumber() -> Double {
    return Double.pi * 10.0
}
getFirstPositiveNumber(n1: 10.0, n2: getNumber())  // 10.0

既然n1已经>0了,我们为何还要去调用getDoubleOfNumber来计算n2呢?

如果getDoubleOfNumber函数 计算很复杂需要去读取本地数据 甚至 需要联网抓取数据 呢?这种浪费就不能不以为然了吧。

那怎么解决呢?当时是使用闭包:

func getFirstPositiveNumber(n1: Double, n2: () -> Double) -> Double {
    return n1 > 0 ? n1 : n2()
}

n2的类型从Double改为() -> Double,调用时:

getFirstPositiveNumber(n1: 10.0, n2: {
    Double.pi * 10.00
})

或者使用尾随闭包:

getFirstPositiveNumber(n1: 10) {
    Double.pi * 10.00
}

n1 > 0时,闭包的函数体Double.pi * 10.00根本不用执行!完美!

可是每次调用getFirstPositiveNumber都要写闭包会很繁琐,因此Swift标准库提供了自动闭包的语法糖来解决这个问题,getFirstPositiveNumber函数只需要像下面这么写:

func getFirstPositiveNumber1(n1: Double, n2: @autoclosure () -> Double) -> Double {
    return n1 > 0 ? n1 : n2()
}
getFirstPositiveNumber1(n1: 10, n2: Double.pi * 2)

调用时n2会自动写成闭包的形式!

逃逸闭包

func fn1(_ closure: (Int) -> Int) {
    print(closure(10))
}

fn1函数只有一个闭包参数closure,且closurefn1函数体内部直接调用,这时候我们称closure为非逃逸闭包。

如果像下面这么写编译器就会报错:

var c: ((Int) -> Int)?
func fn2(_ closure: (Int) -> Int) {
    c = closure
}

closurefn2作用域外调用,即成为逃逸闭包,可以使用@escaping关键词消除编译器报错:

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

推荐阅读更多精彩内容