闭包, 一个捕获了全局上下文的常量或者变量的函数
。闭包
在实现上是一个结构体
,它存储了一个函数
(通常是其入口地址)和一个关联的环境(相当于一个符号查找表
)。
一、闭包的使用
全局函数 是一种特殊的闭包,定义一个全局函数,只是当前的全局函数并不捕获值。
func test(){
print("test")
}
函数闭包:下面的函数是一个闭包,函数中的incrementer
是一个内嵌函数
,可以从makeIncrementer
中捕获变量runningTotal
。
func makeIncrementer() -> () -> Int{
var runningTotal = 10
//内嵌函数,也是一个闭包
func incrementer() -> Int{
runningTotal += 1
return runningTotal
}
return incrementer
}
let incre = makeIncrementer()
print(incre())
闭包表达式 / 匿名函数:下面是一个闭包表达式
,即一个匿名函数
,而且是从上下文中捕获变量和常量。
//闭包表达式
{ (param) -> ReturnType in
//方法体
}
闭包表达式特点
:是一个匿名函数
,所有代码都在花括号{}内,参数和返回类型
在in关键字
之前,in关键字
之后是主体内容(类似方法体)。
尾随闭包
当闭包作为函数的最后一个参数
,如果当前的闭包表达式很长,我们可以通过尾随闭包的书写方法来提高代码的可读性。
//闭包表达式作为函数的最后一个参数
func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item2: Int, _ item3: Int)
-> Bool) -> Bool{
return by(a, b, c)
}
//常规写法
test(10, 20, 30, by: {(_ item1: Int, _ item2: Int, _ item3: Int) -> Bool in
return (item1 + item2 < item3)
})
//尾随闭包写法
test(10, 20, 30) { (item1, item2, item3) -> Bool in
return (item1 + item2 < item3)
}
array.sorted
就是一个尾随闭包,且这个函数就只有一个参数
,如下所示:
//array.sorted就是一个尾随闭包
var array = [1, 2, 3]
//1、完整写法
array.sorted { (item1: Int, item2: Int) -> Bool in return item1 < item2}
//2、省略参数类型:通过array中的参数推断类型
array.sorted { (item1, item2) -> Bool in return item1 < item2}
//3、省略参数类型 + 返回值类型:通过return推断返回值类型
array.sorted { (item1, item2) in return item1 < item2}
//4、省略参数类型 + 返回值类型 + return关键字:单表达式可以隐士表达,即省略return关键字
array.sorted { (item1, item2) in item1 < item2}
//5、参数名称简写
array.sorted {return $0 < $1}
//6、参数名称简写 + 省略return关键字
array.sorted {$0 < $1}
//7、最简:直接传比较符号
array.sorted (by: <)
闭包的有点
:
利用上下文推断参数和返回类型
;
单表达式
可以隐式返回,省略return
关键字;
参数名称可以直接使用简写(如$0
,$1
,元组的$0.0
);
尾随闭包可以更简洁的表达。
二、变量捕获
func makeIncrementer() -> () -> Int{
var runningTotal = 10
//内嵌函数,也是一个闭包
func incrementer() -> Int{
runningTotal += 1
return runningTotal
}
return incrementer
}
let incre = makeIncrementer()
print(incre())
print(incre())
print(incre())
//打印结果:11 12 13
结果为什么是累加
的:因为内嵌函数捕获了runningTotal
,不再是单纯的一个变量了。
换一种方式:
print(makeIncrementer()())
print(makeIncrementer()())
print(makeIncrementer()())
//打印结果:11 11 11
老司机应该能猜到结果,下面我们深入分析一下原因。
2.1 SIL
SIL命令:swiftc -emit-sil ${SRCROOT}/SwiftTest/main.swift | xcrun swift-demangle > ./main.sil && open main.sil
1、通过
alloc_box
申请了一个堆
上的空间,并将引用计数地址给了RunningTotal
,将变量存储到堆
上;2、通过
project_box
从堆上取出变量;3、将取出的变量交给
闭包
进行调用。结论
:捕获值的本质是将变量存储到堆上
。
LLDB:
断点
,看到在makeIncrementer
方法内部调用了swift_allocObject
方法。
总结:
1、一个闭包能够从上下文捕获已经定义的常量和变量,并且能够在其函数体内引用和修改这些值,即使这些定义的常量和变量的原作用域不存在;
2、修改捕获值实际是修改堆区中的value值
;
3、当每次重新执行当前函数时,都会重新创建
内存空间。
三、逃逸闭包 & 非逃逸闭包
逃逸闭包
,当闭包作为一个实际参数传递给一个函数时,并且是在函数返回之后
调用,我们就说这个闭包逃逸了。
当声明一个接受闭包作为形式参数的函数时,可以在形式参数前写@escaping
来明确闭包是允许逃逸的。
- 如果用
@escaping
修饰闭包后,我们必须显示的在闭包中使用self
; -
swift3.0
之后,系统默认闭包参数就是被@nonescaping
修饰。
逃逸闭包的使用场景,①延迟调用,②作为属性存储,在后面进行调用。
延迟调用
1、在延迟方法
中调用逃逸闭包
class Animal {
//定义一个闭包属性
var complitionHandler: ((Int)->Void)?
//函数参数使用@escaping修饰,表示允许函数返回之后调用
func makeIncrementer(amount: Int, handler: @escaping (Int)->Void){
var runningTotal = 0
runningTotal += amount
//赋值给属性
self.complitionHandler = handler
//延迟调用
DispatchQueue.global().asyncAfter(deadline: .now()+0.1) {
print("逃逸闭包延迟执行")
handler(runningTotal)
}
print("函数执行完了")
}
func doSomething(){
self.makeIncrementer(amount: 10) {
print($0)
}
}
deinit {
print("Animal deinit")
}
}
var t = Animal()
t.doSomething()
sleep(2)
//打印结果:
//函数执行完了
//逃逸闭包延迟执行
//10
当前方法执行的过程中不会等待闭包执行完成后再执行,而是直接返回,所以当前闭包的生命周期要比方法长
。
作为属性
当闭包作为
存储属性
时,主要有以下几点说明:
1、定义一个闭包属性;
2、在方法中对闭包属性进行赋值;
3、在合适的时机调用(与业务逻辑相关)。
观察上一段代码,complitionHandler
作为Animal的属性
,是在方法makeIncrementer
调用完成后才会调用,这时,闭包的生命周期要
比当前方法的生命周期长
。
逃逸闭包 vs 非逃逸闭包 区别
-
非逃逸闭包:一个接受
闭包作为参数
的函数,闭包是在这个函数结束前
被调用,即可以理解为闭包是在函数作用域结束前被调用。
1.1不会产生循环引用
,因为闭包的作用域在函数作用域内,在函数执行完成后,就会释放闭包捕获的所有对象;
1.2 针对非逃逸闭包,编译器会做优化:省略内存管理调用
;
1.3 非逃逸闭包捕获的上下文保存在栈上
,而不是堆上(官方文档说明)。 -
逃逸闭包:一个接受
闭包作为参数
的函数,逃逸闭包可能会在函数返回之后
才被调用,即闭包逃离了函数的作用域。
2.1 可能会产生循环引用
,因为逃逸闭包中需要显式
的引用self
(猜测其原因是为了提醒开发者,这里可能会出现循环引用了),而self可能是持有闭包变量的(与OC中block
的的循环引用类似);
2.2 一般用于异步函数的返回,例如网络请求。 -
使用建议
:如果没有特别需要,开发中使用非逃逸闭包是有利于内存优化
的,所以苹果把闭包区分为两种,特殊情况时再使用逃逸闭包。 -
总结
:主要区别就是调用时机和内存管理不同
。
四、自动闭包
自动闭包
是一种自动创建的闭包
,用于包装传递给函数作为参数
的表达式。这种闭包不接受任何参数
,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包
的花括号
,用一个普通的表达式来代替显式的闭包
。
有下面一个例子,当condition
为true
时,会打印错误信息,即,如果是false
,当前条件不会执行。
//1、condition为false时,当前条件不会执行
func debugOutPrint(_ condition: Bool, _ message: String){
if condition {
print("cjl_debug: \(message)")
}
}
debugOutPrint(true, "Application Error Occured")
如果字符串
是在某个业务逻辑中获取的
func debugOutPrint(_ condition: Bool, _ message: String){
if condition {
print("animal_debug: \(message)")
}
}
func doSomething() -> String{
print("doSomething")
return "Network Error Occured"
}
//如果传入true
debugOutPrint(true, doSomething())
//打印结果:animal_debug: Network Error Occured
//如果传入false
debugOutPrint(false, doSomething())
//打印结果:doSomething
此时,无论是传入true
还是false
,当前的方法doSomething
都会执行,如果这个方法是一个非常耗时
的操作,这里就会造成一定的资源浪费。所以为了避免这种情况,需要将当前参数修改为一个闭包。
修改:将message
参数修改成一个闭包,需要传入的是一个函数。即,需要时在调用
,延缓
了方法的调用时机。
//3、为了避免资源浪费,将当前参数修改成一个闭包
func debugOutPrint(_ condition: Bool, _ message: () -> String){
if condition {
print("animal_debug: \(message())")
}
}
func doSomething() -> String{
print("doSomething")
return "Network Error Occured"
}
debugOutPrint(true, doSomething)
如果这里既可以传string,也可以传闭包,可以实现吗?
可以通过@autoclosure
将当前的闭包声明成一个自动闭包
,不接收任何参数
,返回值是当前内部表达式的值。所以当传入一个String时,其实就是将String放入一个闭包表达式中
,在调用的时候返回。
//4、将当前参数修改成一个闭包,并使用@autoclosure声明成一个自动闭包
func debugOutPrint(_ condition: Bool, _ message: @autoclosure() -> String){
if condition {
print("cjl_debug: \(message())")
}
}
func doSomething() -> String{
print("doSomething")
return "Network Error Occured"
}
//使用1:传入函数
debugOutPrint(true, doSomething())
//使用2:传入字符串
debugOutPrint(true, "Application Error Occured")
debugOutPrint(true, "Application Error Occured")
这句代码等价于:
//相当于用{}包裹传入的对象,然后返回{}内的值
{
//表达式里的值
return "Network Error Occured"
}