swift学习笔记 ⑥ —— 闭包

Swift学习笔记 - 文集

闭包,就是能够读取其他函数内部变量的函数。Swift 中的闭包与 C 和 OC 中的 block 以及其他编程语言中的匿名函数类似。Swift 中的闭包有以下几种形式:

  • 全局函数:有名称,但是不能捕获任何值的闭包
  • 嵌套函数:有名称,也能捕获封闭函数内的值的闭包
  • 闭包表达式:没有名称,用轻量级语法编写,可以从周围的上下文中捕获值的闭包

一、闭包表达式

闭包表达式是一种利用简介语法构建内联闭包的方式。在 Swift 中,可以通过关键字 func 来定义一个函数,也可以通过闭包表达式定义一个函数。

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

例如定义一个函数,传入两个参数求和:

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

上面我们通过关键字 func 来定义的函数,我们也可以通过闭包表达式来定义:

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

闭包表达式的简写

闭包表达式还提供了一些语法优化,使得撰写闭包变得简单明了。例如从上下文中推断参数和返回值类型、单闭包表达式的隐式返回、速记参数名称、尾随闭包语法等。下面我们用一个例子的几种简写方式来帮助理解。

我们定义一个函数,然后将上文中求和的闭包表达式作为函数的参数,来求出函数另外两个参数的和。执行函数就能计算出和:

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

func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
    print(fn(v1, v2))
}
exec(v1: 20, v2: 30, fn: fun) // 50

在调用求和函数exec时,我们通过简写,可以省略函数外部声明的闭包表达式fun,将闭包表达式直接写在函数内部:

exec(v1: 10, v2: 20, fn: {
    (v1: Int, v2: Int) -> Int in
    return v1 + v2
}) // 30

由于闭包表达式能够从上下文中推断参数和返回值类型,那么我们还可以简写成:

exec(v1: 10, v2: 20, fn: {
    v1, v2 in return v1 + v2
}) // 30

我们甚至连return都可以省略:

exec(v1: 10, v2: 20, fn: {
    v1, v2 in v1 + v2
}) // 30

由于我们定义的函数只是简单的将两个参数进行相加,那么就可以用$符号来代替参数名:

exec(v1: 10, v2: 20, fn: { $0 + $1 }) // 30

甚至可以不用任何参数,仅用一个+符号来表示求和:

exec(v1: 10, v2: 20, fn: + ) // 30

尾随闭包

尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。

上文的exec函数在定义时就是将一个闭包表达式作为最后一个参数,我们在调用exec函数时都是将闭包表达式放在函数的()内来调用。如果我们采用尾随闭包,那么调用exec函数就可以将闭包表达式放在函数的()的外面:

exec(v1: 10, v2: 20) {
    v1, v2 in v1 + v2
}

如果将一个很长的闭包表达式作为函数的最后一个实参,那么使用尾随闭包就可以增强函数的可读性。

闭包表达式的应用

如果我们在日常开发中相对一个数组进行排序,可以通过系统提供的sort函数或者sorted函数直接实现。例如:

var arr = [10, 23, 1, 45, 39]
arr.sort()
print(arr)  // [1, 10, 23, 39, 45]

可以看出系统提供的sort函数是升序排序,如果我们想降序排序呢?Swift 中系统就提供了一个函数,来让我们自定义排序是降序还是升序。

func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool)

这个函数需要传入两个参数,会返回Bool类型的返回值。

var arr = [10, 23, 1, 45, 39]
/* v1 > v2
 * 返回true,v1排在v2前面
 * 返回false,v1排在v2后面
 */
func cmp(v1: Int, v2: Int) -> Bool {
    return v1 > v2
}

arr.sort(by: cmp)
print(arr) // [45, 39, 23, 10, 1]

二、闭包

一个函数和它所捕获的变量或者常量环境组合起来,我们称之为闭包。闭包一般指的是在函数内部的函数,它捕获的是外层函数的局部变量或者常量。

typealias Fn = (Int) -> Int

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

我们定义一个函数Fn,并将函数Fn作为另外一个函数getFn的返回值。那么例子中的plusnum就构成了闭包。

在内存空间中,闭包是保存在堆空间的。我们可以把闭包当做一个类的实例对象,捕获的局部变量或常量就是实例对象的属性,组成闭包的函数就是这个类内部定义的方法。

我们来看一下下面的代码,调用上面例子中的getFn函数:

var fn1 = getFn()
print(fn1(1)) // 1
print(fn1(2)) // 3
print(fn1(3)) // 6
var fn2 = getFn()
print(fn2(5)) // 5
print(fn2(6)) // 11
print(fn2(7)) // 18

由于闭包是保存在堆空间中的,所以当我们调用getFn函数时,系统就会为函数内部的闭包在堆空间中分配一块内存以用来保存捕获的局部变量num。所以我们在执行fn1fn2时才能完成打印。而在执行var fn1 = getFn()var fn2 = getFn()时,系统会分配一块新的内存空间。

三、自动闭包

我们首先来看下面的例子,一个函数传入两个参数,如果第一个参数大于 0 就返回第一个参数,否则就返回第二个参数:

func getFirstPositive(_ v1: Int, _ v2: Int) -> Int {
    return v1 > 0 ? v1 : v2
}

getFirstPositive(10, 12) // 10
getFirstPositive(0, 20) // 20
getFirstPositive(-1, 14) // 14

代码很简单,也实现了我们上面的需求。但是这段代码实际是有问题的,原则上来将如果我们传入的第一个参数大于 0 ,那么就不要再继续判断第二个数了,但是实际上编译器会继续判断第二个参数。我们声明一个函数来验证一下:

func getNum() -> Int {
    let a = 10
    let b = 20
    return a + b
}

getFirstPositive(20, getNum())

当我们将getNum()函数作为第二个参数传入函数时,在getNum()函数内部打上断点,运行发现进入断点了。

此时我们就可以把函数的第二个参数v2设置为一个函数,这样可以避免编译器进行的多余的判断:

func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int {
    return v1 > 0 ? v1 : v2()
}

我们再执行就会发现如果第一个参数大于 0 ,就不会再进行第二个参数的判断。我们将之前的Int类型的参数v2改成了函数类型,这样就可以让参数v2延迟加载。如果我们在参数v2的函数中的代码很复杂或者开销很大,延迟加载就能帮我们提高效率。

当然,Swift 中提供了自动闭包这一技术,进行了编译器优化,就是使用@autoclosure关键字修饰参数v2,提高了代码的可读性:

func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
    return v1 > 0 ? v1 : v2()
}

我们在调用函数时,就可以直接调用。这样编译器也不会再去判断第二个参数:

getFirstPositive(1, 14) // 1

要注意的是,@autoclosure关键字只能修饰() -> T格式的参数,也就是说修饰的函数必须是无参带返回值的函数。当然@autoclosure关键字并非只支持最后一个参数,如果上面例子中getFirstPositive函数在v2参数后面还有参数,@autoclosure关键字也可以修饰v2参数。我们在之前的文章中介绍的空合并运算符 ?? 也是使用了@autoclosure技术。

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

推荐阅读更多精彩内容