闭包是自包含的函数代码块,可以在代码中被传递和使用。Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的匿名函数(Lambdas)比较相似。
闭包可以捕获和存储其所在上下文中任意常量和变量的引用。被称为包裹常量和变量。 Swift 会为你管理在捕获过程中涉及到的所有内存操作。
注意
如果你不熟悉捕获(capturing)这个概念也不用担心,在 值捕获 章节有它更详细的介绍。
在 函数 章节中介绍的全局和嵌套函数实际上也是特殊的闭包,闭包采用如下三种形式之一:
- 全局函数是一个有名字但不会捕获任何值的闭包
- 嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包
- 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包
Swift 的闭包表达式拥有简洁的风格,并鼓励在常见场景中进行语法优化,主要优化如下:
- 利用上下文推断参数和返回值类型
- 隐式返回单表达式闭包,即单表达式闭包可以省略
return
关键字 - 参数名称缩写
- 尾随闭包语法
闭包表达式
嵌套函数 作为复杂函数的一部分时,它自包含代码块式的定义和命名形式在使用上带来了方便。当然,编写未完整声明和没有函数名的类函数结构代码是很有用的,尤其是在编码中涉及到函数作为参数的那些方法时。
闭包表达式是一种构建内联闭包的方式,它的语法简洁。在保证不丢失它语法清晰明了的同时,闭包表达式提供了几种优化的语法简写形式。下面通过对 sorted(by:)
这一个案例的多次迭代改进来展示这个过程,每次迭代都使用了更加简明的方式描述了相同功能。
排序方法
Swift 标准库提供了名为 sorted(by:)
的方法,它会基于你提供的排序闭包表达式的判断结果对数组中的值(类型确定)进行排序。一旦它完成排序过程,sorted(by:)
方法会返回一个与旧数组类型大小相同类型的新数组,该数组的元素有着正确的排序顺序。原数组不会被 sorted(by:)
方法修改。
下面的闭包表达式示例使用 sorted(by:)
方法对一个 String
类型的数组进行字母逆序排序。以下是初始数组:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
sorted(by:)
方法接受一个闭包,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来表明当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值前面,排序闭包函数需要返回 true
,反之返回 false
。
该例子对一个 String
类型的数组进行排序,因此排序闭包函数类型需为 (String, String) -> Bool
。
提供排序闭包函数的一种方式是撰写一个符合其类型要求的普通函数,并将其作为 sorted(by:)
方法的参数传入:
func backward(_ s1: String, _ s2: String) -> Bool { return s1 > s2 } var reversedNames = names.sorted(by: backward) // reversedNames 为 ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
如果第一个字符串(s1
)大于第二个字符串(s2
),backward(_:_:)
函数会返回 true
,表示在新的数组中 s1
应该出现在 s2
前。对于字符串中的字符来说,“大于”表示“按照字母顺序较晚出现”。这意味着字母 "B"
大于字母 "A"
,字符串 "Tom"
大于字符串 "Tim"
。该闭包将进行字母逆序排序,"Barry"
将会排在 "Alex"
之前。
然而,以这种方式来编写一个实际上很简单的表达式(a > b
),确实太过繁琐了。对于这个例子来说,利用闭包表达式语法可以更好地构造一个内联排序闭包。
闭包表达式语法
闭包表达式语法有如下的一般形式:
{ (parameters) -> return type in statements }
闭包表达式参数 可以是 in-out 参数,但不能设定默认值。如果你命名了可变参数,也可以使用此可变参数。元组也可以作为参数和返回值。
下面的例子展示了之前 backward(_:_:)
函数对应的闭包表达式版本的代码:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 })
需要注意的是内联闭包参数和返回值类型声明与 backward(_:_:)
函数类型声明相同。在这两种方式中,都写成了 (s1: String, s2: String) -> Bool
。然而在内联闭包表达式中,函数和返回值类型都写在大括号内,而不是大括号外。
闭包的函数体部分由关键字 in
引入。该关键字表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。
由于这个闭包的函数体部分如此短,以至于可以将其改写成一行代码:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )
该例中 sorted(by:)
方法的整体调用保持不变,一对圆括号仍然包裹住了方法的整个参数。然而,参数现在变成了内联闭包。
根据上下文推断类型
因为排序闭包函数是作为sorted(by:)
方法的参数传入的,Swift 可以推断其参数和返回值的类型。sorted(by:)
方法被一个字符串数组调用,因此其参数必须是(String, String) ->Bool
类型的函数。这意味着(String, String)
和Bool
类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断,返回箭头(->)
和围绕在参数周围的括号也可以被省略:
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
实际上,通过内联闭包表达式构造的闭包作为参数传递给函数或方法时,总是能够推断出闭包的参数和返回值类型。这意味着闭包作为函数或者方法的参数时,你几乎不需要利用完整格式构造内联闭包。
尽管如此,你仍然可以明确写出有着完整格式的闭包。如果完整格式的闭包能够提高代码的可读性,则我们更鼓励采用完整格式的闭包。而在sorted(by:)
方法这个例子里,显然闭包的目的就是排序。由于这个闭包是为了处理字符串数组的排序,因此读者能够推测出这个闭包是用于字符串处理的。
单表达式闭包的隐式返回
单行表达式闭包可以通过省略return
关键字来隐式返回单行表达式的结果,如上版本的例子可以改写为:
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
在这个例子中,sorted(by:)
方法的参数类型明确了闭包必须返回一个 Bool
类型值。因为闭包函数体只包含了一个单一表达式(s1 > s2)
,该表达式返回Bool
类型值,因此这里没有歧义,return
关键字可以省略。
参数名称缩写
Swift自动为内联闭包提供了参数名称缩写功能,你可以直接通过 $0
,$1
,$2
来顺序调用闭包的参数,以此类推。
如果你在闭包表达式中使用参数名称缩写,你可以在闭包定义中省略参数列表,并且对应参数名称缩写的类型会通过函数类型进行推断。in 关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:
reversedNames = names.sorted(by: { $0 > $1 } )
在这个例子中,$0
和$1
表示闭包中第一个和第二个String
类型的参数。
运算符方法
实际上还有一种更简短的方式来编写上面例子中的闭包表达式。Swift 的 String
类型定义了关于大于号(>)
的字符串实现,其作为一个函数接受两个 String
类型的参数并返回 Bool
类型的值。而这正好与 sorted(by:)
方法的参数需要的函数类型相符合。因此,你可以简单地传递一个大于号,Swift 可以自动推断找到系统自带的那个字符串函数的实现:
reversedNames = names.sorted(by: >)
更多关于运算符方法的内容请查看 运算符方法。
注:
闭包可以理解为一个匿名函数,所以
1.不需要函数名
2.不需要有外部参数名
3.闭包的声明最后要加in关键字
4.闭包使用{}包裹