闭包是自包含的函数代码块,可以在代码中被传递和使用。Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的匿名函数比较相似。
闭包可以捕获和存储其所在上下文中任意常量和变量的引用。这就是所谓的闭合并包裹着这些常量和变量,俗称闭包。Swift 会为您管理在捕获过程中涉及到的所有内存操作。
在函数章节中介绍的全局和嵌套函数实际上也是特殊的闭包,闭包采取如下三种形式之一:
- 全局函数是一个有名字但不会捕获任何值的闭包
- 嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包
- 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包
Swift 的闭包表达式拥有简洁的风格,并鼓励在常见场景中进行语法优化,主要优化如下:
- 利用上下文推断参数和返回值类型
- 隐式返回单表达式闭包,即单表达式闭包可以省略return关键字
- 参数名称缩写
- 尾随(Trailing)闭包语法
闭包表达式(Closure Expressions)
闭包表达式是一种利用简洁语法构建内联闭包的方式。闭包表达式提供了一些语法优化,使得撰写闭包变得简单明了。
下面的闭包表达式示例使用sort(_:)
方法对一个String
类型的数组进行字母逆序排序.以下是初始数组值:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
sort(_:)
方法接受一个闭包,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来表明当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值前面,排序闭包函数需要返回true
,反之返回false
。
该例子对一个String
类型的数组进行排序,因此排序闭包函数类型需为(String, String) -> Bool
。
提供排序闭包函数的一种方式是撰写一个符合其类型要求的普通函数,并将其作为sort(_:)
方法的参数传入:
func backwards(s1: String, s2: String) -> Bool {
return s1 > s2
}
var reversed = names.sort(backwards)
// reversed 为 ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
闭包表达式语法(Closure Expression Syntax)
{ (parameters) -> returnType in
statements
}
闭包表达式语法可以使用常量、变量和inout
类型作为参数,不能提供默认值。也可以在参数列表的最后使用可变参数。元组也可以作为参数和返回值。
下面的例子展示了之前backwards(_:_:)
函数对应的闭包表达式版本的代码:
reversed = names.sort({ (s1: String, s2: String) -> Bool in
return s1 > s2
})
需要注意的是内联闭包参数和返回值类型声明与backwards(_:_:)
函数类型声明相同。在这两种方式中,都写成了(s1: String, s2: String) -> Bool
。然而在内联闭包表达式中,函数和返回值类型都写在大括号内,而不是大括号外。
闭包的函数体部分由关键字in
引入。该关键字表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。
由于这个闭包的函数体部分如此短,以至于可以将其改写成一行代码:
reversed = names.sort( { (s1: String, s2: String) -> Bool in return s1 > s2 } )
reversed = names.sort( { s1, s2 in return s1 > s2 } )
//根据上下文推断类型(Inferring Type From Context)
reversed = names.sort( { s1, s2 in s1 > s2 } )
//单行表达式闭包可以通过省略`return`关键字来隐式返回单行表达式的结果
reversed = names.sort( { $0 > $1 } )
//参数名称缩写(Shorthand Argument Names)in关键字也同样可以被省略
reversed = names.sort(>)
运算符函数(Operator Functions)
实际上还有一种更简短的方式来撰写上面例子中的闭包表达式。Swift 的String
类型定义了关于大于号(>
)的字符串实现,其作为一个函数接受两个String
类型的参数并返回Bool
类型的值。而这正好与sort(_:)
方法的第二个参数需要的函数类型相符合。因此,您可以简单地传递一个大于号,Swift 可以自动推断出您想使用大于号的字符串函数实现:
reversed = names.sort(>)
尾随闭包(Trailing Closures)
如果您需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用:
func someFunctionThatTakesAClosure(closure: () -> Void) {
// 函数体部分
}
// 以下是不使用尾随闭包进行函数调用
someFunctionThatTakesAClosure({
// 闭包主体部分
})
// 以下是使用尾随闭包进行函数调用
someFunctionThatTakesAClosure() {
// 闭包主体部分
}
如果函数只需要闭包表达式一个参数,当您使用尾随闭包时,您甚至可以把()
省略掉:
reversed = names.sort { $0 > $1 }
捕获值(Capturing Values)
闭包可以在其被定义的上下文中捕获常量或变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。
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
let incrementBySeven = makeIncrementor(forIncrement: 7)
incrementBySeven()
// 返回的值为7
闭包是引用类型(Closures Are Reference Types)
无论您将函数或闭包赋值给一个常量还是变量,您实际上都是将常量或变量的值设置为对应函数或闭包的引用。
这也意味着如果您将闭包赋值给了两个不同的常量或变量,两个值都会指向同一个闭包:
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 返回的值为50
非逃逸闭包(Nonescaping Closures)
当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。当你定义接受闭包作为参数的函数时,你可以在参数名之前标注@noescape
,用来指明这个闭包是不允许“逃逸”出这个函数的。将闭包标注@noescape
能使编译器知道这个闭包的生命周期(ps:闭包只能在函数体中被执行,不能脱离函数体执行,所以编译器明确知道运行时的上下文),从而可以进行一些比较激进的优化。
func someFunctionWithNoescapeClosure(@noescape closure: () -> Void) {
closure()
}
一种能使闭包“逃逸”出函数的方法是,将这个闭包保存在一个函数外部定义的变量中。举个例子,很多启动异步操作的函数接受一个闭包参数作为 completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。在这种情况下,闭包需要“逃逸”出函数,因为闭包需要在函数返回之后被调用。
将闭包标注为@noescape
使你能在闭包中隐式地引用self
。
自动闭包(Autoclosures)
自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够用一个普通的表达式来代替显式的闭包,从而省略闭包的花括号。
我们经常会调用一个接受闭包作为参数的函数,但是很少实现那样的函数。举个例子来说,assert(condition:message:file:line:)
函数接受闭包作为它的condition
参数和message
参数;它的condition
参数仅会在 debug 模式下被求值,它的message
参数仅当condition
参数为false
时被计算求值。 *PS:这个场景仔细体会一下会明白为什么会有逃逸的闭包存在
自动闭包让你能够延迟求值,因为代码段不会被执行直到你调用这个闭包。延迟求值对于那些有副作用(Side Effect)和代价昂贵的代码来说是很有益处的,因为你能控制代码什么时候执行。下面的代码展示了闭包如何延时求值。
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// prints "5"
let customerProvider = { customersInLine.removeAtIndex(0) }
print(customersInLine.count)
// prints "5"
print("Now serving \(customerProvider())!")
// prints "Now serving Chris!"
print(customersInLine.count)
// prints "4"
尽管在闭包的代码中,customersInLine
的第一个元素被移除了,不过在闭包被调用之前,这个元素是不会被移除的。如果这个闭包永远不被调用,那么在闭包里面的表达式将永远不会执行,那意味着列表中的元素永远不会被移除。请注意,customerProvider
的类型不是String
,而是() -> String
,一个没有参数且返回值为String
的函数。
将闭包作为参数传递给函数时,你能获得同样的延时求值行为。
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serveCustomer(customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serveCustomer( { customersInLine.removeAtIndex(0) } )
// prints "Now serving Alex!"
serveCustomer(_:)
接受一个返回顾客名字的显式的闭包。下面这个版本的serveCustomer(_:)
完成了相同的操作,不过它并没有接受一个显式的闭包,而是通过将参数标记为@autoclosure
来接收一个自动闭包。现在你可以将该函数当做接受String类型参数的函数来调用。customerProvider
参数将自动转化为一个闭包,因为该参数被标记了@autoclosure
特性。
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serveCustomer(@autoclosure customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serveCustomer(customersInLine.removeAtIndex(0))
// prints "Now serving Ewa!"
ps: 过度使用autoclosures会让你的代码变得难以理解。上下文和函数名应该能够清晰地表明求值是被延迟执行的。
@autoclosure
特性暗含了@noescape
特性,这个特性在非逃逸闭包一节中有描述。如果你想让这个闭包可以“逃逸”,则应该使用@autoclosure(escaping)
特性.
// customersInLine is ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(@autoclosure(escaping) customerProvider: () -> String) {
customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.removeAtIndex(0))
collectCustomerProviders(customersInLine.removeAtIndex(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!"
在上面的代码中,collectCustomerProviders(_:)
函数并没有调用传入的customerProvider
闭包,而是将闭包追加到了customerProviders
数组中。这个数组定义在函数作用域范围外,这意味着数组内的闭包将会在函数返回之后被调用。因此,customerPtovider
参数必须允许“逃逸”出函数作用域。
要点总结
逃逸闭包对于那些消耗很大但并不一定需要执行的代码是有很大帮助的。这一点可以从以上那个系统assert函数的例子说明里体会到。