Swift闭包

1.闭包

闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来作为参数传值。

1.1.语法

1
2
3
{(parameters) -> return type in
statements
}

示例:

1
2
3
4
5
let divide = {(param1: Int, param2: Int) -> Int in 
return param1 / param2
}
let result = divide(200, 20)
print (result) // 输出:10

1.2.优化

Swift对闭包做了很多优化:

  • 根据上下文推断参数和返回值类型;
  • 从单行表达式闭包中隐式返回(闭包体只有一行代码,可以省略return);
  • 可以使用简化参数名,如$0, $1(从0开始,表示第i个参数);
  • 提供了尾随闭包语法(Trailing closure syntax);

示例:

版本1:对数组内元素做排序

1
2
3
4
5
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

版本2:使用闭包

1
2
3
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})

版本3:写在一行

1
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )

版本4:根据上下文推断参数和返回值类型

1
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

版本5:闭包体只有一行代码,可以省略return关键字

1
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

版本6:使用简化参数名

1
reversedNames = names.sorted(by: { $0 > $1 } )

甚至还可以继续简化:

1
reversedNames = names.sorted(by: >)

版本7:使用尾随闭包语法

1
reversedNames = names.sorted() { $0 > $1 }

2.@escaping

A closure is said to escape a function when the closure is passed as an argument to the function, but is called after the function returns. When you declare a function that takes a closure as one of its parameters, you can write @escaping before the parameter’s type to indicate that the closure is allowed to escape.

当闭包作为函数的参数,在函数return之后被调用时,我们就说这个闭包从函数中逃离,即逃逸闭包,使用@escaping来标示。

这种情况常见于函数中发起了一个异步请求并把闭包作为异步操作的回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 逃逸闭包示例
class NetHelper { //网络工具类

var statusOK = false
func httpRequest(withUrl url:String,
succeesCallback: @escaping (Data?,URLResponse?)->(),
failCallback: @escaping (Error?)->()) {
//逃逸闭包必须用@escaping声明;
//否则编译时报错 Escaping closure captures non-escaping parameter 'succeesCallback'
guard !url.isEmpty else{
print("+++URL is nil~")
return
}
print("++task start")
let urlT:URL? = URL(string: url)
let task = URLSession.shared.dataTask(with: urlT!) {(data:Data?, response:URLResponse?, error:Error?) in
if error != nil {
self.statusOK = false // 逃逸闭包中使用到闭包所在类型时 需要显式的调用self 以便提醒自己捕获了self, 不写编译器会报错!
failCallback(error) //调用逃逸闭包
}else{
self.statusOK = true
succeesCallback(data,response) //调用逃逸闭包
}
}
task.resume() // 开始任务
}
}
// 发起网络请求
var netHelper = NetHelper()
netHelper.httpRequest(withUrl: "https://www.baidu.com", succeesCallback: { (data, response) in
print("+++Network finished successfully~\n response:\(response!)")
}) { (error) in
print("++++error:\(error!)")
}

3.autoclosure

An autoclosure is a closure that’s automatically created to wrap an expression that’s being passed as an argument to a function. It doesn’t take any arguments, and when it’s called, it returns the value of the expression that’s wrapped inside of it. This syntactic convenience lets you omit braces around a function’s parameter by writing a normal expression instead of an explicit closure.

自动闭包,一种自动创建的闭包,用来把作为函数参数的表达式自动封装成闭包

示例1:

1
2
3
4
5
func logIfTrue(predicate: ()->Bool){
if predicate() {
print("TRUE")
}
}

示例分析:

  1. 函数接受一个闭包作为参数;
  2. 闭包不接受任何参数;
  3. 闭包被调用时,返回一个值;
  4. 值为true时,执行打印;

示例调用:

1
2
3
4
logIfTrue(predicate: { () in
return 2 > 1
}
)

优化闭包的调用:

1
logIfTrue(predicate: {2 > 1}) //写成一行并省略return关键字

继续优化,使用尾随闭包写法:

1
logIfTrue{ 2 > 1}

但调用时不管怎么优化,要么书写起来十分麻烦,要么表达上不太清晰,于是自动闭包登场了。

1
2
3
4
5
func logIfTrue(predicate: @autoclosure ()->Bool){
if predicate() {
print("TRUE")
}
}

这里改换了原方法的参数,在闭包的类型前加上@autoclosure关键字,再次调用:

1
logIfTrue( predicate: 2 > 1)

Swift 会把2 > 1这个表达式自动转换为()->Bool类型的闭包。

自动闭包的好处是:

  1. 调用时写法简单,表意清楚;
  2. 允许延迟处理,对于闭包内有副作用或占用资源的代码,直到你调用闭包时才会运行。

