什么是闭包?
Swift 中的闭包是一种可以在代码中被传递和使用的功能强大的特性。它们可以捕获并存储任意封闭上下文中定义的任何常量或变量的引用,这些引用在闭包被调用时仍然有效。
严格来说,函数也是一种闭包,因为它们能够捕获封闭作用域中的常量和变量。不过,在 Swift 中,我们通常使用术语“闭包”来指代那些没有名字的、能够捕获上下文中变量或常量的块。
Swift 中的闭包有三种形式:
- 全局函数是一个有名字但不能捕获任何值的闭包
- 嵌套函数是一个有名字并可以捕获其封闭函数内部变量或常量值的闭包
- 闭包表达式是一个轻量级语法的闭包,可以捕获其上下文中变量或常量的值
闭包表达式
闭包表达式是一种创建匿名函数的方式,通常被用于内联函数操作,例如:对集合元素进行排序、过滤、映射等。
语法
闭包表达式的语法非常简洁,由以下几部分组成:
- 一对花括号
{ }
- 参数列表,可以是空的,或者有一个或多个参数,每个参数之间用逗号
,
分隔 - 关键字
in
- 闭包体,即执行的代码块
1
2
3
4
5
6
7
8
9
// 无参数闭包
{ () -> ReturnType in
statements
}
// 有参数闭包
{ (parameters) -> ReturnType in
statements
}
其中,ReturnType 表示闭包返回值类型。
例子
让我们看一个例子,假设我们有一个整数数组,我们想要将其按升序排序。这里我们可以使用 sorted(by:)
方法,该方法接受一个闭包作为参数,该闭包定义了数组元素比较规则。
1
2
3
4
5
6
7
8
let numbers = [10, 8, 7, 5, 1, 2, 6, 4, 3, 9]
let sortedNumbers = numbers.sorted(by: {
(number1: Int, number2: Int) -> Bool in
return number1 < number2
})
print(sortedNumbers) // prints [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
这里我们定义了一个闭包,它接受两个整数参数 number1
和 number2
,并返回一个布尔值表示哪个数字更小。在闭包体中,我们只需要比较这两个数字的大小,并返回结果即可。
在这个例子中,我们使用了 sorted(by:)
方法将数组按照自定义比较规则排序,得到了一个新的数组 sortedNumbers
,其中的元素按照升序排列。
省略参数和返回类型
对于简单的闭包,我们可以省略参数和返回类型,Swift 会自动推断出类型信息。
1
let sortedNumbers = numbers.sorted { number1, number2 in number1 < number2 }
在这个例子中,我们省略了参数的类型声明和返回值的类型声明。另外,由于 sorted
方法只接收一个闭包表达式作为参数,我们可以将花括号写在方法的后面。
省略参数甚至连关键字都不需要
如果闭包体中只有一个表达式,我们甚至可以完全省略参数和关键字 in
。
1
let sortedNumbers = numbers.sorted { $0 < $1 }
在这个例子中,我们省略了参数和关键字 in
,并使用 $0
和 $1
分别表示第一个和第二个参数。
尾随闭包
如果闭包是作为函数的最后一个参数传递的,我们可以将闭包表达式写在括号外面,这样代码看起来更加清晰易读。这种写法被称为“尾随闭包”。
1
let sortedNumbers = numbers.sorted() { $0 < $1 }
在这个例子中,我们将闭包表达式写在了 sorted()
方法的后面,使得代码更加简洁明了。
注意事项
需要注意的是,当我们使用尾随闭包时,如果闭包表达式不止一行,我们需要将其写在花括号 {}
内部,并且需要在闭包表达式前面添加一个换行符。例如:
1
2
3
4
5
let sortedNumbers = numbers.sorted {
(number1, number2) -> Bool in
return number1 < number2
}
print(sortedNumbers) // prints [1, 2, 4, 5, 7]
在这个例子中,我们仍然使用了 sorted()
方法和尾随闭包来对数组进行排序。但是,由于闭包表达式比较复杂,我们将其写在了花括号内部,并在其前面添加了一个换行符。在闭包表达式内部,我们使用了完整的参数列表 (number1, number2) -> Bool
来表示闭包的参数和返回值类型,并且使用了多行语句来计算排序的结果。
捕获值
在闭包表达式中,我们可以捕获其上下文中的变量或常量,即使当上下文被销毁时,仍然可以访问它们。
例子
假设我们有一个函数 makeAdder
,它返回一个接受一个整数参数并返回该整数与某个常量相加的函数。我们可以使用闭包来实现这个功能。
1
2
3
4
5
6
func makeAdder(constant: Int) -> ((Int) -> Int) {
return { (number: Int) in number + constant }
}
let adder = makeAdder(constant: 10)
print(adder(2)) // prints 12
在这个例子中,我们定义了一个函数 makeAdder
,它接受一个整数参数 constant
,并返回一个闭包,该闭包接受一个整数参数 number
,并返回 number + constant
的结果。我们用 makeAdder(constant: 10)
创建了一个 adder
函数,并将其存储在变量中。我们可以调用 adder(2)
来计算 2 + 10
的值,并打印结果。
请注意,在闭包中,我们访问了 constant
变量,这个变量是定义在 makeAdder
函数的上下文中的。尽管 makeAdder
函数已经返回了,但是我们仍然可以在 adder
闭包中访问到 constant
,因为闭包捕获并存储了常量的引用。
隐式捕获 self
在 Swift 中,如果我们在闭包中访问了对象的属性或者调用了对象的方法,Swift 会自动捕获该对象的引用。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
class MyClass {
var value = 0
func doSomething() {
let closure = { [self] in
print(self.value)
}
closure()
}
}
let myObject = MyClass()
myObject.doSomething() // prints 0
在这个例子中,我们定义了一个类 MyClass
,它有一个属性 value
和一个方法 doSomething
。我们在 doSomething
方法中定义了一个闭包,该闭包访问了 self.value
属性,并将闭包存储在变量 closure
中。在闭包内部,我们使用了 [self] in
的语法来显式地捕获 self
,以避免循环引用问题。我们调用 closure()
来执行闭包,并打印出 self.value
的值。
循环引用问题
由于闭包能够捕获其上下文中的变量或常量,如果我们不小心将闭包传递给了其他可能存在循环引用的对象,就容易导致内存泄漏问题。例如,假设我们有一个视图控制器 MyViewController
,它有一个属性 button
,当用户点击按钮时,会弹出一个提示框,提示框中显示当前视图控制器的名称。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyViewController: UIViewController {
var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
let alert = UIAlertController(title: "Hello",message: "My name is (self)", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
present(alert, animated: true, completion: nil)
}
}
let viewController = MyViewController()
viewController.button = UIButton()
// ... configure button ...
在这个例子中,我们定义了一个视图控制器 MyViewController
,它有一个属性 button
,该属性是一个按钮。在 viewDidLoad
方法中,我们将按钮的点击事件与 buttonTapped
方法绑定。在 buttonTapped
方法中,我们创建了一个提示框 alert
,并使用 self
显示当前视图控制器的名称。
虽然这个例子看起来很简单,但是它存在一个潜在的循环引用问题。由于闭包捕获了 self
,当提示框弹出时,视图控制器不能够被销毁,因为提示框仍然持有对视图控制器的引用,从而导致内存泄漏。
为了避免这种情况,我们可以使用 [weak self]
语法来显式地声明 self
为弱引用,并在闭包中使用可选绑定来访问视图控制器实例。例如:
1
2
3
4
5
6
7
8
@objc func buttonTapped() {
guard let strongSelf = self else { return }
let alert = UIAlertController(title: "Hello", message: "My name is \(strongSelf)", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
present(alert, animated: true, completion: nil)
}
在这个例子中,我们定义了一个弱引用 weak self
,并使用可选绑定 guard let
来获取强引用 strongSelf
。在闭包中,我们使用 strongSelf
来表示视图控制器实例,这样即使视图控制器被销毁,也不会导致内存泄漏。
逃逸闭包
在某些情况下,闭包可能会在函数返回之后才被执行,这种闭包被称为“逃逸闭包”。例如,如果一个函数接受一个异步回调作为参数,并在未来某个时间点调用该回调,则该回调可能会逃逸。为了声明一个逃逸闭包,我们需要在参数类型前添加 @escaping
关键字。
1
2
3
4
5
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
completionHandler()
}
}
在这个例子中,我们定义了一个函数 someFunctionWithEscapingClosure
,它接受一个逃逸闭包 completionHandler
。在函数体中,我们使用 GCD 的 asyncAfter
方法模拟一个异步操作,并在 1 秒钟后调用 completionHandler
闭包。
注意,在逃逸闭包中,我们通常需要显式地标记 self
为弱引用,以避免循环引用问题。
自动闭包
自动闭包是一种特殊类型的闭包,它能够延迟求值。换句话说,自动闭包只有在被调用时才会计算其表达式的值。
1
2
3
4
5
6
7
func printIfDebug(_ message: @autoclosure () -> String) {
#if DEBUG
print(message())
#endif
}
printIfDebug("This message will only be printed in debug mode.")
在这个例子中,我们定义了一个函数 printIfDebug
,它接受一个自动闭包 message
。在函数体中,我们使用了 #if DEBUG
的编译指令来检查是否处于调试模式,如果是,则打印 message()
的结果。
注意,在使用自动闭包时需要注意一些细节。首先,由于自动闭包是延迟求值的,因此我们需要在其表达式前面添加 @autoclosure
关键字来告诉编译器将该表达式转换为自动闭包。其次,由于自动闭包是一个闭包,因此我们可以使用传统的闭包语法来访问其上下文中的变量或常量,例如:
1
2
3
4
5
6
7
8
9
10
11
12
func foo(condition: Bool, ifTrue: @autoclosure () -> Void, ifFalse: @autoclosure () -> Void) {
if condition {
ifTrue()
} else {
ifFalse()
}
}
var flag = true
foo(condition: flag, ifTrue: { print("true") }, ifFalse: { print("false") })
flag = false
foo(condition: flag, ifTrue: { print("true") }, ifFalse: { print("false") })
在这个例子中,我们定义了一个函数 foo
,它接受一个布尔型参数 condition
和两个自动闭包 ifTrue
和 ifFalse
。在函数体中,我们根据 condition
的值来调用相应的闭包。
注意,在调用 foo
函数时,我们使用了传统的闭包语法来定义 ifTrue
和 ifFalse
闭包。由于这些闭包被声明为自动闭包,因此它们将在被调用时才会计算其表达式的值。