一、什么是闭包?
在 Swift 中,可以通过 func 定义一个函数,也可以通过闭包表达式定义一个函数,闭包是一个捕获了上下文的常量或者是变量的函数。闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来作为参数传值。 Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的匿名函数 比较相似,全局函数和嵌套函数其实就是特殊的闭包。 闭包的形式有:
全局函数 嵌套函数 闭包表达式 有名字但不能捕获任何值 有名字,也能捕获封闭函数内的值 无名闭包,使用轻量级语法,可以根据上下文环境捕获值
Swift 中的闭包有很多优化的地方:
从单行表达式闭包中隐式返回(也就是闭包体只有一行代码,可以省略 return);
可以使用简化参数名,如 $0, $1(从 0 开始,表示第i个参数…);
提供了尾随闭包语法(Trailing closure syntax)。 实现一个加法功能的闭包:
func sum ( _ a: Int , _ b: Int ) -> Int { a + b }
var add = {
( a: Int , b: Int ) -> Int in
return a + b
}
add ( 10 , 20 )
{ ( parameters) -> return type in
statements
}
OC 中的 Block 其实是一个匿名函数,所以这个表达式要具备:
Swift 中的闭包即可以当做变量,也可以当做参数传递:
var closure : ( Int ) -> Int = { ( a: Int ) in return a }
let closure: ( Int ) -> Int
func test ( param : ( ) -> Int ) { print ( param ( ) ) }
var a = 10
test { ( ) -> Int in
a += 1
return a
}
二、常用的闭包类型
① 尾随闭包
尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。如果把闭包表达式作为函数的最后一个参数,当前的闭包表达式很长,可以通过尾随闭包的书写方式来提高代码的可读性。
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 ) { item1, item2, item3 in
return item1 + item2 > item3
}
使用闭包表达式能更简洁的传达信息。当然闭包表达式的好处有很多:
单表达式可以隐式返回,既省略 return 关键字;
如下所示,写法都是等效的,但是不建议使用最后这种太简写的,不太好理解:
var array = [ 1 , 2 , 3 ]
array. sort ( by: { ( item1 : Int , item2: Int ) -> Bool in return item1 < item2 } )
array. sort ( by: { ( item1, item2) -> Bool in return item1 < item2 } )
array. sort ( by: { ( item1, item2) in return item1 < item2 } )
array. sort{ ( item1, item2) in item1 < item2 }
array. sort{ return $0 < $1 }
array. sort{ $0 < $1 }
array. sort ( by: < )
② 捕获值
闭包可以在其定义的上下文中捕获常量或变量。即使定义这些常量和变量的原域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。 Swift 最简单的闭包形式是嵌套函数,也就是定义在其他函数的函数体内的函数。嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。 在 OC 中,我们捕获值都知道需要在局部变量前加上 __block 修饰符,比如定义一个 Block,修改局部变量的值:
- ( void) testBlock {
NSInteger a = 1 ;
void ( ^ block) ( void) = ^ {
NSLog ( @"block=%ld" , a) ;
} ;
a += 1 ;
NSLog ( @"before block=%ld" , a) ;
block ( ) ;
NSLog ( @"after block=%ld" , a) ;
}
运行结果发现 a 的值在 block 里面输出并没有发生改变:
before block= 2
block= 1
after block= 2
- ( void) testBlock {
__block NSInteger a = 1 ;
void ( ^ block) ( void) = ^ {
NSLog ( @"block=%ld" , a) ;
} ;
a += 1 ;
NSLog ( @"before block=%ld" , a) ;
block ( ) ;
NSLog ( @"after block=%ld" , a) ;
}
运行结果发现 a 的值在 block 里面输出发生改变:
before block= 2
block= 2
after block= 2
在 Swift 中捕获局部变量,首先运行下面的程序:
var a = 10
let closure = {
print ( "closure = " , a)
}
a = 20
print ( "before" , a)
closure ( )
print ( "after" , a)
before 20
closure = 20
after 20
说明 Swift 值的捕获是在执行的时候再捕获,当代码执行到 closure(),对变量 a 进行捕获,捕获到的变量是修改之后的值。假如想实现捕获发生在定义 closure 内部:
var a = 10
let closure = { [ a] in
print ( "closure = " , a)
}
a = 20
print ( "before" , a)
closure ( )
print ( "after" , a)
before 20
closure = 10
after 20
也就是使用 [],实现捕获列表 capturing list,就实现了捕获发生在 closure 内部,这是因为这个时候它已经不是捕获的引用了,而是最初原始值的 copy 副本。 OC Block 和 Swift 闭包相互调用:
Swift 调用 OC Block,需要在桥接文件.header里面引入 OC 类,然后就可以在 Swift 中直接调用;
OC 调用 Swift 闭包,需要在闭包的类中使当前类继承于 NSObject, 并且闭包表达式使用 @objc 修饰,这样就可以调用。
③ 逃逸闭包
当闭包作为一个实际参数传递给一个函数的时候,并且是在函数返回之后调用,就可以说这个闭包逃逸。当声明一个接受闭包作为形式参数的函数时,可以在形式参数前写 @escaping 来明确闭包是允许逃逸的。不需要在函数结束前被调用,可以等到特定时机时才被调用。 如下所示,当闭包参数传给属性使用时:
这样就会报错,需要声明称逃逸闭包,添加 @escaping 修饰:
class Closure {
var handle: ( ( Int ) -> Void ) ?
func test ( _ a: Int , handler: @escaping ( Int ) -> Void ) {
self . handle = handler
}
}
class Closure {
var handle: ( ( Int ) -> Void ) ?
func test ( _ a: Int , handler: @escaping ( Int ) -> Void ) {
var b = 10
DispatchQueue . main. async {
handler ( b)
}
}
}
逃逸闭包生命周期常于函数,函数退出的时候,逃逸闭包的引用仍被其他对象持有,不会在函数结束时释放。如果在闭包中使用了当前对象,这样就会导致循环引用发生内存泄露。
④ 非逃逸闭包
func testNoEscaping ( _ f: ( ) -> Void ) {
f ( )
}
func test ( ) -> Int {
var age = 18
testNoEscaping {
age += 20
}
return 30
}
test ( )
这个就是一个非逃逸闭包,当函数调用完之后这个闭包也就消失了。并且非逃逸闭包还有以下优点:
编译器更多性能优化 (retain, release) ;
⑤ 自动闭包
自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。 自动闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。 自动闭包的便利语法能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。 自动闭包能够延迟求值,因为直到调用这个闭包,代码段才会被执行。 延迟求值对于那些有副作用(Side Effect)和高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。
var dataArr = [ "a" , "b" , "c" , "d" , "e" ]
print ( dataArr. count)
let removeStr = { dataArr. remove ( at: 0 ) }
print ( dataArr. count)
print ( "debug \( removeStr ( ) ) !" )
print ( dataArr. count)
⑥ 复制代码
在闭包的代码中,dataArr 的第一个元素被移除了,不过在闭包被调用之前,这个元素是不会被移除的。如果这个闭包永远不被调用,那么在闭包里面的表达式将永远不会执行,那意味着列表中的元素永远不会被移除。 将闭包作为参数传递给函数时,获得同样的延时求值行为:
func serve ( element elementProvider: ( ) -> String ) {
print ( "debug \( elementProvider ( ) ) !" )
}
serve ( element: { dataArr. remove ( at: 0 ) } )
通过将参数标记为 @autoclosure 来接收一个自动闭包,可以将该函数当作接受 String 类型参数(而非闭包)的函数来调用。elementProvider 参数将自动转化为一个闭包,因为该参数被标记了 @autoclosure 特性。
func serve ( element elementProvider: @autoclosure ( ) -> String ) {
print ( "debug \( elementProvider ( ) ) !" )
}
serve ( element: dataArr. remove ( at: 0 ) )
可以看出用 @autoclosure 修饰后,这个闭包参数可以传入字符串或者闭包:
serve ( element: { dataArr. remove ( at: 0 ) } )
serve ( element: dataArr. remove ( at: 0 ) )
三、defer 关键字用法
defer block 里的代码会在函数 return 之前执行,无论函数是从哪个分支 return 的,还是有 throw,还是自然而然走到最后一行。如果多个 defer 语句出现在同一作用域中,它们执行的顺序和添加的顺序是相反的。
① try catch 结构
func foo ( ) {
defer {
print ( "finally" )
}
do {
throw NSError ( )
print ( "impossible" )
} catch {
print ( "handle error" )
}
}
不管 do block 是否 throw error,有没有 catch 到,还是 throw 出去了,都会保证在整个函数 return 前执行 defer。在这个例子里,就是先 print 出 “handle error” 再 print 出 “finally”。
② 清理工作、回收资源
func foo ( ) {
let fileDescriptor = open ( url. path, O_EVTONLY )
defer {
close ( fileDescriptor)
}
}
这样就不怕哪个分支忘了写,或者中间 throw 个 error,导致 fileDescriptor 没法正常关闭。 加/解锁:
func foo ( ) {
objc_sync_enter ( lock)
defer {
objc_sync_exit ( lock)
}
}
像这种成对调用的方法,可以用 defer 把它们放在一起,这样结构就特别清晰。
③ 调 completion block
有时候一个函数分支比较多,可能某个小分支 return 之前就忘了调 completion block,结果导致出一个不易发现的 bug,这样写就很好的避免了这个问题:
func foo ( ) {
defer {
self . completion = nil
}
if ( succeed) {
self . completion ( . success ( result) )
} else {
self . completion ( . error ( error) )
}
}
④ 调 super 方法
func override foo ( ) {
defer {
super . foo ( )
}
}
四、闭包中的循环引用
比如将一个闭包赋值给类实例的某个属性,并且这个闭包体中又使用了实例,这样会发生引用循环。如下所示,Server 引用 Client ,而 Client 又引用 Server 导致的循环引用:
class Server {
var clients : [ Client ] = [ ]
func add ( client: Client ) {
self . clients. append ( client)
}
}
class Client {
var server : Server !
var server : Server !
init ( server : Server ) {
self . server = server
self . server. add ( client: self )
}
}
要想解决这个问题,也是像要打破这种循环,有两种解决办法:
弱引用weak:一个变量可以选择不持有对其引用对象的拥有权,弱引用可以是空(nil);
无主引用unowned:像弱引用,无主引用对引用对象不保持很强的关系。和弱引用不同的是,无主引用总是被设定为一个值。因此,无主引用总是被设定为不可选择的类型。无主引用不可以是空。
class Client {
weak var server : Server !
init ( server : Server ) {
self . server = server
self . server. add ( client: self )
}
}