闭包是自包含的代码块,可以在代码中被传递和使用。Swift中的闭包与C和Objective-C中代码块(blocks)以及其他语言中的一些匿名函数比较相似。
闭包可以捕获(capturing)和存储其所在上下文中任意常量和变量的引用。这就是所谓的闭合并包裹着这些常量和变量,俗称闭包。Swift会为您管理在捕获过程中涉及到的所有内存操作。
全局和嵌套函数实际上也是特殊的闭包, 闭包采取如下三种形式之一:
• 全局函数是一个有名字但不会捕获任何值的闭包
• 嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包
• 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包
Swift 的闭包表达式拥有简洁的风格,并鼓励在常见场景中进行语法优化,主要优化如下:
• 利用上下文推断参数和返回值类型
• 隐式返回单表达式闭包,即单表达式闭包可以省略 return 关键字
• 参数名称缩写
• 尾随(Trailing)闭包语法
闭包表达式语法
闭包表达式语法有如下一般形式
{ (parameters) -> returnType in
statements
}
闭包表达式语法可以使用常量、变量和inout类型作为参数,不能提供默认值。也可以在参数列表的最后使用可变参数。元组也可以作为参数和返回值。
下面这个例子中使用闭包来代替函数:
reversed = name.sort({ s1: String, s2: String -> Bool in
return s1 > s2
})
在内联闭包表达式中(如上),函数和返回值类型都写在大括号内,而不是大括号外。
闭包的函数体部分由关键字in
引入。该关键字表明闭包的参数和返回值类型已经完成,闭包函数体即将展开。
由于这个闭包的函数体非常短,所以可以写成一行。
reversed = name.sort({ s1: String, s2: String -> Bool in return s1 > s2 })
根据上下文推断类型
因为排序闭包函数是作为sort(_:)
方法的参数传入的, Swift 可以推断其参数和返回值的类型。sort(_:)
方法被一个字符串数组调用,因此其参数必须是(String, String) -> Bool
类型的函数。这意味着(String, String)
和 Bool
类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断,返回箭头( -> )
和围绕在参数周围的括号也可以被省略:
reversed = name.sort({ s1, s2 in return s1 > s2 })
实际上任何情况下,通过内联闭包表达式构造的闭包作为参数传递给函数或方法时,都可以推断出闭包的参数和返回值类型。 这意味着闭包作为函数或者方法的参数时,您几乎不需要利用完整格式构造内联闭包。
单表达式闭包隐式返回
单行表达式闭包可以通过省略return
关键字来隐式返回单行表达式的结果,如下:
reversed = name.sort({ s1, s2 in s1 > s2 })
在这个例子中,sort(_:)
方法的第二个参数函数类型明确了闭包必须返回一个 Bool
类型值。因为闭包函数体只包含了一个单一表达式( s1 > s2 )
,该表达式返回Bool
类型值,因此这里没有歧义,return
关键字可以省略。
参数名称缩写
Swift自动为内联闭包表达式提供了参数名称缩写的功能,可以直接用$0
, $1
, $2
来顺序调用闭包的参数,以此类推。
如果您在闭包表达式中使用参数名称缩写,您可以在闭包参数列表中省略对其的定义,并且对应参数名称缩写的类型会通过函数类型进行推断。in
关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:
reversed = name.sort({ $0 > $1 })
在这个例子中, $0
和$1
表示闭包中第一个和第二个String
类型的参数。
运算符函数
实际上还有一种更简短的方式来撰写上面例子中的闭包表达式。Swift 的String
类型定义了关于大于号(>
)的字符串实现,其作为一个函数接受两个String
类型的参数并返回Bool
类型的值。而这正好与sort(_:)
方法的第二个参数需要的函数类型相符合。因此,您可以简单地传递一个大于号,Swift 可以自动推断出您想使用大于号的字符串函数实现:
reversed = name.sort( > )
尾随闭包
如果需要将一个很长的闭包作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。尾随闭包是一个书写在函数体括号之后的闭包表达式,函数支持将其作为最后一个参数调用。
func someFunctionThatTakesAClosure(closure: () -> void) {
// 函数体部分
}
someFunctionThatTakesAClosure({
// 闭包主体部分
})
someFunctionThatTakesAClosure() {
// 闭包主体部分
}
因为之前的函数可以写成:
reversed = name.sort() { $0 > $1 }
如果函数只需要闭包表达式一个参数,当使用尾随闭包的时候,甚至可以去掉():
reversed = name.sort { $0 > $1 }
下面是一个使用尾随闭包的例子:
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]
let strings = numbers.map {
(var number) -> String in
var output = ""
while number > 0 {
output =digitNames[number % 10]! + output
numbers /= 10
}
return output
}
// strings 常量被推断为字符串类型数组,即 [String]
// 其值为 ["OneSix", "FiveEight", "FiveOneZero"]
注意
字典digitNames
下标后跟着一个叹号(!
),因为字典下标返回一个可选值(optional value),表明该键不存在时会查找失败。在上例中,由于可以确定numbers % 10
总是digitNames
字典的有效下标,因此叹号可以用于强制解包 (force-unwrap) 存储在下标的可选类型的返回值中的String
类型的值。
捕获值
闭包可以在其被定义的上下文中捕获常量或变量。即使定义这些常量或变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。
Swift中,可以捕获值的闭包的最简单形式是嵌套函数,也就是定义在其他函数体内的函数。嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。关于嵌套函数可以查看swift-函数。
闭包是引用类型
无论您将函数或闭包赋值给一个常量还是变量,您实际上都是将常量或变量的值设置为对应函数或闭包的引用。
非逃逸闭包
当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。当你定义接受闭包作为参数的函数时,你可以在参数名之前标注@noescape
,用来指明这个闭包是不允许“逃逸”出这个函数的。将闭包标注@noescape
能使编译器知道这个闭包的生命周期(译者注:闭包只能在函数体中被执行,不能脱离函数体执行,所以编译器明确知道运行时的上下文),从而可以进行一些比较激进的优化。
一种能使闭包“逃逸”出函数的方法是, 将这个闭包保存在一个函数外部定义的变量中。举个例子,很多启动异步操作的函数接受一个闭包参数作为 completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。在这种情况下,闭包需要“逃逸”出函数,因为闭包需要在函数返回之后被调用。例如:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: () -> Void) {
completionHandlers.append(completionHandler)
}
someFunctionWithEscapingClosure(_:)
函数接受一个闭包作为参数,该闭包被添加到一个函数外定义的数组中。如果你试图将这个参数标注为@noescape
,你将会获得一个编译错误。
自动闭包
自动闭包是一种自动创建的闭包, 用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够用一个普通的表达式来代替显式的闭包,从而省略闭包的花括号。
我们经常会调用一个接受闭包作为参数的函数, 但是很少实现那样的函数。举个例子来说,assert(condition:message:file:line:)
函数接受闭包作为它的condition
参数和message
参数;它的condition
参数仅会在 debug 模式下被求值,它的message
参数仅当condition
参数为false
时被计算求值。
自动闭包让你能够延迟求值,因为代码段不会被执行直到你调用这个闭包。延迟求值对于那些有副作用(Side Effect)和代价昂贵的代码来说是很有益处的,因为你能控制代码什么时候执行。