闭包
闭包是自包含的函数代码块,可以在代码中被传递和使用。Swift中的闭包与C和Objective-C中的代码块
(blocks
)以及其他一些编程语言中的匿名函数
比较相似。
闭包可以捕获和存储其所在上下文中任意常量和变量的引用。这就是所谓的闭合并包裹着这些常量和变量,俗称闭包
。Swift会为您管理在捕获过程中涉及到的所有内存操作。
全局和嵌套函数实际上也是特殊的闭包,闭包有三种形式
Swift的闭包表达式拥有简洁的风格,并鼓励在常见场景中进行语法优化,主要优化如下:
- 利用上下文推断参数和返回值类型
- 隐式返回单表达式闭包,即单表达式闭包可以省略return关键字
- 参数名称缩写
- 尾随(Trailing)闭包语法
闭包表达式
嵌套函数是一个在较复杂函数中方便进行命名和定义自包含代码模块的方式。当然,有时候撰写小巧的没有完整定义和命名的类函数结构也是很有用处的,尤其是在您处理一些函数并需要将另外一些函数作为该函数的参数时。
闭包表达式是一种利用简洁语法构建内联闭包的方式。闭包表达式提供了一些语法优化,使得撰写闭包变得简单明了。
sort方法
Swift标准库提供了名为sort
的方法,会根据您提供的用于排序的闭包函数将已知类型数组中的值进行排序。一旦排序完成,sort(_:)
方法会返回一个与原数组大小相同,包含同类型元素且元素已正确排序的新数组。原数组不会被sort(_:)
方法修改。
sort(_:)
方法接受一个闭包,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来表明当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值前面,排序闭包函数需要返回true
,反之返回false
。
提供排序闭包函数的一种方式是撰写一个符合其类型要求的普通函数,并将其作为sort(_:)
方法的参数传入
let data = [3,2,5,1,6,89,4]
func fun1(a: Int,b: Int) -> Bool {
return a < b // a <= b???,否则不稳定排序了?
}
print(data.sort(fun1)) // [1,3,4,89]
上面需要对整数Int
排序(正序),因此排序闭包函数类型需为(String,String) -> Bool
。
然而,这是一个相当冗长的方式,本质上只是写了一个单表达式函数 (a > b
)。其实,利用闭包表达式语法可以更好地构造一个内联排序闭包。
闭包表达式语法
闭包表达式的一般语法格式是
{ (parameters) -> returnType in
statements
}
闭包表达式语法可以使用常量、变量和inout
类型作为参数,不能提供默认值。也可以在参数列表的最后使用可变参数。元组也可以作为参数和返回值。
let data = [3,2,5,1,6,89,4]
print(data.sort({ (a: Int,b: Int) -> Bool in return a < b }))
可以看出,在内联闭包表达式中,函数和返回值类型都写在大括号内,而不是大括号外。闭包的函数体部分由关键字in
引入。该关键字表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。
let data = [3,4]
print(data.sort( { (a: Int,b: Int) -> Bool in return a < b } ))
一对圆括号仍然包裹住了方法的整个参数。然而,参数现在变成了内联闭包。
根据上下文推断类型
因为排序闭包函数是作为sort(_:)
方法的参数传入的,Swift可以推断其参数和返回值的类型,即因此其参数必须是(String,String) -> Bool
类型的函数。返回箭头(->
)和围绕在参数周围的括号也可以被省略。
实际上任何情况下,通过内联闭包表达式构造的闭包作为参数传递给函数或方法时,都可以推断出闭包的参数和返回值类型。 这意味着闭包作为函数或者方法的参数时,您几乎不需要利用完整格式构造内联闭包。
尽管如此,您仍然可以明确写出有着完整格式的闭包。如果完整格式的闭包能够提高代码的可读性,则可以采用完整格式的闭包。
let data = [3,4]
print(data.sort( { a,b in return a < b } ))
单表达式闭包隐式返回
单行表达式闭包可以通过省略return
关键字来隐式返回单行表达式的结果。下面的sort(_:)
函数明确了闭包必须返回Bool
类型值,可以省略return
。
let data = [3,b in a < b } ))
参数名称缩写
Swift自动为内联闭包提供了参数名称缩写功能,您可以直接通过$0
,$1
,$2
来顺序调用闭包的参数,以此类推。
如果您在闭包表达式中使用参数名称缩写,您可以在闭包参数列表中省略对其的定义,并且对应参数名称缩写的类型会通过函数类型进行推断。in
关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成。
let data = [3,4]
print(data.sort( { $0 < $1 } ))
运算符函数
实际上还有一种更简短的方式来撰写上面例子中的闭包表达式。Swift的Int
类型定义了关于大于号(>
)的实现,其作为一个函数接受两个Int
类型的参数并返回Bool类型的值。而这正好与sort(_:)
方法的第二个参数需要的函数类型相符合。因此,您可以简单地传递一个大于号,Swift 可以自动推断出您想使用大于号的字符串函数实现:
let data = [3,4]
print(data.sort(<))
尾随闭包
如果您需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。
如果函数只需要闭包表达式一个参数,当您使用尾随闭包时,您甚至可以把()省略掉。
let data = [3,4]
print(data.sort() { $0 < $1 })
print(data.sort { $0 < $1 })
当闭包非常长以至于不能在一行中进行书写时,尾随闭包变得非常有用。例如,Swift的Array
类型有一个map(_:)
方法,其获取一个闭包表达式作为其唯一参数。该闭包函数会为数组中的每一个元素调用一次,并返回该元素所映射的值。具体的映射方式和返回值类型由闭包来指定。当提供给数组的闭包应用于每个数组元素后,map(_:)
方法将返回一个新的数组,数组中包含了与原数组中的元素一一对应的映射后的值。
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 s = numbers.map {
(var number) -> String in // number声明为变量,因此可以在闭包函数体内对其进行修改,而不用再定义一个新的局部变量并将number的值赋值给它。
var r = ""
while number > 0 {
r = digitNames[number % 10]! + r // 先取最低位,注意是返回可选类型!
number /= 10
}
return r
}
print(digitNames[0]) // 注意是返回可选类型:Optional("Zero")
print(s) // ["OneSix","FiveEight","FiveOneZero"]
注意,字典下标返回可选类型!
值捕获
闭包可以在其被定义的上下文中捕获常量或变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。
Swift中,可以捕获值的闭包的最简单形式是嵌套函数,也就是定义在其他函数的函数体内的函数。嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。
func makeIncrementor(forIncrementor amount: Int) -> () -> Int {
var runningTotal = 0
func incrementor() -> Int {
runningTotal += amount
return runningTotal
}
return incrementor
}
let a = makeIncrementor(forIncrementor: 1)
print(a) // (Function)
print(a()) // 1
print(a()) // 2
print(a()) // 3
let b = makeIncrementor(forIncrementor: 1) // 再次创建一个incrementor,它会有属于它自己的一个全新、独立的runningTotal变量的引用,从头开始计算
print(b()) // 1
print(b()) // 2
print(b()) // 3
可以看出,incrementer()
函数并没有任何参数,但是在函数体内访问了runningTotal
和amount
变量。这是因为它从外围函数捕获
了runningTotal
和amount
变量的引用。捕获引用保证了runningTotal
和amount
变量在调用完makeIncrementer
后不会消失,并且保证了在下一次执行incrementer
函数时,runningTotal
依旧存在。
注意,为了优化,如果一个值是不可变的,Swift可能会改为捕获并保存一份对值的拷贝。Swift也会负责被捕获变量的所有内存管理工作,包括释放不再需要的变量。
在上面代码中,再次创建一个incrementor
,它会有属于它自己的一个全新、独立的runningTotal
变量的引用。
如果您将闭包赋值给一个类实例的属性,并且该闭包通过访问该实例或其成员而捕获了该实例,您将创建一个在闭包和该实例间的循环强引用。Swift使用捕获列表来打破这种循环强引用。
闭包是引用类型
上面的代码中,a
和b
都是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量的值。这是因为函数和闭包都是引用类型。
无论您将函数或闭包赋值给一个常量还是变量,您实际上都是将常量或变量的值设置为对应函数或闭包的引用。上面的例子中,指向闭包的引用incrementByTen
是一个常量,而并非闭包内容本身。
这也意味着如果您将闭包赋值给了两个不同的常量或变量,两个值都会指向同一个闭包。
func makeIncrementor(forIncrementor amount: Int) -> () -> Int {
var runningTotal = 0
func incrementor() -> Int {
runningTotal += amount
return runningTotal
}
return incrementor
}
let a = makeIncrementor(forIncrementor: 1)
print(a) // (Function)
print(a()) // 1
print(a()) // 2
print(a()) // 3
let b = a // 注意:a和b指向同一个闭包!!!
print(b()) // 4
非逃逸闭包
当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。当你定义接受闭包作为参数的函数时,你可以在参数名之前标注@noescape
,用来指明这个闭包是不允许“逃逸
”出这个函数的。将闭包标注@noescape
能使编译器知道这个闭包的生命周期(闭包只能在函数体中被执行,不能脱离函数体执行,所以编译器明确知道运行时的上下文),从而可以进行一些比较激进的优化。
func someFunctionWithNoescapeClosure(@noescape closure: () -> Void) { closure() }
sort(_:)
方法接受一个用来进行元素比较的闭包作为参数。这个参数被标注了@noescape
,因为它确保自己在排序结束之后就没用了。
一种能使闭包“逃逸”出函数的方法是,将这个闭包保存在一个函数外部定义的变量中。举个例子,很多启动异步操作的函数接受一个闭包参数作为completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。在这种情况下,闭包需要“逃逸”出函数,因为闭包需要在函数返回之后被调用。
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: () -> Void) { completionHandlers.append(completionHandler) }
someFunctionWithEscapingClosure(_:)
函数接受一个闭包作为参数,该闭包被添加到一个函数外定义的数组中。如果你试图将这个参数标注为@noescape
,你将会获得一个编译错误。
将闭包标注为@noescape
使你能在闭包中隐式地引用self
。
func someFunctionWithNoescapeClosure(@noescape closure: () -> Void) { closure() } var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: () -> Void) { completionHandlers.append(completionHandler) } class SomeClass { var x = 10 func doSomething() { someFunctionWithEscapingClosure { self.x = 100 } // 不加self报错:error: reference to property 'x' in closure requires explicit 'self.' to make capture semantics explicit someFunctionWithNoescapeClosure { x = 200 } } } let instance = SomeClass() instance.doSomething() print(instance.x) // 200 completionHandlers.first?() print(instance.x) // 100
自动闭包
自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够用一个普通的表达式来代替显式的闭包,从而省略闭包的花括号。
我们经常会调用一个接受闭包作为参数的函数,但是很少实现那样的函数。举个例子来说,assert(condition:message:file:line:)
函数接受闭包作为它的condition
参数和message
参数;它的condition
参数仅会在debug模式下被求值,它的message
参数仅当condition
参数为false
时被计算求值。
自动闭包让你能够延迟求值,因为代码段不会被执行直到你调用这个闭包。延迟求值对于那些有副作用(Side Effect)和代价昂贵的代码来说是很有益处的,因为你能控制代码什么时候执行。
var a = [1,3,4,5]
print(a.count) // 5
let b = { a.removeAtIndex(0) } // b是一个闭包表达式:() -> Int
print(b()) // 1
print(a.count) // 4
在闭包被调用之前,这个元素是不会被移除的。如果这个闭包永远不被调用,那么在闭包里面的表达式将永远不会执行,那意味着列表中的元素永远不会被移除。注意,b的类型为() -> Int
的函数。
将闭包作为参数传递给函数时,你能获得同样的延时求值行为。
var a = [1,5]
print(a.count) // 5
func b(c: () -> Int) { print(c()) } } b( { a.removeAtIndex(0) } ) // 1 print(a.count) // 4
上面的b
函数接受一个显式地闭包。
var a = [1,5]
print(a.count) // 5
func b(@autoclosure c: () -> Int) { print(c()) } b(a.removeAtIndex(0)) // 1 print(a.count) // 4
上面的b
函数并没有接受一个显式的闭包,而是通过将参数标记为@autoclosure
来接收一个自动闭包
。现在可以将该函数当做接受Int
类型参数的函数来调用。c
参数将自动转化为一个闭包,因为该参数被标记了@autoclosure
特性。
过度使用autoclosures
会让你的代码变得难以理解。上下文和函数名应该能够清晰地表明求值是被延迟执行的。
@autoclosure
特性暗含了@noescape
特性。如果你想让这个闭包可以“逃逸”,则应该使用@autoclosure(escaping)
特性。
var a = [1,5]
print(a.count) // 5
var arr: [() -> Int] = []
func b(@autoclosure(escaping) c: () -> Int) { arr.append(c) } b(a.removeAtIndex(0)) b(a.removeAtIndex(0)) print(a.count) // 5,没有执行删除,仅仅是把函数追加进数组 print(arr.count) // 2 for i in arr { print(i,terminator: "-") // (Function)-(Function)- } print("") arr[0]() // 此时,数组内的闭包在函数返回之后被调用! print(a.count) // 4 arr[1]() print(a.count) // 3
函数并没有调用传入的c
闭包,而是将闭包追加到了arr
数组中。这个数组定义在函数作用域范围外,这意味着数组内的闭包将会在函数返回之后被调用。因此,c
参数必须允许“逃逸”出函数作用域。