1、闭包是自包含的功能代码块,跟C和Objective-C中的代码块(blocks)和其他一些语言中的匿名函数相似。
2、闭包可以作为函数的参数也可以作为函数的返回值。
3、可以像oc中用于回调和反向传值
一、闭包表达式
闭包表达式可以理解为闭包的表现形式
语法形式为
{
(参数列表) -> 返回值类型 in 函数体代码
}
- 参数可以有多个,多个参数用,隔开
- 参数列表的小括号可以省略
- 返回值类型也可以省略
- 当没有参数时in可以省略
- in可以看作是一个分隔符,将函数体和前面的参数、返回值分割开来
1、有参有返回值
// 有参有返回值
let testOne: (String,String) -> String = {(str1,str2) -> String in
return str1 + str2
}
print(testOne("test","One"))
:的右边是闭包的类型,=右边就是一个闭包的表达式,也可以理解为一个闭包。
=右边是严格按照了闭包表达式来写的,有参数,有括号,有返回值。下面再看下闭包表达式的简写
let testOne = {str1,str2 in
return str1 + str2
}
print(testOne("test","One"))
这个跟上一个是等价的,闭包表达式省去了参数的括号和返回值。:右边的闭包类型省去了是因为swift编译器能自动根据=右边去判断类型
2、无参无返回值
//无参无返回值
let testThree: () -> Void = {
print("testThree")
}
testThree()
:右边类型省去后
let testThree = {
print("testThree")
}
testThree()
因为没有参数in可以省略
二、闭包作为函数参数
func exec(fn: (Int, Int) -> Int, v1: Int, v2: Int) {
print(fn(v1, v2))
}
这个函数有三个参数,第一个参数是一个函数
下面是exec函数的调用
exec(fn: { a, b in
return a + b
}, v1: 1, v2: 2)
exec函数调用时,{}里就是一个闭包表达式,可以看作是第一个参数函数的实现。
这样的函数调用形式看起来很不友好,如果闭包表达式有很多行的话,会更加不友好,不利于代码的阅读。swift提供了一个尾随闭包的概念
1、尾随闭包
- 当函数的最后一个参数是函数时,在函数调用时可以把闭包表达式写在()外面
- 尾随闭包是一个书写在函数括号之后的闭包表达式
- 如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
print(fn(v1, v2))
}
这次把exec函数的第一个参数fn放到了最后,下面可以看下调用方式和上次的有什么不同
exec(v1: 1, v2: 2) { a, b in
return a + b
}
跟上一次的函数调用对比,这次是把闭包表达式写在了函数括号之后,增强了代码的可读性,这就是尾随闭包。
- 可以使用简化参数名,如$0, $1(从0开始,表示第i个参数...)
exec(v1: 1, v2: 2) {
return $0 + $1
}
还有很多的简写就不一一列举了,太简写了也不利于代码阅读
- 如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的语法,可以将函数名后边的圆括号省略
func exec(fn: (Int, Int) -> Int) {
print(fn(1,2))
}
exec { a, b in
return a + b
}
2、逃逸闭包
- 如果一个闭包被作为一个函数的参数,并且在函数执行完之后才被执行,那么这种情况下的闭包就被称为逃逸闭包
- 在参数名的:后面用@escaping来修饰说明逃逸闭包
一般在涉及到异步操作时,闭包放在异步线程里,在这种情况下就会出现逃逸闭包,特别是在网络请求时会出现这种情况
func exec(fn: @escaping () -> ()) {
//延迟5s
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) {
//5s后调用闭包
fn()
}
print("函数执行完毕")
}
exec {
print("闭包执行完毕")
}
这段代码会先打印"函数执行完毕",5秒后再执行闭包打印"闭包执行完毕"
3、自动闭包
- 在参数名的:后面用@autoclosure来修饰说明自动闭包
- @autoclosure会自动将20封装成闭包{20}
- @autoclosure只支持() -> T格式的参数
- @autoclosure并非只支持最后1个参数
- 有@autoclosure和无@autoclosure构成了函数重载
- 空合并运算符??使用了@autoclosure技术
先分析为什么会有自动闭包,自动闭包能实现什么作用
先看下下面的一个函数
func getFirstPositive(_ a: Int, _ b: () -> Int) -> Int {
return a > 0 ? a : b()
}
getFirstPositive(5) {
return 20
}
getFirstPositive函数调用时使用了一个尾随闭包,当参数满足() -> T这个格式时可以写成自动闭包,会使代码阅读起来更直观
func getFirstPositive(_ a: Int, _ b: @autoclosure () -> Int) -> Int {
return a > 0 ? a : b()
}
getFirstPositive(1, 10)
需要注意的是闭包会有推迟执行的特点,会在函数内部调用时才会执行
func getFirstPositive(_ a: Int, _ b: @autoclosure () -> Int) -> Int {
return a > 0 ? a : b()
}
func exec() -> Int {
print("执行了exec")
return 20
}
getFirstPositive(1, exec())
看getFirstPositive(1, exec())函数调用时,很容易误以为exec()就已经执行了函数exec,其实并没有,exec内部没有执行,没有输出执行了exec。这是因为闭包有延迟执行的特点,getFirstPositive函数内部因为a>0返回的结果为a的值,并没有调用到b(),所以exec函数没有执行。只有当getFirstPositive函数内部调用到了b(),exec函数才会被执行
三、闭包对变量捕获
- 闭包可以对外部函数的变量\常量进行捕获
- 闭包捕获时机是在函数执行完,return时再去捕获
- 当函数里有多个闭包时,只会对变量\常量捕获一次,多个闭包对捕获的变量\常量共享
- 闭包不会对全局变量进行捕获
下面由几份代码来说明这几个结论
//MARK: 局部变量捕获
typealias fn = (Int) -> ()
func exec() -> fn {
var num = 0
return {a in
num += a
print(num)
}
}
let fn1 = exec()
fn1(1)
fn1(2)
fn1(3)
fn1、fn2、fn3输出的结果分别是1、3、6。
这是一个函数中返回了一个闭包,在闭包里对num进行了累加并输出结果。
1、第一次调用fn1时,num为0,0加上参数1=1
2、第二次调用fn1时,闭包里的num的值是第一次fn1里的累加结果1,1加上参数2=3
3、第三次调用fn1时,闭包里num是第二次fn1里累加的结果3,3加上参数3=6
从三次调用fn1来看,闭包里num都是保存了上次调用后num的值,这是因为闭包捕获了外部的num,并重新在堆上分配了内存,当执行let fn1 = exec()时,把闭包的内存地址给了fn1,所以每次调用fn1都是调用的同一块内存,同一个闭包,闭包里有保存中捕获后的num的内存地址,所以每次调用都是同一个num
可以把闭包想象成是一个类的实例对象,内存在堆空间,捕获的局部变量\常量就是对象的成员(存储属性),组成闭包的函数就是类内部定义的方法
typealias fn = (Int) -> ()
func exec() -> fn {
var num = 0
return {a in
num += a
print(num)
}
}
let fn1 = exec()
fn1(1)
let fn2 = exec()
fn2(1)
将上面的代码稍微改一下,将exec分别赋值给fn1和fn2,输出的结果为1和1。这个为什么不是跟上面一样累加呢,因为exec分别赋值给了fn1和fn2,fn1和fn2指向的是两个不一样的地址,当每调用一次exec()函数,num会初始化为0
typealias fn = (Int) -> ()
func exec() -> fn {
var num = 0
func plus(a: Int) {
num += a
print(num)
}
num = 6
return plus
}
let fn1 = exec()
fn1(1)
上面这份代码输出的num又是多少呢?答案是7
这还是一个局部变量捕获的问题,闭包会在函数执行完,return的时候才会去捕获num,此时num已经由0变为6,所以执行fn1(1)输出结果为7
typealias fn = (Int) -> ()
func exec() -> (fn, fn) {
var num = 0
func plus(a: Int) {
num += a
print("plus:", num)
}
func minus(a: Int) {
num -= a
print("minus:", num)
}
return (plus, minus)
}
let (p, m) = exec()
p(5)
m(4)
这份代码函数返回了一个元祖,元祖里是两个闭包,两个闭包里面都调用了num,输出的结果为:
plus: 5
minus: 1
因为当函数里有多个闭包时,只会对变量\常量捕获一次,多个闭包对捕获的变量\常量共享
因为当函数里有多个闭包时,只会对变量\常量捕获一次,多个闭包对捕获的变量\常量共享。在调用m(4)时,前面已经调用过p(5),此时num已经变为5,所以当调用m(4),输出结果为1。
四、闭包中的循环引用及解决办法
大家可以看这位大神的文章Swift中闭包的简单使用,文中有详细的解析
参考文章
Swift 闭包的定义和使用
Swift中闭包的简单使用
逃逸闭包、非逃逸闭包