欢迎关注微信公众号「Swift 花园」
创建基本的闭包
Swift 允许我们像字符串和整数一样使用函数。具体来说,你可以创建一个函数然后把它赋给一个变量,利用那个变量来调用函数。你甚至可以把函数作为参数传给另一个函数。
函数的这种用法被称为 闭包 。虽然工作机制差不多,写法上是有一些小差异的。
还是以打印信息为例:
1 | let driving = { |
上面的代码实际上创建了一个匿名的函数,并将这个函数赋给了 driving
。之后你就可以把 driving ()
当作一个常规的函数来用,就像这样:
1 | driving () |
在闭包中接收参数
当你创建闭包的时候,它们并没有名字,也没有提供书写参数的地方。但这并不意味着它们不能接收参数,只不过它们接收参数的方式稍有不同:这些参数是被写在花括号里面的。
为了让一个闭包接收参数,你需要在花括号之后把这些参数列出来,然后跟上一个 in
关键字。这样就告诉 Swift ,闭包的主体是从哪里开始的。
举个例子,我们来创建一个闭包,接收一个叫 place
的字符串作为唯一的参数,就像这样:
1 | let driving = { (place: String) in |
函数和闭包的一个区别是运行闭包的时候你不会用到参数标签。因此,调用 driving ()
的时候,我们是这样写的:
1 | driving (" 北京 & quot;) |
从闭包中返回值
闭包也能返回值,写法和闭包的参数类似:写在闭包内部, in
关键字前面。
还是以 driving ()
闭包为例, 让它返回一个字符串。原来的函数是这样的:
1 | let driving = { (place: String) in |
改成返回字符串而不是直接打印那个字符串,需要 in
之前添加 -> String
,然后像常规函数那样用到 return
关键字:
1 | let drivingWithReturn = { (place: String) -> String in |
现在我们运行这个闭包并且打印出它的返回值:
1 | let message = drivingWithReturn (" 北京 & quot;) |
闭包作为参数
既然闭包可以像字符串和整数一样使用,你就可以将它们传入函数。闭包作为参数的语法乍一看一看挺伤脑筋的,让我们慢慢来。
首先,还是基本的 driving ()
闭包。
1 | let driving = { |
如果我们打算把这个闭包传入一个函数,以便函数内部可以运行这个闭包。我们需要把函数的参数类型指定为 () -> Void
。它的意思是 “不接收参数,并且返回 Void
”。在 Swift 中, Void
是什么也没有的意思。
好了,让我们来写一个 travel ()
函数,接收不同类型的 traveling 动作, 并且在动作前后分别打印信息:
1 | func travel(action: () -> Void) { |
现在可以用上 driving
闭包了,就像这样:
1 | travel (action: driving) |
拖尾闭包语法
如果一个函数的最后一个参数是闭包, Swift 允许你采用一种被称为 “拖尾闭包语法” 的方式来调用这个闭包。你可以把闭包传入函数之后的花括号里,而不必像传入参数那样。
又用到我们的 travel ()
函数了。它接收一个 action
闭包。闭包在两个 print ()
调用之间执行:
1 | func travel(action: () -> Void) { |
由于函数的最后一个参数是闭包,我们可以用拖尾闭包语法来调用 travel ()
函数,就像这样:
1 | travel () { |
实际上,由于函数没有别的参数了,我们还可以将圆括号完全移除:
1 | travel { |
拖尾闭包语法在 Swift 中非常常见,所以你要适应它。
使用接收参数的闭包作为函数的参数
接下来要说到的闭包用法会有点复杂:当你把闭包作为函数参数时,闭包本身也接收参数。
前面我们用 () -> Void
来表示 “不接收参数,并且什么也不返回”,但实际上你可以在 ()
里填上你任何想要闭包接收的参数类型。
再次用到 travel ()
函数。函数只接收一个闭包作为参数,但这次闭包会接收一个字符串参数:
1 | func travel(action: (String) -> Void) { |
现在,当我们采用拖尾闭包语法调用 travel ()
时,我们的闭包代码会要求接收一个字符串:
1 | travel { (place: String) in |
使用有返回值的闭包作为函数的参数
我们之前用 () -> Void
来表示 “不接收参数,并且什么也不返回”。你可以把 Void
替换成任意的类型从而让闭包可以返回值。
还是 travel ()
函数,这次闭包会返回一个字符串。
1 | func travel(action: (String) -> String) { |
仍然用拖尾闭包语法来调用 travel ()
,闭包要求接收一个字符串并且返回一个字符串:
1 | travel { (place: String) -> String in |
速记参数名
前面我们了构建 travel ()
函数。它接收一个闭包作为参数,这个闭包本身接收一个参数并且返回一个字符串,它在两个 print ()
调用之间运行。
代码如下:
1 | func travel(action: (String) -> String) { |
我们可以像这样调用 travel ()
:
1 | travel { (place: String) -> String in |
不过, Swift 知道提供给闭包的参数必须是一个字符串,所以调用的代码可以简写成这样:
1 | travel { place -> String in |
Swfit 也知道闭包必须返回一个字符串,于是进一步简写:
1 | travel { place in |
由于这里的闭包只有一行代码,这行代码肯定是返回值的那行代码,因此 Swift 允许我们把 return
关键字也移除:
1 | travel { place in |
最后, Swift 还提供一种速记语法,让你可以把代码变得更短。我们可以让 Swift 为闭包的参数自动提供一个名字,而不必自行写下 place in
。这些自动生成的名字以 $ 开头,然后跟着一个从 0 开始的整数,就像下面这样:
1 | travel { |
有多个参数的闭包
让我们把闭包这个概念一次讲透吧。接下来举一个接收两个参数的闭包的例子。
将 travel ()
函数改造一下,不仅接收旅行目的地,也接收速度。闭包的类型会变成 (String, Int) -> String
:
1 | func travel(action: (String, Int) -> String) { |
再一次用速记闭包参数名来调用函数。由于这次闭包有两个参数了,于是自动参数名分别是 $0
和 $1
:
1 | travel { |
有些人可能不喜欢用速记参数名,因为它们的语义不是很清晰。你可以根据自己的喜好来决定是否采用它们。不过了解一下这个语法还是必要的,这样读到别人的代码时就不会感到困惑。
从函数中返回闭包
就如同你可以把闭包传入函数那样,你也可以从函数中返回闭包。
返回闭包的语法看起来有点绕,因为用了两次 ->
:第一次用于指定函数的返回值,第二次用于指定闭包的返回值。
又又又要把 travel ()
函数拉出来了。这次它不接收参数,但返回一个闭包。这个返回的闭包在用的时候必须传入一个字符串,但闭包本身没有返回值。
Swift 代码长这样:
1 | func travel() -> (String) -> Void { |
接下来我们通过调用 travel ()
拿到闭包,然后作为函数来调用:
1 | let result = travel () |
留意下面的代码,它是直接调用 travel ()
的返回值。这个写法虽然在语法上完全没问题,但是可读性较差,建议尽量不要这样写。
1 | let result2 = travel ()(" 北京 & quot;) |
捕获变量
如果你想要使用闭包之外的对象, Swift 会为你 “捕捉” 它们,并把它们和闭包一同存储,以便外部作用域已经失效的情况下闭包内部还可以使用它们。
最后一次用到 travel ()
函数,它返回一个闭包,这个闭包接收字符串作为唯一的参数并且什么也不返回:
1 | func travel() -> (String) -> Void { |
调用 travel ()
拿到闭包,然后自由使用:
1 | let result = travel () |
闭包捕获变量可以发生在什么情况下呢?举个例子,当 travel ()
函数内创建了一个变量,这个变量需要在闭包里面用到,那么这个变量就会被闭包捕获。比如,我们想知道闭包被调用的次数:
1 | func travel() -> (String) -> Void { |
尽管 counter
变量是在 travel ()
里被创建的,它被闭包捕获,因而会在闭包内部存续。
当我们多次调用 result (" 北京 ")
,计数器会持续增加:
1 | result (" 北京 & quot;) |
总结
让我们来总结一下。
- 你可以把闭包赋值给变量,之后再用变量名来调用闭包。
- 闭包和常规函数一样可以接收参数和返回值。
- 你可以将闭包作为参数传入函数,并且这些闭包也可以有自己的参数和返回值。
- 如果函数的最后一个参数是闭包,你可以使用拖尾闭包语法。
- Swift 为拖尾闭包语法自动生成了
$0
和$1
这样的速记闭包参数名,但不是所有人都习惯这种速记法。 - 如果你在闭包中使用了外部变量,这些变量将被闭包 “捕捉” 以便后续引用。