第十四章 Swift 闭包
Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的匿名函数比较相似,全局函数和嵌套函数其实就是特殊的闭包。
闭包的形式有:
全局函数 | 嵌套函数 | 闭包表达式 |
---|---|---|
有名字但不能捕获任何值。 | 有名字,也能捕获封闭函数内的值。 | 无名闭包,使用轻量级语法,可以根据上下文环境捕获值。 |
Swift中的闭包有很多优点:
- 根据上下文推断参数和返回值类型
- 从单行表达式闭包中隐式返回(也就是闭包体只有一行代码,可以省略return)
- 可以使用简化参数名,如$0, $1(从0开始,表示第i个参数...)
- 提供了尾随闭包语法
闭包表达式
闭包表达式是一种利用简洁语法构建内联闭包的方式。 闭包表达式提供了一些语法优化,使得撰写闭包变得简单明了。
//语法
{(parameters) -> ReturnType in
statements
return ReturnType
}
let studname = { print("Swift 闭包实例。") }
studname()
1. 参数名称缩写
闭包的外部参数名必须为_
,您可以直接通过$0
,$1
,$2
来顺序使用闭包的参数。
let names = ["AT", "AE", "D", "S", "BE"]
var reversed = names.sorted( by: { $0 > $1 } ) // $0表示当前循环到元素,$1为$0后面一个,以此类推。
print(reversed) // ["S", "D", "BE", "AT", "AE"]
如果你在闭包中使用参数名称缩写, 您可以在闭包参数列表中省略对其定义, 并且对应参数名称缩写的类型会通过函数类型进行推断。in
关键字同样也可以被省略.
2. 运算符函数
Swift支持一些更简短的方式来使用闭包中的比较。
Swift 的String类型定义了关于大于号 >
的字符串实现,其作为一个函数接受两个String类型的参数并返回Bool类型的值。 而这正好与sort(_:)
方法的第二个参数需要的函数类型相符合。 因此,您可以简单地传递一个大于号,Swift可以自动推断出您想使用大于号的字符串函数实现:
let names = ["AT", "AE", "D", "S", "BE"]
var reversed = names.sorted(by: >)
print(reversed) // ["S", "D", "BE", "AT", "AE"]
3. 尾随闭包
尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。
func someFunctionThatTakesAClosure(closure: () -> Void) {
// 函数体部分
}
// 以下是不使用尾随闭包进行函数调用
someFunctionThatTakesAClosure({
// 闭包主体部分
})
// 以下是使用尾随闭包进行函数调用
someFunctionThatTakesAClosure() {
// 闭包主体部分
}
let names = ["AT", "AE", "D", "S", "BE"]
//尾随闭包
var reversed = names.sorted() { $0 > $1 } // sort() 后的 { $0 > $1} 为尾随闭包。
print(reversed) // ["S", "D", "BE", "AT", "AE"]
注意: 如果函数只需要闭包表达式一个参数,当您使用尾随闭包时,您甚至可以把()省略掉。
reversed = names.sorted { $0 > $1 }
4. 捕获值
闭包可以在其定义的上下文中捕获常量或变量,如果是对象类型则会进行只读复制,其他类型会进行复制,和OC一样。
Swift最简单的闭包形式是嵌套函数,也就是定义在其他函数的函数体内的函数,嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。
func makeIncrementor(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementor() -> Int {
runningTotal += amount
return runningTotal
}
return incrementor
}
/*
incrementor函数并没有获取任何参数,但是在函数体内访问了runningTotal和amount变量。这是因为其通过捕获在包含它的函数体内已经存在的runningTotal和amount变量而实现。
由于没有修改amount变量,incrementor实际上捕获并存储了该变量的一个副本,而该副本随着incrementor一同被存储。
所以我们调用这个函数时会累加:
*/
let incrementByTen = makeIncrementor(forIncrement: 10)
print(incrementByTen()) // 10
print(incrementByTen()) // 20
print(incrementByTen()) // 30
5. 闭包是引用类型
上面的例子中,incrementByTen
是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量值,这是因为函数和闭包都是引用类型。
无论您将函数/闭包赋值给一个常量还是变量,您实际上都是将常量/变量的值设置为对应函数/闭包的引用。 上面的例子中,incrementByTen
指向闭包的引用是一个常量,而并非闭包内容本身。
这也意味着如果您将闭包赋值给了两个不同的常量/变量,两个值都会指向同一个闭包:
func makeIncrementor(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementor() -> Int {
runningTotal += amount
return runningTotal
}
return incrementor
}
let incrementByTen = makeIncrementor(forIncrement: 10)
incrementByTen() // 10
incrementByTen() // 20
incrementByTen() // 30
incrementByTen() // 40
let alsoIncrementByTen = incrementByTen
print(alsoIncrementByTen()) // 50
6. 无主引用和弱引用
为了避免引用循环,Swift为闭包提供了两种方式打断循环。
unowned
:无主引用,被引用的对象不会增加引用计数,并且一定有值(如果原本就有的话),当对象被释放后,再次调用将会抛出异常。
weak
:弱引用,被引用的对象会通过弱引用的方式被引用,此时对象类型变更为可选类型,对象被释放后不会有异常出现。
let block1 = { [unowned self] in
print(self.view.frame)
}
let block2 = { [weak self] in
print(self?.view.frame)
}
7.逃逸闭包
闭包分为逃逸闭包和非逃逸闭包。
- 若闭包的生命周期就在创建它的上下文中,那么它就是非逃逸闭包,其捕获的变量就没有必要增加其引用计数。
- 若闭包被赋值存储起来等待延时调用,为了避免捕获的值被释放,故会对捕获的值进行一份只读拷贝。
逃逸闭包才需要使用unowned
或weak
来避免引用循环。
8. 作为参数和返回值
闭包可以作为参数传递,和OC用法一样。
Swift额外支持了将闭包作为返回值来返回,使得代码能够更加紧凑。
9. 常见的闭包使用场景
- 懒加载
lazy var label: UILabel = {
return UILabel()
}()
- 计算属性
var index: Int {
return 1
}
- 普通闭包
let block = { [unowned self] (a: Int, b: Int) -> Int in
return Int(self.view.frame.minX) + a + b
}
block(1,2)
- 作为参数
func eat(food: ((String,Int) -> ())?) {
}
eat { (food, count) in //尾随闭包
}
- 作为返回值
func eat(food: ((String,Int) -> ())?) -> ((String) -> ()) {
return { (key) -> () in
print(key)
}
}
let block = eat { _ in // 参数可用`_`无视
}
block("Apple") // Apple