闭包是引用类型
上面的例子中,incrementBySeven
和 incrementByTen
都是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量的值。这是因为函数和闭包都是 引用类型 。
无论你将函数或闭包赋值给一个常量还是变量,你实际上都是将常量或变量的值设置为对应函数或闭包的 引用 。上面的例子中,指向闭包的引用 incrementByTen
是一个常量,而并非闭包内容本身。
这也意味着如果你将闭包赋值给了两个不同的常量或变量,两个值都会指向同一个闭包:
let alsoIncrementByTen = incrementByTen alsoIncrementByTen() // 返回的值为50
逃逸闭包
当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中 逃逸 。当你定义接受闭包作为参数的函数时,你可以在参数名之前标注 @escaping
,用来指明这个闭包是允许“逃逸”出这个函数的。
一种能使闭包“逃逸”出函数的方法是,将这个闭包保存在一个函数外部定义的变量中。举个例子,很多启动异步操作的函数接受一个闭包参数作为 completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。在这种情况下,闭包需要“逃逸”出函数,因为闭包需要在函数返回之后被调用。例如:
var completionHandlers: [() ->Void] = [] funcsomeFunctionWithEscapingClosure(completionHandler: @escaping () ->Void) { completionHandlers.append(completionHandler) }
someFunctionWithEscapingClosure(_:)
函数接受一个闭包作为参数,该闭包被添加到一个函数外定义的数组中。如果你不将这个参数标记为 @escaping
,就会得到一个编译错误。
将一个闭包标记为 @escaping
意味着你必须在闭包中显式地引用 self
。比如说,在下面的代码中,传递到 someFunctionWithEscapingClosure(_:)
中的闭包是一个逃逸闭包,这意味着它需要显式地引用 self
。相对的,传递到 someFunctionWithNonescapingClosure(_:)
中的闭包是一个非逃逸闭包,这意味着它可以隐式引用 self
。
funcsomeFunctionWithNonescapingClosure(closure: () ->Void) { closure() } classSomeClass { var x =10 funcdoSomething() { 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())!") // 打印出“Now serving Chris!” print(customersInLine.count) // 打印出“4”
尽管在闭包的代码中,customersInLine
的第一个元素被移除了,不过在闭包被调用之前,这个元素是不会被移除的。如果这个闭包永远不被调用,那么在闭包里面的表达式将永远不会执行,那意味着列表中的元素永远不会被移除。请注意,customerProvider
的类型不是 String
,而是 () -> String
,一个没有参数且返回值为 String
的函数。
将闭包作为参数传递给函数时,你能获得同样的延时求值行为。
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"] funcserve(customercustomerProvider: () ->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"] funcserve(customercustomerProvider: @autoclosure () ->String) { print("Now serving \(customerProvider())!") } serve(customer: customersInLine.remove(at:0)) // 打印“Now serving Ewa!”
注意
过度使用
autoclosures
会让你的代码变得难以理解。上下文和函数名应该能够清晰地表明求值是被延迟执行的。
如果你想让一个自动闭包可以“逃逸”,则应该同时使用 @autoclosure
和 @escaping
属性。@escaping
属性的讲解见上面的 逃逸闭包。
// customersInLine i= ["Barry", "Daniella"] var customerProviders: [() ->String] = [] funccollectCustomerProviders(_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!”
在上面的代码中,collectCustomerProviders(_:)
函数并没有调用传入的 customerProvider
闭包,而是将闭包追加到了 customerProviders
数组中。这个数组定义在函数作用域范围外,这意味着数组内的闭包能够在函数返回之后被调用。因此,customerProvider
参数必须允许“逃逸”出函数作用域。