示例2:

1
2
3
4
5
6
7
8
9
10
11
12
var customersInLine = ["Chris","Alex","Ewa","Barry","Daniella"]

//定义block,其类型为()->String,此时尚未执行
let customProvider = {customersInLine.remove(at: 0)}

//定义函数,接受一个()->String类型的block作为参数,使用非自动闭包的写法
func serve(custom customerProvider:() ->String){
print("Now serving\(customerProvider())!")
}
// 调用
serve(custom: {customersInLine.remove(at: 0)})
// 打印结果 "Now serving Chris!"

自动闭包的写法:

1
2
3
4
5
6
//将闭包参数标记为@autoclosure
func serve(custom customerProvider: @autoclosure () ->String ){
print("Now serving \(customerProvider())!")
}
//调用函数时,不需要再写花括号{},@autoclosure会根据上面的定义,自动将其转换为闭包
serve(custom: customersInLine.remove(at: 0))

注意:自动闭包不接受任何参数,只有形如()->T的参数才能使用这个特性进行简化。

4.捕获变量

4.1.捕获引用

A closure can capture constants and variables from the surrounding context in which it is defined. The closure can then refer to and modify the values of those constants and variables from within its body, even if the original scope that defined the constants and variables no longer exists.

As an optimization, Swift may instead capture and store a copy of a value if that value is not mutated by a closure, and if the value is not mutated after the closure is created.

Swift also handles all memory management involved in disposing of variables when they are no longer needed.

OC 中 block 会捕获变量,且捕获的是变量的值。

Swift 的闭包也会自动捕获其上下文中定义的变量,但默认捕获的是变量的引用,这样就可以在闭包内修改它们的值。换句话说,Swift 闭包中变量的默认行为与 OC 中__block 变量一致。

#示例:

1
2
3
4
5
6
7
8
9
10
11
// Block 引用变量
func blockRetain() {
var num = 1
let block1 = { // 最简单的闭包 内部引用了变量
num += 1
print("\(num)")
}
num += 1
block1()
}
blockRetain() // 打印 "3"

num是局部变量,它在block1中和之后都被修改了,而这两处改变也都影响了最终打印的信息。这说明block1中是对num变量进行了引用,而非值的复制,这与OC中 block 对变量的捕获有很大的不同。

4.2.强制捕获值

如果不想被引用而是被复制,则可以使用捕获列表

#示例:

1
2
3
4
5
6
7
8
9
10
11
// Block 捕获变量
func blockCapture() {
var num = 1
let block2 = { [num] in //声明捕获列表 捕获变量的值 而非引用
//num += 1 // 此处会报错:Left side of mutating operator isn't mutable: 'num' is an immutable capture
print("\(num)")
}
num += 1
block2()
}
blockCapture() // 打印 "1"

定义捕获列表之后,num在闭包中被捕获,但这次是被复制且成为了一个常量,不能在闭包内被修改。闭包之后的修改也并未影响到闭包内的打印结果,这才有点像OC中的 block。

Swift 出于性能考虑会对闭包做一些优化,比如它会自动判断你是否在闭包内或闭包外修改了变量,如果没有则会直接持有一份该变量的拷贝。

5.循环引用

5.1.原因

A strong reference cycle can also occur if you assign a closure to a property of a class instance, and the body of that closure captures the instance. This capture might occur because the closure’s body accesses a property of the instance, such as self.someProperty, or because the closure calls a method on the instance, such as self.someMethod(). In either case, these accesses cause the closure to “capture” self, creating a strong reference cycle.

与 OC 中的 block 类似,Swift 闭包也会强引用被它捕获的对象,从而引发可能的循环引用问题。

比如对象持有一个闭包属性,而闭包体中通过self.调用了对象的属性或方法,从而捕获了self 本身,造成循环引用。

#示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ViewController: UIViewController {

var name :String? = "s"
var sBlock:(()->())? //定义闭包属性,VC强引用闭包

override func viewDidLoad() {
super.viewDidLoad()
sBlock = {
//闭包访问VC的其他成员,闭包捕获并强引用self对象
self.name = "x"
}
}

deinit{
print("++ deinited !")//因为闭包的循环引用,这里析构并不会执行
}
}

5.2.解决方案1:捕获列表

You resolve a strong reference cycle between a closure and a class instance by defining a capture list as part of the closure’s definition.Each item in a capture list is a pairing of the weak or unowned keyword with a reference to a class instance (such as self) or a variable initialized with some value (such as delegate = self.delegate!).

