闭包是自包含的函数代码块,Swift 中的闭包与 C 和 Objective-C 中的代码块(b
locks)以及其他一些编程语言中的匿名函数比较相似。Swift 会为你管理在捕获过程中涉及到的所有内存操作。
闭包表达式的语法:
1 | { (parameters) -> returnType in |
闭包表达式参数 可以是 in-out 参数,但不能设定默认值。也可以使用具名的可变参数。闭包的函数体部分由关键字 in
引入,表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。
举个例子,sorted(by:)
方法接收一个闭包,接收两个参数,返回一个 Bool 值:
1 | let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"] |
上面的闭包函数体也可以改写成一行代码,一对圆括号仍然包裹住了方法的整个参数。然而,参数现在变成了内联闭包
。:
1 | reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } ) |
由于 Swift 可以推断其参数和返回值类型,所以其参数(String, String) -> Bool
类型的函数并不需要作为闭包表达式定义的一部分。因为所有类型都可以被推断,所以->
和围绕在参数周围的括号也可以被省略:
1 | reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } ) |
单表达式闭包
(这里闭包函数体只包含了一个单一表达式( s1 > s2 ))可以通过省略return
关键字来隐式返回单行表达式的结果:
1 | reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } ) |
Swift 为内联闭包
提供参数名称缩写功能
,你可以通过$0
,$1
,$2
来顺序调用闭包的参数,以此类推。如果闭包表达式中使用参数名称缩写,也可以在闭包定义中省略参数列表,并且对应参数名称缩写的类型会通过函数类型进行推断。in
关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成($0
、$1
分别表示闭包的第一个和第二个参数)。
1 | reversedNames = names.sorted(by: { $0 > $1 } ) |
运算符方法
:String
类型定义了关于>
的字符串实现,其作为一个函数接受两个String
类型的参数并返回Bool
类型。这恰好与sorted(by:)
方法的参数需要函数类型相符合。所以可以自动推断出>
的字符串函数实现。
1 | reversedNames = names.sorted(by: >) |
尾随闭包
如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。尾随闭包是一个书写在函数括号之后的闭包表达式函数支持将其作为最后一个参数调用
。在使用尾随闭包时,你不用写出它的参数标签:
1 | func someFunctionThatTakesAClosure(closure: () -> Void) { |
上面的例子可改写为:
1 | reversedNames = names.sorted() { $0 > $1 } |
如果闭包表达式是函数或方法的唯一参数,可以省略()
:
1 | reversedNames = names.sorted { $0 > $1 } |
值捕获
闭包可以在其被定义的上下文中捕获常量或变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。
1 | func makeIncrementer(forIncrement amount: Int) -> () -> Int { |
闭包捕获了amount
、runningTotal
两个变量的引用,捕获引用保证了 runningTotal
和 amount
变量在调用完 makeIncrementer
后不会消失,并且保证了在下一次执行 incrementer
函数时,runningTotal
依旧存在。Swift 负责被捕获变量的所有内存管理工作,包括释放不再需要的变量。
1 | incrementByTen() // 10 |
注意:
如果你将闭包赋值给一个类实例的属性,并且该闭包通过访问该实例或其成员而捕获了该实例,你将在闭包和该 实例间创建一个循环强引用
逃逸闭包
逃逸闭包
:当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。可以在参数名之前标注@escaping
,来指明这个闭包时允许“逃逸”出这个函数的。
一种能使闭包“逃逸”出函数的方法是,将这个闭包保存在一个函数外部定义的变量中
。比如常用的回调函数,这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。在这种情况下,闭包需要“逃逸”出函数,因为闭包需要在函数返回之后被调用。例如:
1 | // someFunctionWithEscapingClosure(_:)中的闭包是一个逃逸闭包,该闭包被添加到一个函数外定义的数组中。如果不显示将参数标记为 @escaping ,会得到编译错误 |
注意
:标记为@escaping
的闭包必须在闭包中显式的引用self
,而非逃逸闭包,它可以隐式的引用self
。
1 | func someFunctionWithNonescapingClosure(closure: () -> Void) { |
自动闭包
自动闭包
是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。
自动闭包能够延延迟求职,因为知道你调用这个闭包,代码段才会被执行。延迟求值对于高计算成本的代码来说很有用。举个例子表示如何延时求值:
1 | var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"] |
将闭包作为参数传递给函数时,同样能获取延时求值行为。
1 | var customersInLine = ["Alex", "Ewa", "Barry", "Daniella"] |
也可以将参数标记为 @autoclosure
来接收一个自动闭包。现在可以将该函数当做接受String
类型参数(而非闭包)的函数来调用。customerProvider
参数将自动转化为一个闭包,因为该参数被标记了@autoclosure
特性。
1 | var customersInLine = ["Alex", "Ewa", "Barry", "Daniella"] |
如果想让一个闭包自动“逃逸”,则应该同时使用@autoclosure
和@escaping
。
1 | var customersInLine = ["Alex", "Ewa", "Barry", "Daniella"] |