什么是闭包?
在Swift中,可以通过func
定义一个函数,也可以通过闭包表达式定义一个函数,闭包是一个捕获了上下文的常量或者是变量的函数。
实现一个加法功能的闭包:
func sum(_ a: Int, _ b: Int) -> Int { a + b }
var add = {
(a: Int, b: Int) -> Int in
return a + b
}
add(10, 20)
复制代码
闭包表达式:
{ (param) -> (returnType) in
//do something
}
复制代码
首先按照我们之前的知识积累, OC 中的 Block 其实是一个匿名函数,所以这个表达式要具备
- 作用域(也就是大括号)
- 参数和返回值
- 函数体(in)之后的代码
Swift 中的闭包即可以当做变量
,也可以当做参数传递
。
var closure : (Int) -> Int = { (a: Int) in return a }
//通过 let 关键字将闭包声明位一个常量(也就意味着一旦赋值之后就不能改变了)
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
关键字 - 参数名称的简写(比如我们的 $0)
- 尾随闭包表达式
下面这些写法都是等效的,但是不建议使用最后这种太简写的,不能很好的理解意思
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: <)
复制代码
捕获值
- 在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 里面输出并没有发生改变
改成通过__block
修饰 a
- (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 里面输出发生改变
- 在 Swift 中捕获局部变量
- 假如运行下面的程序
var a = 10
let closure = {
print("closure=", a)
}
a = 20
print("before", a)
closure()
print("after", a)
复制代码
说明swift值的捕获是在执行的时候再捕获
,当代码执行到 closure(),对变量a进行捕获,捕获到的变量是修改之后的值。
- 假如想实现捕获发生在定义closure内部
var a = 10
let closure = {[a] in
print("closure=", a)
}
a = 20
print("before", a)
closure()
print("after", a)
复制代码
也就是使用 []
,实现捕获列表capturing list
,就实现了捕获发生在closure内部。因为这个时候它已经不是捕获的引用了,而是最初原始值的copy副本。
OC Block 和 Swift 闭包相互调用
- Swift 调用 OC Block,需要在桥接文件
.header
里面引入OC 类
,然后就可以在 Swift 中直接调用 - OC 调用 Swift 闭包,需要在闭包的类中使当前类继承于
NSObject
, 并且闭包表达式使用@objc
修饰,这样就可以调用了
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)
}
// use fileDescriptor...
}
复制代码
这样就不怕哪个分支忘了写,或者中间 throw 个 error,导致 fileDescriptor
没法正常关闭。
- 加/解锁
func foo() {
objc_sync_enter(lock)
defer {
objc_sync_exit(lock)
}
// do something...
}
复制代码
像这种成对调用的方法,可以用 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 方法
可以在super方法之前做一些准备工作
func override foo() {
defer {
super.foo()
}
// some preparation before super.foo()...
}
复制代码
逃逸闭包
什么是逃逸闭包?
当闭包作为一个实际参数传递给一个函数的时候,并且是在函数返回之后调用,我们就说这个闭包逃逸了。当我们声明一个接受闭包作为形式参数的函数时,你可以在形式参数前写 @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, relsase)
- 上下文的内存保存再栈上,不是堆上
自动闭包
- 自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。
- 这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。
- 这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。
- 自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。
- 延迟求值对于那些有副作用(Side Effect)和高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。
var dataArr = ["a", "b", "c", "d", "e"]
print(dataArr.count) // 打印出“5”
let removeStr = { dataArr.remove(at: 0) }
print(dataArr.count) // 打印出“5”
print("debug \(removeStr())!") // Prints "debug a!"
print(dataArr.count) // 打印出“4”
复制代码
在闭包的代码中,dataArr 的第一个元素被移除了,不过在闭包被调用之前,这个元素是不会被移除的。 如果这个闭包永远不被调用,那么在闭包里面的表达式将永远不会执行,那意味着列表中的元素永远不会被移除。
将闭包作为参数传递给函数时,你能获得同样的延时求值行为。
// serve(element:) 函数接受一个返回元素的显式的闭包。
func serve(element elementProvider: () -> String) {
print("debug \(elementProvider())!")
}
serve(element: { dataArr.remove(at: 0) } )
// 打印出“debug Alex!”
复制代码
通过将参数标记为 @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))
复制代码
闭包中的循环引用
比如将一个闭包赋值给类实例的某个属性,并且这个闭包体中又使用了实例,这样会发生引用循环。下面这个例子就是 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)
}
}
复制代码
要想解决这个问题,也是像要打破这种循环,有两种解决办法:
class Client {
//或者换成unowned
weak var server : Server!
init (server : Server) {
self.server = server
self.server.add(client:self)
}
}
复制代码
弱引用weak
:一个变量可以选择不持有对其引用对象的拥有权。弱引用可以是空(nil)
无主引用unowned
:像弱引用,无主引用对引用对象不保持很强的关系。和弱引用不同的是,无主引用总是被设定为一个值。因此,无主引用总是被设定为不可选择的类型。无主引用不可以是空
。