在OC中存储一段代码块可以使用Block,而对于Swift中也有相应的对照用于存储代码块这个就是今天所说的闭包,在其它语言中也叫匿名函数。
一、闭包
闭包是自包含的函数代码块,可以在代码中被传递和使用。Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的匿名函数比较相似。
闭包可以捕获和存储其所在上下文中任意常量和变量的引用。被称为包裹常量和变量。 Swift 会为你管理在捕获过程中涉及到的所有内存操作。
二、闭包表达式
嵌套函数是一个在较复杂函数中方便进行命名和定义自包含代码模块的方式。当然,有时候撰写小巧的没有完整定义和命名的类函数结构也是很有用处的,尤其是在处理一些函数并需要将另外一些函数作为该函数的参数时。
闭包表达式是一种利用简洁语法构建内联闭包的方式。闭包表达式提供了一些语法优化,使得撰写闭包变得简单明了。下面闭包表达式的例子通过使用几次迭代展示了sort(_:)
方法定义和语法优化的方式。每一次迭代都用更简洁的方式描述了相同的功能。
三、sort 方法
Swift 标准库提供了名为sort
的方法,会根据提供的用于排序的闭包函数将已知类型数组中的值进行排序。一旦排序完成,sort(_:)
方法会返回一个与原数组大小相同,包含同类型元素且元素已正确排序的新数组。原数组不会被sort(_:)
方法修改。
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"]
四、闭包表达式语法
闭包表达式语法有如下一般形式:
{ (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 } )
该例中sort(_:)
方法的整体调用保持不变,一对圆括号仍然包裹住了方法的整个参数。然而,参数现在变成了内联闭包。
五、值捕获
闭包可以在其被定义的上下文中捕获常量或变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。
Swift 中,可以捕获值的闭包的最简单形式是嵌套函数,也就是定义在其他函数的函数体内的函数。嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。
举个例子,这有一个叫做 makeIncrementer
的函数,其包含了一个叫做incrementer
的嵌套函数。嵌套函数 incrementer()
从上下文中捕获了两个值,runningTotal
和 amount
。捕获这些值之后,makeIncrementer
将 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(forIncrement:) 函数定义了一个初始值为 0 的整型变量 runningTotal,用来存储当前总计数值。该值为 incrementer 的返回值。
makeIncrementer(forIncrement:)
有一个 Int
类型的参数,其外部参数名为 forIncrement
,内部参数名为 amount
,该参数表示每次 incrementer
被调用时 runningTotal
将要增加的量。makeIncrementer
函数还定义了一个嵌套函数 incrementer
,用来执行实际的增加操作。该函数简单地使 runningTotal
增加 amount
,并将其返回。
如果我们单独考虑嵌套函数incrementer()
,会发现它有些不同寻常
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
incrementer()
函数并没有任何参数,但是在函数体内访问了 runningTotal
和 amount
变量。这是因为它从外围函数捕获了 runningTotal
和 amount
变量的引用。捕获引用保证了 runningTotal
和 amount
变量在调用完 makeIncrementer
后不会消失,并且保证了在下一次执行incrementer
函数时,runningTotal
依旧存在。
注意 为了优化,如果一个值不会被闭包改变,或者在闭包创建后不会改变,Swift 可能会改为捕获并保存一份对值的拷贝。 Swift 也会负责被捕获变量的所有内存管理工作,包括释放不再需要的变量
下面是一个使用makeIncrementer
的例子:
let incrementByTen = makeIncrementer(forIncrement: 10)
该例子定义了一个叫做incrementByTen
的常量,该常量指向一个每次调用会将其runningTotal
变量增加 10 的 incrementer
函数。调用这个函数多次可以得到以下结果:
incrementByTen()
// 返回的值为10
incrementByTen()
// 返回的值为20
incrementByTen()
// 返回的值为30
如果你创建了另一个 incrementer
,它会有属于自己的引用,指向一个全新、独立的 runningTotal
变量:
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// 返回的值为7
再次调用原来的incrementByTen
会继续增加它自己的 runningTotal
变量,该变量和 incrementBySeven
中捕获的变量没有任何联系:
incrementByTen()
// 返回的值为40
注意:
如果你将闭包赋值给一个类实例的属性,并且该闭包通过访问该实例或其成员而捕获了该实例,你将在闭包和该实例间创建一个循环强引用。Swift 使用捕获列表来打破这种循环强引用
六、闭包是引用类型
上面的例子中,incrementBySeven
和 incrementByTen
都是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量的值。这是因为函数和闭包都是引用类型。
无论你将函数或闭包赋值给一个常量还是变量,你实际上都是将常量或变量的值设置为对应函数或闭包的引用。上面的例子中,指向闭包的引用 incrementByTen
是一个常量,而并非闭包内容本身。
这也意味着如果你将闭包赋值给了两个不同的常量或变量,两个值都会指向同一个闭包:
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 返回的值为50
七、逃逸闭包
当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。当你定义接受闭包作为参数的函数时,你可以在参数名之前标注 @escaping
,用来指明这个闭包是允许“逃逸”出这个函数的。
一种能使闭包“逃逸”出函数的方法是,将这个闭包保存在一个函数外部定义的变量中。举个例子,很多启动异步操作的函数接受一个闭包参数作为 completion handler
。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。在这种情况下,闭包需要“逃逸”出函数,因为闭包需要在函数返回之后被调用。例如:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
someFunctionWithEscapingClosure(_:)
函数接受一个闭包作为参数,该闭包被添加到一个函数外定义的数组中。如果你不将这个参数标记为 @escaping
,就会得到一个编译错误。
将一个闭包标记为 @escaping
意味着你必须在闭包中显式地引用 self
。比如说,在下面的代码中,传递到 someFunctionWithEscapingClosure(_:)
中的闭包是一个逃逸闭包,这意味着它需要显式地引用self
。相对的,传递到 someFunctionWithNonescapingClosure(_:)
中的闭包是一个非逃逸闭包,这意味着它可以隐式引用 self
。
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)
// 打印出 "200"
completionHandlers.first?()
print(instance.x)
// 打印出 "100"
八、自动闭包
自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。
我们经常会调用采用自动闭包的函数,但是很少去实现这样的函数。举个例子来说,assert(condition:message:file:line:)
函数接受自动闭包作为它的 condition
参数和 message
参数;它的 condition
参数仅会在 debug
模式下被求值,它的 message
参数仅当 condition
参数为 false
时被计算求值。
自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。延迟求值对于那些有副作用(Side Effect)和高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。下面的代码展示了闭包如何延时求值。
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// 打印出 "5"
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// 打印出 "5"
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// 打印出 "4"
尽管在闭包的代码中,customersInLine
的第一个元素被移除了,不过在闭包被调用之前,这个元素是不会被移除的。如果这个闭包永远不被调用,那么在闭包里面的表达式将永远不会执行,那意味着列表中的元素永远不会被移除。请注意,customerProvider
的类型不是 String
,而是 () -> String
,一个没有参数且返回值为 String
的函数。
将闭包作为参数传递给函数时,你能获得同样的延时求值行为。
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// 打印出 "Now serving Alex!"
上面的 serve(customer:)
函数接受一个返回顾客名字的显式的闭包。下面这个版本的 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))
// 打印 "Now serving Ewa!"
注意 过度使用 autoclosures 会让你的代码变得难以理解。上下文和函数名应该能够清晰地表明求值是被延迟执行的。
如果你想让一个自动闭包可以“逃逸”,则应该同时使用 @autoclosure
和 @escaping
属性
// customersInLine i= ["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.")
// 打印 "Collected 2 closures."
for customerProvider in customerProviders {
print("Now serving \(customerProvider())!")
}
// 打印 "Now serving Barry!"
// 打印 "Now serving Daniella!"