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