Swift 闭包
闭包
闭包是可以在你的代码中被传递和引用的功能性独立代码块。Swift 中的闭包和 C 以及 Objective-C 中的 blocks 很像,还有其他语言中的匿名函数也类似。
闭包 能够 捕获和 存储 定义在其上下文中的任何 常量和变量 的引用,这也就是所谓的闭合并包裹那些常量和变量,因此被称为“闭包”,Swift 能够为你处理所有关于捕获的内存管理的操作。
1 闭包表达式
闭包表达式是一种在简短行内就能写完闭包的语法。闭包表达式为了缩减书写长度又不失易读明晰而提供了一系列的语法优化。下边的闭包表达式栗子通过使用几次迭代展示 sorted(by:)方法的精简来展示这些优化,每一次都让相同的功能性更加简明扼要。
1.1 Sorted 方法调用
let names = ["Chris","Alex","Ewa","Barry","Daniella"]
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames = ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
1.2 闭包表达式语法
闭包表达式语法有如下的一般形式:
{ (parameters) -> (return type) in
statements
}
闭包表达式语法能够使用常量形式参数、变量形式参数和输入输出形式参数,但不能提供默认值。可变形式参数也能使用,但需要在形式参数列表的最后面使用。元组也可被用来作为形式参数和返回类型。
in 关键字 导入闭包的函数整体部分,这个关键字表示闭包的形式参数类型和返回类型定义已经完成,并且闭包的函数体即将开始。
Sorted 方法,使用闭包, (函数 backward 匿名了)
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
闭包的函数体特别短, 以至于能够只用一行来书写:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )
示例中 sorted(by:) 方法的整体部分调用保持不变,一对圆括号仍然包裹函数的所有实际参数。然而,这些实际参数中的一个变成了现在的行内闭包。
1.3 从语境中推断类型
由于排序闭包为实际参数来传递给方法,Swift 就能推断它的形式参数类型和返回类型。 sorted(by:) 方法是在字符串数组上调用的,所以它的形式参数必须是一个 (String, String) -> Bool 类型的函数。这意味着 (String, String)和 Bool 类型不需要写成闭包表达式定义中的一部分。
因为 所有的类型 都能被推断,返回箭头 ( ->) 和 围绕在形式参数名周围的括号 也能被省略:
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
当把闭包作为行内闭包表达式传递给函数或方法时,形式参数类型和返回类型都可以被推断出来。所以说,当闭包被用作函数的实际参数时你都不需要用完整格式来书写行内闭包。
1.4 从单表达式闭包隐式返回
单表达式闭包能够通过从它们的声明中 删掉 return 关键字来 隐式返回 它们单个表达式的结果,前面的栗子可以写作:
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
这里, sorted(by:) 函数类型的实际参数已经明确必须通过闭包返回一个 Bool 值。因为闭包的结构包含返回 Bool 值的单一表达式 (s1 > s2),因此没有歧义, return 关键字可省略。
1.5 简写的实际参数名
Swift 自动对行内闭包提供简写实际参数名,你也可以通过 1 , $2 等名字来引用闭包的实际参数值。 in 关键字也能被省略,
reversedNames = names.sorted(by: { $0 > $1 } )
1.6 运算符函数
Swift 的 String 类型定义了关于大于号( >)的特定字符串实现,让其作为一个有两个 String 类型形式参数的函数并返回一个 Bool 类型的值。
sorted(by:) 方法的第二个形式参数需要的函数相匹配。
因此,传递一个 大于号,Swift 将推断你想使用 大于号 特殊字符串 函数实现:
reversedNames = names.sorted(by: >)
2 尾随闭包
将一个 很长的闭包表达式 作为 函数最后一个实际参数 传递给函数,使用尾随闭包将
增强函数的可读性.
尾随闭包 是一个被书写在函数形式参数的括号外面(后面)的闭包表达式,但它仍然是这个 函数的实际参数。当你使用 尾随闭包表达式 时,不需要把第一个尾随闭包写对应的实际参数标签。函数调用可包含多个尾随闭包,但下边的例子展示了
单一尾随闭包的写法:
func someFunctionThatTakesAClosure(closure:() -> Void){
//function body goes here
}
//here's how you call this function without using a trailing closure
someFunctionThatTakesAClosure(closure: {
//closure's body goes here
})
//here's how you call this function with a trailing closure instead
someFunctionThatTakesAClosure() {
// trailing closure's body goes here
}
2.1 字符串排列闭包 可以作为一个 尾随闭包 被书写在 sorted(by:) 方法的括号外面:
reversedNames = names.sorted() { $0 > $1 }
2.2 闭包表达式作为 函数的 唯一实际参数 传入,而你又使用了尾随闭包的语法,省略 函数名后边 圆括号:
reversedNames = names.sorted { $0 > $1 }
2.3 长闭包 不能被写成一行时, 尾随闭包 就显得非常有用了
Swift 的 Array 类型中有一个以闭包表达式为唯一的实际参数的 map(:) 方法。数组中每一个元素调用一次该闭包,并且返回该元素所映射的值(有可能是其他类型)。具体的映射方式和返回值的类型由你传入 map(:)的闭包来指定。
在给数组中每一个元素应用了你提供的闭包后, map(_:)方法返回一个新的数组,数组中包涵与原数组一一对应的映射值。
总之,使用带有尾随闭包的 map(_:) 方法将包含 Int 值的数组转换成包含 String 值的数组。这个数组 [16,58,510] 被转换成一个新的数组 ["OneSix","FiveEight","FiveOneZero"]
let digitNames = [
0: "Zero",1: "One",2: "Two", 3: "Three",4: "Four",
5: "Five",6: "Six",7: "Seven",8: "Eight",9: "Nine"
]
let numbers = [16,58,510]
上面的代码创建了一个 整数数字 到它们 英文名字 之间的 映射字典,同时定义了一个 将被转换成字符串的 整数数组。
你现在可以利用 numbers 数组来创建一个 String 类型的数组。通过给数组的 map(_:)方法传入闭包表达式来实现,这个形式参数将以尾随闭包的形式来书写
let strings = numbers.map { (number) -> String in
var number = number
var output = ""
repeat {
output = digitNames[number % 10]! + output
number /= 10
} while number > 0
return output
}
在上面这个例子中尾随闭包语法在函数后整洁地封装了具体的闭包功能,而不再需要将整个闭包包裹在 map(_:) 方法的括号内。
2.4 如果函数接收多个闭包,你可省略第一个 尾随闭包的实际参数标签,但要给后续的尾随闭包写标签。比如说,下面的函数给照片墙加载图片:
func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
if let picture = download("photo.jpg", from: server) {
completion(picture)
} else {
onFailure()
}
}
当你调用这个函数来加载图片,你需要提供两个闭包。第一个闭包是图片下载成功后的回调。第二个闭包是报错回调,给用户显示错误。
在这个例子中, loadPicture(from:completion:onFailure:) 函数把它的网络任务派遣到后台执行,然后等任务结束就调用这两个回调之一。这么写函数能让你整洁地安排代码,单独处理网络错误,避免处理成功的用户界面混为一谈,取代了用一个闭包处理两种状况的操作。
loadPicture(from: someServer) { picture in
someView.currentPicture = picture
} onFailure: {
print("Couldn't download the next picture.")
}
3 捕获值
一个闭包能够从上下文捕获已被定义的常量和变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍能够在其函数体内引用和修改这些值。
在 Swift 中,一个能够捕获值的闭包最简单的模型是内嵌函数,即被书写在另一个函数的内部。一个内嵌函数能够捕获外部函数的实际参数并且能够捕获任何在外部函数的内部定义了的常量与变量。
这里有个命名为 makeIncrement 的函数栗子,其中包含了一个名叫 incrementer 一个内嵌函数。这个内嵌 incrementer() 函数能在它的上下文捕获两个值, runningTotal 和 amount 。在捕获这些值后,通过 makeIncrement 将 incrementer作为一个闭包返回,每一次调用 incrementer 时,将以 amount作为增量来增加 runningTotal :
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
makeIncrementer 的返回类型是 () -> Int ,意思就是比起返回一个单一的值,它返回的是一个函数。这个函数没有返回任何形式参数,每调用一次就返回一个 Int 值
makeIncrementer 定义了一个名叫 incrementer 的内嵌函数,表明实际增加量,这个函数直接把 amount 增加到 runningTotal ,并且返回结果。
incrementer() 函数是没有任何形式参数, runningTotal 和 amount 不是来自于函数体的内部,而是通过捕获主函数的 runningTotal 和 amount 把它们内嵌在自身函数内部供使用。当调用 makeIncrementer 结束时通过引用捕获来确保不会消失,并确保了在下次再次调用 incrementer 时, runningTotal 将继续增加。
这有个使用 makeIncrementer 的栗子:
let incrementByTen = makeIncrementer(forIncrement: 10)
这个例子定义了一个叫 incrementByTen 的常量,该常量指向一个每次调用会加 10 的函数。调用这个函数多次得到以下结果:
incrementByTen()
//return a value of 10
incrementByTen()
//return a value of 20
incrementByTen()
//return a value of 30
第二个 incrementer ,它将会有一个新的、独立的 runningTotal 变量的引用:
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7
再次调用原来增量器 incrementByTen 继续增加它自己的变量 runningTotal 的值,并且不会影响 incrementBySeven 捕获的变量 runningTotal 值:
incrementByTen()
// returns a value of 40
闭包是引用类型
在上面例子中, incrementBySeven 和 incrementByTen 是常量,但是这些常量指向的闭包仍可以增加已捕获的变量 runningTotal 的值。这是因为函数和闭包都是引用类型。
无论你什么时候赋值一个函数或者闭包给常量或者变量,你实际上都是将常量和变量设置为对函数和闭包的引用。这上面这个例子中,闭包选择 incrementByTen 指向一个常量,而不是闭包它自身的内容。
这也意味着你赋值一个闭包到两个不同的常量或变量中,这两个常量或变量都将指向相同的闭包:
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
//return a value of 50
4 逃逸闭包
当闭包作为一个实际参数传递给一个函数的时候,我们就说这个闭包逃逸了,因为它是在函数返回之后调用的。当你声明一个接受闭包作为形式参数的函数时,你可以在形式参数前写 @escaping 来明确闭包是允许逃逸的。
闭包可以逃逸的一种方法是被储存在定义于函数外的变量里。比如说,很多函数接收闭包实际参数来作为启动异步任务的回调。函数在启动任务后返回,但是闭包要直到任务完成——闭包需要逃逸,以便于稍后调用。举例来说:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
4.1 逃逸闭包修改变量
要捕获 self ,就明显地写出来,或者在闭包的捕获列表中包含 self 。显式地写出 self 能让你更清楚地表达自己的意图,并且提醒你去确认这里有没有引用循环
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"
completionHandlers.first?()
print(instance.x)
// Prints "100"
这里是一个通过把 self 放在闭包捕获列表来捕获 self 的 doSomething() 版本:
class SomeOtherClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { [self] in x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}
SomeOtherClass()
// x = 10
如果 self 是结构体或者枚举的实例,你就可以隐式地引用 self 。总之,当 self 是结构体或者枚举的实例时,逃逸闭包不能捕获可修改的 self 引用。如同结构体和枚举是值类型中描述的那样,结构体和枚举不允许共享可修改性。
struct SomeStruct {
var x = 10
mutating func doSomething() {
someFunctionWithNonescapingClosure { x = 200 } // Ok
someFunctionWithEscapingClosure { x = 100 } // Error
}
}
someFunctionWithEscapingClosure 调用在上文中是错误的,因为它在一个异变方法中,所以 self 是可编辑的。这就违反了逃逸闭包不能捕获结构体的可编辑引用 self 的规则。
5 自动闭包
自动闭包是一种自动创建的用来把作为实际参数传递给函数的表达式打包的闭包。它不接受任何实际参数,并且当它被调用时,它会返回内部打包的表达式的值。这个语法的好处在于通过写普通表达式代替显式闭包而使你省略包围函数形式参数的括号。
调用一个带有自动闭包的函数是很常见的,但实现这类函数就不那么常见了。比如说, assert(condition:message:file:line:) 函数为它的 condition 和 message 形式参数接收一个自动闭包;它的 condition 形式参数只有在调试构建是才评判,而且 message 形式参数只有在 condition 是 false 时才评判。
自动闭包允许你延迟处理,因此闭包内部的代码直到你调用它的时候才会运行。对于有副作用或者占用资源的代码来说很有用,因为它可以允许你控制代码何时才进行求值。下面的代码展示了闭包如何延迟求值。
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Prints "5"
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"
尽管 customersInLine 数组的第一个元素以闭包的一部分被移除了,但任务并没有执行直到闭包被实际调用。如果闭包永远不被调用,那么闭包里边的表达式就永远不会求值。注意 customerProvider 的类型不是 String 而是 () -> String ——一个不接受实际参数并且返回一个字符串的函数。
5.1 当你传一个闭包作为实际参数到函数的时候,你会得到与延迟处理相同的行为。
接收一个明确的返回下一个客户名称的闭包
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"
5.2 另一个版本的 serve(customer:) 执行相同的任务但是不使用明确的闭包而是通过 @autoclosure 标志标记它的形式参数使用了自动闭包。
现在你可以调用函数就像它接收了一个 String 实际参数而不是闭包。实际参数自动地转换为闭包,因为 customerProvider 形式参数的类型被标记为 @autoclosure 标记。
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"
5.3 自动闭包逃逸,同时使用 @autoclosure 和 @escaping
不是调用传入后作为 customerProvider 实际参数的闭包, collectCustomerProviders(_:) 函数把闭包追加到了 customerProviders 数组的末尾。数组声明在函数的生效范围之外,也就是说数组里的闭包有可能在函数返回之后执行。结果, customerProvider 实际参数的值必须能够逃逸出函数的生效范围。
// customersInLine is ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))
print("Collected \(customerProviders.count) closures.")
// Prints "Collected 2 closures."
for customerProvider in customerProviders {
print("Now serving \(customerProvider())!")
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"