捕获列表也可以解决闭包的循环引用问题,把被捕获的变量标记为weakunowned即可。

  • 给带参数的闭包定义捕获列表:
1
2
3
4
var aClosure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
//闭包具体内容
}
  • 给不带参数的闭包定义捕获列表:
1
2
3
4
var aClosure: () -> Int = {
[unowned self, weak delegate = self.delegate!] in
//闭包具体内容
}

所以,上面#示例1中的问题可以这样解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ViewController: UIViewController {

var name :String? = "s"
var sBlock:(()->())?

override func viewDidLoad() {
super.viewDidLoad()
sBlock = {
[unowned self] in //定义捕获列表
self.name = "x"
}
}

deinit{
print("++ deinited !") //能正常析构
}
}

VC虽然强引用了闭包,但是闭包对VC的引用变成了弱引用,不增加VC的引用计数,当指向VC的其他强引用都被移除后,其引用计数为0,即可正常销毁。

区分weakunowned

  • weak

A weak reference is a reference that does not keep a strong hold on the instance it refers to, and so does not stop ARC from disposing of the referenced instance.

Use a weak reference when the other instance has a shorter lifetime—that is, when the other instance can be deallocated first.

  • unowned

Like a weak reference, an unowned reference does not keep a strong hold on the instance it refers to. Unlike a weak reference, however, an unowned reference is used when the other instance has the same lifetime or a longer lifetime.

Use an unowned reference only when you are sure that the reference always refers to an instance that has not been deallocated.

If you try to access the value of an unowned reference after that instance has been deallocated, you’ll get a runtime error.

weakunowned的作用类似,都是用来解决循环引用问题。区别在于:

1.生命周期:

weak 对象的生命周期一般 < weak 对象所在的类的实例的生命周期,当访问该 weak 对象时它可能已经被释放了,比如 delegate、房子中的租客。因此,weak 修饰的属性一定是optional值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}

class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}

var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

// 相互持有
john!.apartment = unit4A
unit4A!.tenant = john

// 看看是否能调用各自的析构函数
john = nil
unit4A = nil

日志:

1
2
John Appleseed is being deinitialized
Apartment 4A is being deinitialized

说明两个对象都已经顺利释放了~

unowned 对象的生命周期一般 >= unowned 对象所在的类的实例的生命周期。比如 Customer 与 CreditCard,人可能没有信用卡,但信用卡一定得有个主人,Customer的生命周期比 CreditCard 长。因此,unowned 修饰的属性不能是optional值,也不能指向 nil。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}

class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}

// 相互持有
var john: Customer? = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
// 尝试看Customer和CreditCard对象是否能顺利析构
john = nil

日志:

1
2
John Appleseed is being deinitialized
Card #1234567890123456 is being deinitialized

两个实例都顺利析构并释放内存~

2.野指针问题

weak 修饰可选对象,当引用的对象被释放时,可选对象自动变成nil,继续访问该对象时不会闪退。

unowned 相当于OC中的unsafe_unretained,也不会增加引用计数,其引用的对象被释放后,它依然会保持对已被释放对象的一个无效引用,继续访问该对象会闪退。

5.3.解决方案2:用结构体

循环引用,从其命名来看实际上是两个问题:

  • 循环
  • 引用

即对象之间出现了相互引用的怪圈。在解决此类问题时,我们的第一反应往往是使用weakunowned来弱引用对象,从而打破这个环,这解决了第一个问题;

其实我们也可以从第二个问题来入手:仔细回想一下,我们所见到的循环引用一般都是出现在两个或多个引用类型之间,比如闭包之间。所以换个角度来想,如果将引用类型改成值类型,那么也就不存在相互引用的情况了,比如可能的话,将某些改成值类型的结构体来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct Customer { // 将此类改成由结构体来实现
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
}

class CreditCard {
let number: UInt64
let customer: Customer // 这里也不再需要 unowned 修饰了
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit {
print("Card #\(number) is being deinitialized")
}
}

// playground中定义的方法
func tryStruct(){
var customer = Customer(name: "XXX")
let card = CreditCard(number: 10, customer: customer)
customer.card = card;
}

调用:

1
tryStruct()

日志:

1
Card #10 is being deinitialized

分析:

  • 客户对象是结构体,作为信用卡的参数时是值的拷贝而非引用,因此不存在相互引用一说;
  • 作为值类型的客户对象,在出了方法体之后被自动释放。

综上,解决循环引用问题时,可以从弱化引用和替换成值类型两处入手~


相关参考:

#©Swift:ARC


Swift闭包
https://davidlii.cn/2018/08/15/swift-block.html
作者
Davidli
发布于
2018年8月15日
许可协议