作者刘文涛
转载请注明出处
闭包(Closures)
闭包是自包含的函数代码块,可以在代码中被传递和使用。Swift 中的闭包与 C 和 Objective-C 中的代码块(block)以及其他一些编程语言中的匿名函数比较相似。
闭包可以捕获和存储其所在上下文中任意常量和变量的引用。被称为包裹常量和变量。Swift会为你管理在捕获过程中涉及到的所有内存操作。
在函数章节中介绍的全局和嵌套函数实际上也是特殊的闭包,闭包采取如下三种形式之一:
• 全局函数是一个有名字但不会捕获任何值的闭包
• 嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包
• 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包
Swift 的闭包表达式拥有简洁的风格,并鼓励在常见场景中进行语法优化,主要优化如下:
• 利用上下文推断参数和返回值类型
• 隐式返回单表达式闭包,即单表达式闭包可以忽略 return 关键字
• 参数名称缩写
• 尾随闭包语法
闭包表达式
闭包表达式是一种利用简洁语法构建内联闭包的方式。闭包表达式提供了一些语法优化,使得撰写闭包变得简单明了。下面闭包表达式的例子通过使用几次迭代展示了 sorted(by:)
方法定义和语法优化的方式。每一次迭代都用更简洁的方式描述了相同的功能。
sorted 方法
Swift 标准库提供了名为 sorted(by:)
的方法,它会根据你所提供的用于排序的闭包函数将已知类型数组中的值进行排序。一旦排序完成, sorted(by:)
方法会返回一个与原数组大小相同,包含同类型元素且元素已正确排序的新数组。原数组不会被 sorted(by:)
方法修改。
|
|
然而,以这种方式来编写一个实际上很简单的表达式 (a>b),确实太过繁琐了。对于这个例子来说,利用闭包表达式语法可以更好的构造一个内联排序闭包。
闭包表达式语法
闭包表达式语法有如下的一般形式:
|
|
闭包表达式参数 可以是 in-out 参数,但不能设定默认值。也可以使用具名的可变参数。元组也可以作为参数和返回值。
下面的例子展示了之前 backward(_:_:)
函数对应的闭包表达式版本的代码:
|
|
需要注意的是内联闭包参数和返回值类型声明与 backward(_:_:)
函数类型声明相同。在这两种方式中,都写成了 (s1: Int, s2: Int) -> Bool
。然而在内联闭包表达式中,函数和返回值类型都写在大括号内,而不是大括号外。
闭包的函数体部分由关键字 in
引入。该关键字表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。
由于这个闭包的函数体部分如此短,以至于可以将其改写成一行代码:
|
|
该例中 sorted(by:)
方法的整体调用保持不变,一对圆括号仍然包裹住了方法的整个参数。然而,参数现在变成了内联闭包。
根据上下文推断类型
因为排序闭包函数是作为 sorted(by:)
方法的参数传入的,Swift可推断其参数和返回值类型。 sorted(by:)
方法被一个Int数组调用,因此其参数必须是 ( Int, Int) -> Bool
类型的函数。这意味着 ( Int, Int)
和 Bool 类型并不需要作为闭包表达式定义的一部分,因为所有的类型都可以被正确推断,返回箭头 和围绕在参数周围的括号也可以被省略:
|
|
实际上,通过内联闭包表达式构造的闭包作为参数传递给函数或方法时,总是能够推断出闭包的参数和返回值类型。这意味着闭包作为函数或者方法的参数时,你几乎不需要利用完整格式构造内联闭包。
尽管如此,你仍然可以明确写出有着完整格式的闭包。如果完整格式的闭包能够提高代码的可读性,则我们更鼓励采用完整格式的闭包。而在 sorted(by:)
方法这个例子里,显然闭包的目的就是排序。由于这个闭包是为了处理字符串数组的排序,因此读者能够推测出这个闭包是用于字符串处理的。
单表达式闭包隐式返回
单行表达式闭包可以通过省略 return
关键字来隐式返回单行表达式的结果,如上版本的例子可以改写为:
|
|
在这个例子中, sorted(by:)
方法的参数类型明确了闭包必须返回一个 Bool
类型值。因为闭包函数体只包含了一个单一表达式 (s1 > s2),该表达式返回 Bool 类型值,因此这里没有歧义,return 关键字可以省略。
参数名称缩写
Swift 自动为内联闭包提供了参数名称缩写功能,你可以直接通过 $0
, $1
, $2
来顺序调用闭包的参数,以此类推。
如果你在闭包表达式中使用参数名称缩写,你可以在闭包定义中省略参数列表,并且对应参数名称缩写的类型会通过函数类型进行推断。 in
关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:
|
|
在这个例子中,$0
, $1
表示闭包中第一个和第二个 Int 类型的参数。
运算符方法
实际上还有一种更简短的方式来编写上面例子中的闭包表达式。你可以简单地传递一个大于号,Swift 可以自动推断出 你想使用大于号的Int函数实现:
|
|
值捕获
闭包可以在其被定义的上下文中捕获常量或变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。
Swift中,可以捕获值的闭包的最简单形式是嵌套函数,也就是定义在其他函数的函数体内的函数。嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。
|
|
incrementer()
函数并没有任何参数,但是在函数体内访问了 runningTotal
和 amount
变量。这是因为它从外围函数捕获了 runningTotal
和 amount
变量的引用。捕获引用保证了 runningTotal
和 amount
变量在调用完makeIncrementer
后不会消失,并且保证了在下一次执行 incrementer
函数时,runningTotal
依旧存在。
注意:
为了优化,如果一个值不会被闭包改变,或者在闭包创建后不会改变,Swift可能会改为捕获并保存一份对值的拷贝。Swift也会负责被捕获变量的所有内存管理工作,包括释放不在需要的变量。
下面是一个使用 makeIncrementor 的例子:
该例子定义了一个叫做 incrementByTen
的常量,该常量指向一个每次调用会将其 runningTotal
变量增加 10 的 incrementor
函数。调用这个函数多次可以得到以下结果:
|
|
如果你创建了另一个 incrementor
,它会有属于自己的引用,指向一个全新、独立的 runningTotal
变量:
|
|
再次调用原来的 incrementByTen
会继续增加它自己的runningTotal
变量,该变量和 incrementBySeven
中捕获的变量没有任何联系:
|
|
注意:
如果你将闭包赋值给一个类实例的属性,并且该闭包通过访问该实例或其成员而捕获了该实例,你将在闭包和该实例间创建一个循环强引用。Swift使用捕获列表来打破这种循环强引用。
闭包是引用类型
上面的例子中, incrementByTen
和 incrementBySeven
都是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量的值。这是因为函数和闭包都是引用类型。
无论你将函数或闭包赋值给一个常量还是变量,你实际上都是将常量或变量的值设置为对应函数或闭包的引用。上面的例子中,指向闭包的引用 incrementByTen
是一个常量,而并非闭包内容本身。
这也意味着如果你将闭包赋值给了两个不同的常量或变量,两个值都会指向同一个闭包:
|
|
自动闭包
自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。
我们经常会调用采用自动闭包的函数,但是很少去实现这样的函数。
自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。延迟求值对于那些有副作用和高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。下面的代码展示了闭包如何延时求值。
|
|
尽管在闭包的代码中,customersInLine
的第一个元素被移除了,不过在闭包被调用之前,这个元素是不会被移除的。如果这个闭包永远不被调用,那么在闭包里面的表达式将永远不会执行,那意味着列表中的元素永远不会被移除。请注意 customerProvider
的类型不是 String
,而是 () -> String
,一个没有参数且返回值为 String
的函数。