闭包是自包含的功能块,可以在代码中传递和使用。Swift中的闭包类似于C和Objective-C中的块以及其他编程语言中的lambdas。
闭包可以捕获和存储对定义它们的上下文中的任何常量和变量的引用。这被称为闭包那些常量和变量。Swift为您处理捕获的所有内存管理。
注意
如果您不熟悉捕获的概念,请不要担心。下面在捕获值中详细说明。
全局和嵌套函数,如捕获的功能,实际上是封闭的特殊情况。闭包采用以下三种形式之一:
- 全局函数是具有名称但不捕获任何值的闭包。
- 嵌套函数是具有名称的闭包,可以从其封闭函数中捕获值。
- Closure表达式是一种未命名的闭包,用轻量级语法编写,可以从周围的上下文中捕获值。
Swift的闭包表达式具有干净,清晰的风格,优化可以在常见场景中鼓励简洁,无杂乱的语法。这些优化包括:
- 从上下文中推断参数和返回值类型
- 单表达式闭包的隐式返回
- 速记参数名称
- 尾随闭包语法
闭包表达式
嵌套函数(在嵌套函数中引入)是一种方便的方法,可以将自包含的代码块命名和定义为更大函数的一部分。但是,在没有完整声明和名称的情况下编写类似函数的构造的更短版本有时是有用的。当您使用将函数作为其一个或多个参数的函数或方法时,尤其如此。
Closure表达式是一种以简短,集中的语法编写内联闭包的方法。Closure表达式提供了几种语法优化,用于以缩短的形式编写闭包,而不会丢失清晰度或意图。下面的闭包表达式示例通过sorted(by:)
在几次迭代中细化方法的单个示例来说明这些优化,每次迭代都以更简洁的方式表达相同的功能。
排序方法
Swift的标准库提供了一个名为的方法sorted(by:)
,它根据您提供的排序闭包的输出对已知类型的值数组进行排序。完成排序过程后,该sorted(by:)
方法返回一个与旧数组相同类型和大小的新数组,其元素按正确的排序顺序排列。该sorted(by:)
方法不会修改原始数组。
下面的闭包表达式示例使用该sorted(by:)
方法以String
反向字母顺序对值数组进行排序。这是要排序的初始数组:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
该sorted(by:)
方法接受一个闭包,它接受与数组内容相同类型的两个参数,并返回一个Bool
值,表示一旦值被排序,第一个值是出现在第二个值之前还是之后。true
如果第一个值应出现在第二个值之前,则需要返回排序闭包,false
否则返回。
这个例子是对String
值数组进行排序,因此排序闭包需要是类型的函数。(String, String) -> Bool
提供排序闭包的一种方法是编写正确类型的普通函数,并将其作为参数传递给sorted(by:)
方法:
1 func backward(_ s1: String, _ s2: String) -> Bool { 2 return s1 > s2 3 } 4 var reversedNames = names.sorted(by: backward) 5 // reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
如果第一个字符串(s1
)大于第二个字符串(s2
),则backward(_:_:)
函数将返回true
,表示s1
应该s2
在排序数组之前出现。对于字符串中的字符,“大于”表示“在字母表后面出现”。这意味着字母"B"
“大于”字母"A"
,字符串"Tom"
大于字符串"Tim"
。这给出了一个反向字母排序,"Barry"
放在之前"Alex"
,依此类推。
然而,这是一种相当冗长的方式来编写本质上是单表达式函数。在此示例中,最好使用闭包表达式语法内联编写排序闭包。a > b
闭包表达式语法
Closure表达式语法具有以下一般形式:
1 { (parameters) -> return type in 2 statements 3 }
该参数在封闭表达式语法可以在输出参数,但是他们不能有一个默认值。如果命名variadic参数,则可以使用变量参数。元组也可以用作参数类型和返回类型。
下面的示例显示了backward(_:_:)
上面函数的闭包表达式版本:
1 reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in 2 return s1 > s2 3 })
请注意,此内联闭包的参数声明和返回类型与backward(_:_:)
函数声明相同。在这两种情况下,它都写成。但是,对于内联闭包表达式,参数和返回类型都写在大括号内,而不是在小括号内。(s1: String, s2: String) -> Bool
闭包的主体的开头由in
关键字引入。这个关键字表示闭包的参数和返回类型的定义已经完成,闭包的主体即将开始。
因为封闭的主体很短,所以甚至可以写在一行上:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )
这说明对sorted(by:)
方法的整体调用保持不变。一对括号仍然包装该方法的整个参数。但是,该参数现在是内联闭包。
从上下文中推断类型
因为排序闭包作为参数传递给方法,所以Swift可以推断出它的参数类型以及它返回的值的类型。该sorted(by:)
方法是在字符串数组上调用的,因此其参数必须是类型的函数。这意味着不需要将和类型作为闭包表达式定义的一部分来编写。因为可以推断出所有类型,所以也可以省略返回箭头(->)和参数名称周围的括号:(String, String) -> Bool
(String, String)
Bool
->
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
在将闭包作为内联闭包表达式传递给函数或方法时,始终可以推断出参数类型和返回类型。因此,当闭包用作函数或方法参数时,您永远不需要以最完整的形式编写内联闭包。
尽管如此,如果您愿意,仍然可以使类型显式化,如果它避免了代码读者的歧义,则鼓励这样做。在该sorted(by:)
方法的情况下,闭包的目的很明显,因为正在进行排序,并且读者可以安全地假设闭包可能正在使用String
值,因为它有助于排序。一串字符串。
单表达式闭包的隐式返回
单表达式闭包可以通过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
,并返回type的值Bool
。这与方法所需的方法类型完全匹配sorted(by:)
。因此,您可以简单地传入大于运算符,Swift将推断您要使用其特定于字符串的实现:
reversedNames = names.sorted(by: { $0 > $1 } )
欲了解更多有关操作方法,请参阅操作方法。
尾随闭包
如果需要将闭包表达式作为函数的最终参数传递给函数,并且闭包表达式很长,则将其写为尾随闭包可能很有用。在函数调用的括号之后写入尾随闭包,即使它仍然是函数的参数。使用尾随闭包语法时,不要将闭包的参数标签写为函数调用的一部分。
1 func someFunctionThatTakesAClosure(closure: () -> Void) { 2 // function body goes here 3 } 4 5 // Here's how you call this function without using a trailing closure: 6 7 someFunctionThatTakesAClosure(closure: { 8 // closure's body goes here 9 }) 10 11 // Here's how you call this function with a trailing closure instead: 12 13 someFunctionThatTakesAClosure() { 14 // trailing closure's body goes here 15 }
上面的Closure Expression Syntax部分的字符串排序闭包可以sorted(by:)
作为尾随闭包写在方法的括号之外:
reversedNames = names.sorted() { $0 > $1 }
如果提供闭包表达式作为函数或方法的唯一参数,并且您将该表达式作为尾随闭包提供,则()
在调用函数时,不需要在函数或方法的名称后面写一对括号:
reversedNames = names.sorted { $0 > $1 }
当闭包足够长以至于无法将其内联写入单行时,尾随闭包最有用。作为一个例子,Swift的Array
类型有一个map(_:)
方法,它将一个闭包表达式作为它的单个参数。对数组中的每个项调用一次闭包,并为该项返回一个替代映射值(可能是某些其他类型)。映射的性质和返回值的类型留给要指定的闭包。
将提供的闭包应用于每个数组元素后,该map(_:)
方法返回一个包含所有新映射值的新数组,其顺序与原始数组中的相应值相同。
以下是如何使用map(_:)
带尾随闭包的方法将Int
值数组转换为值数组String
。该数组用于创建新数组:[16, 58, 510]
["OneSix", "FiveEight", "FiveOneZero"]
1 let digitNames = [ 2 0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four", 3 5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine" 4 ] 5 let numbers = [16, 58, 510]
上面的代码创建了整数位和其名称的英语版本之间的映射字典。它还定义了一个整数数组,可以转换为字符串。
您现在可以通过将闭包表达式作为尾随闭包传递给数组的方法来使用该numbers
数组来创建String
值数组map(_:)
:
1 let strings = numbers.map { (number) -> String in 2 var number = number 3 var output = "" 4 repeat { 5 output = digitNames[number % 10]! + output 6 number /= 10 7 } while number > 0 8 return output 9 } 10 // strings is inferred to be of type [String] 11 // its value is ["OneSix", "FiveEight", "FiveOneZero"]
该map(_:)
方法为数组中的每个项调用一次闭包表达式。您不需要指定闭包的输入参数number
的类型,因为可以从要映射的数组中的值推断出类型。
在此示例中,number
使用closure的number
参数值初始化变量,以便可以在闭包体内修改该值。(函数和闭包的参数总是常量。)闭包表达式还指定了返回类型String
,以指示将存储在映射的输出数组中的类型。
闭包表达式构建一个output
每次调用时都调用的字符串。它number
使用余数运算符()计算最后一位数字,并使用该数字在字典中查找适当的字符串。闭包可用于创建大于零的任何整数的字符串表示。number % 10
digitNames
注意
对digitNames
字典下标的调用后跟一个感叹号(!
),因为字典下标返回一个可选值,表示如果该键不存在,字典查找可能会失败。在上面的示例中,保证始终是字典的有效下标键,因此使用感叹号强制解包存储在下标的可选返回值中的值。number % 10
digitNames
String
从检索到的字符串digitNames
辞典被添加到前面的output
,有效地建立反向一数目的字符串版本。(该表达式给出for ,for 和for的值。)number % 10
6
16
8
58
0
510
number
然后将变量除以10
。因为它是一个整数,所以它在分割期间向下舍入,因此16
变为1
,58
变为5
,510
变为51
。
重复该过程直到number
等于0
,此时output
字符串由闭包返回,并通过该map(_:)
方法添加到输出数组。
在上面的示例中使用尾随闭包语法在闭包支持的函数之后立即巧妙地封装了闭包的功能,而无需在map(_:)
方法的外括号内包装整个闭包。
捕捉常量和变量
闭包可以从定义它的周围上下文中捕获常量和变量。然后闭包可以引用并修改其体内的常量和变量的值,即使定义常量和变量的原始范围不再存在。
在Swift中,可以捕获值的最简单形式的闭包是嵌套函数,写在另一个函数体内。嵌套函数可以捕获其外部函数的任何参数,还可以捕获外部函数中定义的任何常量和变量。
这是一个调用函数的示例makeIncrementer
,其中包含一个名为的嵌套函数incrementer
。嵌套incrementer()
函数捕获两个值,runningTotal
并且amount
,从它的周围环境。捕获这些值后,incrementer
将makeIncrementer
作为一个闭包返回runningTotal
,amount
每次调用时它都会递增。
1 func makeIncrementer(forIncrement amount: Int) -> () -> Int { 2 var runningTotal = 0 3 func incrementer() -> Int { 4 runningTotal += amount 5 return runningTotal 6 } 7 return incrementer 8 }
返回类型makeIncrementer
是。这意味着它返回一个函数,而不是一个简单的值。它返回的函数没有参数,每次调用时都返回一个值。要了解函数如何返回其他函数,请参见函数类型作为返回类型。() -> Int
Int
该makeIncrementer(forIncrement:)
函数定义了一个名为的整数变量runningTotal
,用于存储将返回的增量器的当前运行总数。此变量初始化为值0
。
该makeIncrementer(forIncrement:)
函数有一个Int
参数标签为forIncrement
的参数,参数名称为amount
。传递给此参数的参数值指定runningTotal
每次调用返回的增量函数时应递增多少。该makeIncrementer
函数定义了一个名为的嵌套函数incrementer
,它执行实际的递增。此功能只是增加了amount
对runningTotal
,并返回结果。
在单独考虑时,嵌套incrementer()
函数可能看起来不常见:
1 func incrementer() -> Int { 2 runningTotal += amount 3 return runningTotal 4 }
该incrementer()
函数没有任何参数,但它在其函数体中引用runningTotal
和引用amount
。它通过捕获做到这一点参考,以runningTotal
和amount
从周围的功能和其自身的函数体中使用它们。通过参考捕捉保证runningTotal
和amount
不消失的时候调用makeIncrementer
结束,而且也保证了runningTotal
可用下一次incrementer
函数被调用。
注意
作为优化,如果该值未被闭包变异,并且在创建闭包后该值未发生变化,则Swift可以改为捕获并存储值的副本。
Swift还处理在不再需要变量时处理变量所涉及的所有内存管理。
这是一个实际的例子makeIncrementer
:
let incrementByTen = makeIncrementer(forIncrement: 10)
此示例设置一个常量incrementByTen
,该常量称为引用增量函数,10
该函数在runningTotal
每次调用时都会添加到其变量中。多次调用该函数会显示此行为:
1 incrementByTen() 2 // returns a value of 10 3 incrementByTen() 4 // returns a value of 20 5 incrementByTen() 6 // returns a value of 30
如果您创建第二个增量器,它将拥有自己存储的对新的单独runningTotal
变量的引用:
1 let incrementBySeven = makeIncrementer(forIncrement: 7) 2 incrementBySeven() 3 // returns a value of 7
incrementByTen
再次调用原始增量器()会继续增加其自己的runningTotal
变量,并且不会影响由incrementBySeven
以下内容捕获的变量:
1 incrementByTen() 2 // returns a value of 40
注意
如果为类实例的属性分配闭包,并且闭包通过引用实例或其成员来捕获该实例,则将在闭包和实例之间创建一个强引用循环。Swift使用捕获列表来打破这些强大的参考周期。有关更多信息,请参阅闭包的强引用周期。
闭包是引用类型
在上面的例子中,incrementBySeven
并且incrementByTen
是常量,但这些常量引用的闭包仍然能够增加runningTotal
它们捕获的变量。这是因为函数和闭包是引用类型。
无论何时将函数或闭包赋值给常量或变量,实际上都是将该常量或变量设置为对函数或闭包的引用。在上面的例子中,闭包的选择incrementByTen
引用的是常量,而不是闭包本身的内容。
这也意味着如果为两个不同的常量或变量分配闭包,那么这两个常量或变量都引用相同的闭包。
1 let alsoIncrementByTen = incrementByTen 2 alsoIncrementByTen() 3 // returns a value of 50 4 5 incrementByTen() 6 // returns a value of 60
上面的例子显示调用alsoIncrementByTen
与调用相同incrementByTen
。因为它们都引用相同的闭包,它们都会递增并返回相同的运行总计。
离开闭包
离开闭包是说离开当闭包作为参数传递给函数,但在函数返回之后被调用的函数。当声明一个以闭包作为其参数之一的函数时,可以@escaping
在参数的类型之前写入,以指示允许闭包转义。
闭包可以转义的一种方法是存储在函数外部定义的变量中。作为示例,许多启动异步操作的函数将闭包参数作为完成处理程序。该函数在开始操作后返回,但是在操作完成之前不会调用闭包 - 闭包需要转义,以便稍后调用。例如:
1 var completionHandlers: [() -> Void] = [] 2 func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) { 3 completionHandlers.append(completionHandler) 4 }
该someFunctionWithEscapingClosure(_:)
函数将闭包作为其参数,并将其添加到在函数外部声明的数组中。如果没有用此标记此函数的参数@escaping
,则会出现编译时错误。
标记闭包@escaping
意味着必须self
在闭包中明确引用。例如,在下面的代码中,传递给的闭包someFunctionWithEscapingClosure(_:)
是一个转义闭包,这意味着它需要self
显式引用。相反,传递给的闭包someFunctionWithNonescapingClosure(_:)
是一个非自动闭包,这意味着它可以self
隐含地引用。
1 func someFunctionWithNonescapingClosure(closure: () -> Void) { 2 closure() 3 } 4 5 class SomeClass { 6 var x = 10 7 func doSomething() { 8 someFunctionWithEscapingClosure { self.x = 100 } 9 someFunctionWithNonescapingClosure { x = 200 } 10 } 11 } 12 13 let instance = SomeClass() 14 instance.doSomething() 15 print(instance.x) 16 // Prints "200" 17 18 completionHandlers.first?() 19 print(instance.x) 20 // Prints "100"
自动闭包
一个autoclosure是自动创建来包装被真实作为参数传递给函数的表达式的闭包。它不接受任何参数,当它被调用时,它返回包含在其中的表达式的值。这种语法方便性允许您通过编写普通表达式而不是显式闭包来省略函数参数周围的大括号。
这是常见的来电称取autoclosures的功能,但它不是常见的实现那种功能。例如,该assert(condition:message:file:line:)
函数为其condition
和message
参数采用autoclosure ; 它condition
仅在调试参数进行评估,并建立其message
仅在参数评估condition
是false
。
autoclosure允许您延迟评估,因为在您调用闭包之前,内部代码不会运行。延迟评估对于具有副作用或计算成本高昂的代码非常有用,因为它可以让您控制何时评估该代码。下面的代码显示了闭包延迟评估的方式。
1 var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"] 2 print(customersInLine.count) 3 // Prints "5" 4 5 let customerProvider = { customersInLine.remove(at: 0) } 6 print(customersInLine.count) 7 // Prints "5" 8 9 print("Now serving \(customerProvider())!") 10 // Prints "Now serving Chris!" 11 print(customersInLine.count) 12 // Prints "4"
即使customersInLine
数组的第一个元素被闭包内的代码删除,在实际调用闭包之前不会删除数组元素。如果从不调用闭包,则永远不会计算闭包内的表达式,这意味着永远不会删除数组元素。请注意,类型customerProvider
是不是String
,但不带任何参数,返回一个字符串-a功能。() -> String
当您将闭包作为参数传递给函数时,您会得到相同的延迟求值行为。
1 // customersInLine is ["Alex", "Ewa", "Barry", "Daniella"] 2 func serve(customer customerProvider: () -> String) { 3 print("Now serving \(customerProvider())!") 4 } 5 serve(customer: { customersInLine.remove(at: 0) } ) 6 // Prints "Now serving Alex!"
serve(customer:)
上面列表中的函数采用显式闭包,返回客户的名称。下面的版本serve(customer:)
执行相同的操作,但不是采用显式闭包,而是通过使用@autoclosure
属性标记其参数的类型来进行自动闭包。现在,您可以调用该函数,就好像它使用了一个String
参数而不是一个闭包。参数自动转换为闭包,因为customerProvider
参数的类型用@autoclosure
属性标记。
1 // customersInLine is ["Ewa", "Barry", "Daniella"] 2 func serve(customer customerProvider: @autoclosure () -> String) { 3 print("Now serving \(customerProvider())!") 4 } 5 serve(customer: customersInLine.remove(at: 0)) 6 // Prints "Now serving Ewa!"
注意
过度使用autoclosures会使您的代码难以理解。上下文和函数名称应该明确表示正在推迟评估。
如果您想要允许转义的autoclosure,请使用@autoclosure
和@escaping
属性。@escaping
上面在Escaping Closures中描述了该属性。
1 // customersInLine is ["Barry", "Daniella"] 2 var customerProviders: [() -> String] = [] 3 func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) { 4 customerProviders.append(customerProvider) 5 } 6 collectCustomerProviders(customersInLine.remove(at: 0)) 7 collectCustomerProviders(customersInLine.remove(at: 0)) 8 9 print("Collected \(customerProviders.count) closures.") 10 // Prints "Collected 2 closures." 11 for customerProvider in customerProviders { 12 print("Now serving \(customerProvider())!") 13 } 14 // Prints "Now serving Barry!" 15 // Prints "Now serving Daniella!"
在上面的代码中,函数将闭包附加到数组,而不是调用作为customerProvider
参数传递给它collectCustomerProviders(_:)
的闭包customerProviders
。数组声明在函数范围之外,这意味着数组中的闭包可以在函数返回后执行。因此,customerProvider
必须允许参数的值转义函数的作用域。