闭包
闭包是自包含的功能块,可以在代码中传递和使用。 Swift中的闭包与C和Objective-C中的块以及其他编程语言中的lambda类似。
闭包可以从定义的上下文中捕获和存储对任何常量和变量的引用。这被称为关闭那些常量和变量。 Swift处理所有为你捕获的内存管理。
注意
如果您不熟悉捕捉概念,请不要担心。它在下面的捕获值中有详细的解释。
在函数中引入的全局函数和嵌套函数实际上是闭包的特例。闭包采取以下三种形式之一:
全局函数是具有名称并且不捕获任何值的闭包。
嵌套函数是具有名称的闭包,可以从其封闭函数中捕获值。
Closure表达式是以轻量级语法编写的未命名的闭包,可以捕获周围环境中的值。
Swift的闭包表达式具有干净清晰的风格,优化可以在常见场景中促进简洁,无混乱的语法。这些优化包括:
从上下文中推断参数和返回值类型
来自单表达式闭包的隐式返回
速记参数名称
尾随闭包语法
闭包表达式
嵌套函数中引入的嵌套函数是一种方便的方式,可以将自包含的代码块作为更大函数的一部分进行命名和定义。但是,编写较短版本的类似功能的结构(没有完整的声明和名称)有时很有用。当您使用将函数作为一个或多个参数的函数或方法时,尤其如此。
Closure表达式是一种用简短的聚焦语法编写内联闭包的方法。 Closure表达式提供了几种语法优化,用于以缩写形式编写闭包,而不会损失清晰度或意图。下面的闭包表达式示例通过几次迭代改进排序(by :)方法的单个示例来说明这些优化,每个迭代都以更简洁的方式表达相同的功能。
排序方法
Swift的标准库提供了一个名为sorted(by :)的方法,该方法根据您提供的排序闭包的输出对已知类型的值数组进行排序。排序过程完成后,排序后的(by :)方法将返回一个与旧排序相同类型和大小的新数组,其元素按正确的排序顺序排列。原始数组不会被排序的(by :)方法修改。
下面的关闭表达式示例使用排序的(by :)方法按字母顺序对字符串值数组进行排序。这是要排序的初始数组:
letnames = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
排序后的(by :)方法接受一个闭包,该闭包接受与数组内容相同类型的两个参数,并返回一个Bool值,以表示一旦值排序后,第一个值应该出现在第二个值之前还是之后。 如果第一个值应该出现在第二个值之前,则排序闭包需要返回true,否则返回false。
这个例子是对一个String值的数组进行排序,因此排序闭包需要是类型(String,String) - > Bool的函数。
提供排序闭包的一种方法是编写正确类型的普通函数,并将其作为参数传递给排序的(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之前。 对于字符串中的字符,“大于”意味着“字母后面出现比”。 这意味着字母“B”大于字母“A”,字符串“Tom”大于字符串“Tim”。 这给出了一个反向字母排序,“Barry”被放置在“Alex”之前,依此类推。
然而,这是写一个基本上是单表达式函数(a> b)的相当冗长的方法。 在这个例子中,最好使用闭包表达式语法来内联编写排序闭包。
闭包表达式语法
Closure表达式语法具有以下一般形式:
{ (parameters) -> return typein
statements
}
闭包表达式语法中的参数可以是输入参数,但它们不能有默认值。 如果命名可变参数,则可以使用变量参数。 元组也可以用作参数类型和返回类型。
下面的例子显示了早期的backward(_:_:)函数的闭包表达式版本:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Boolin
returns1 > s2
})
请注意,此内联闭包的参数和返回类型的声明与backward(_:_:)函数的声明相同。 在这两种情况下,它都写为(s1:String,s2:String) - > Bool。 但是,对于内联闭包表达式,参数和返回类型写在花括号内,而不是外部。
关键字引入了关闭的开始。 这个关键字表示闭包的参数和返回类型的定义已经完成,闭包的主体即将开始。
由于封闭体的体积非常短,所以它甚至可以写在一行上:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Boolinreturns1 > s2 } )
这说明对排序(by :)方法的整体调用保持不变。 一对圆括号仍然包含了该方法的整个参数。 但是,这个论点现在是一个内联关闭。
从上下文推断类型
因为排序闭包作为参数传递给方法,所以Swift可以推断它的参数类型和它返回的值的类型。 排序后的(by :)方法在字符串数组上调用,所以它的参数必须是类型(String,String) - > Bool的函数。 这意味着(String,String)和Bool类型不需要作为闭包表达式定义的一部分写入。 由于可以推断所有类型,因此也可以省略返回箭头( - >)和参数名称周围的括号:
reversedNames = names.sorted(by: { s1, s2inreturns1 > s2 } )
在将闭包作为内联闭包表达式传递给函数或方法时,始终可以推断参数类型和返回类型。 因此,当闭包用作函数或方法参数时,您绝不需要以最充分的形式编写内联闭包。
尽管如此,如果您愿意的话,仍然可以明确类型,如果避免代码读者含糊不清,那么可以这样做。 在排序(by :)方法的情况下,闭包的目的从排序发生的事实中清楚,并且读者可以认为闭包可能与String值一起工作是安全的,因为 它正在帮助排序字符串数组。
来自单表达式闭包的隐式返回
通过在声明中省略return关键字,单表达式闭包可隐式返回其单个表达式的结果,如前面示例的此版本所示:
reversedNames = names.sorted(by: { s1, s2ins1 > s2 } )
这里,排序后的(by :)方法参数的函数类型清楚地表明了闭包必须返回一个Bool值。 由于闭包的主体包含一个返回Bool值的单个表达式(s1> s2),因此不存在歧义,并且可以省略return关键字。
速记参数名称
Swift自动为内联闭包提供简写参数名称,这些名称可用于通过名称$ 0,$ 1,$ 2等来引用闭包参数的值。
如果在闭包表达式中使用这些简写参数名称,则可以从其定义中省略闭包的参数列表,并且将从预期的函数类型中推断简写参数名称的数量和类型。 关键字也可以省略,因为闭包表达式完全由它的主体组成:
reversedNames = names.sorted(by: { $0 > $1 } )
这里,$ 0和$ 1引用闭包的第一个和第二个String参数。
运算符方法
实际上有一个更短的方法来编写上面的闭包表达式。 Swift的String类型将其大于运算符(>)的字符串特定实现定义为具有两个String类型参数的方法,并返回一个类型为Bool的值。 这与排序的(by :)方法所需的方法类型完全匹配。 因此,你可以简单地传入大于运算符,Swift会推断你想使用它的字符串特定实现:
reversedNames = names.sorted(by: >)
有关运算符方法的更多信息,请参阅运算符方法
追踪关闭
如果您需要将闭包表达式作为函数的最终参数传递给函数,并且闭包表达式很长,那么将其作为尾部闭包编写可能会很有用。 尾随闭包在函数调用的括号后面写入,尽管它仍然是该函数的参数。 在使用尾随闭包语法时,不要将闭包的参数标签作为函数调用的一部分写入。
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
}
上面的Closure Expression Syntax部分中的字符串排序闭包可以在排序的(by :)方法的括号之外写为尾部闭包:
reversedNames = names.sorted() { $0 > $1 }
如果提供闭包表达式作为函数或方法的唯一参数,并且将该表达式作为尾随闭包提供,则在调用该函数时,无需在函数或方法名称后面编写一对括号():
reversedNames = names.sorted { $0 > $1 }
当封闭足够长以至于不可能将它写在一行上时,尾随封闭非常有用。 作为一个例子,Swift的数组类型有一个map(_ :)方法,它将一个闭包表达式作为其单个参数。 对数组中的每个项目调用一次闭包,并为该项目返回一个替代映射值(可能是某种其他类型)。map(_:)的本质和返回值的类型留给闭包来指定。
将提供的闭包应用于每个数组元素后,map(_:)方法返回一个包含所有新映射值的新数组,其顺序与原始数组中的相应值相同。
以下是如何将map(_ :)方法与尾部闭包一起使用将Int值数组转换为String值数组。 数组[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数组创建一个String值的数组,通过将闭包表达式作为尾随闭包传递给数组的map(_ :)方法:
let strings = numbers.map { (number) -> Stringin
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(_ :)方法为数组中的每个项调用一次闭包表达式。您不需要指定闭包的输入参数类型,因为可以从要映射的数组中的值中推断出该类型。
在这个例子中,变量数用闭包数值参数的值初始化,以便可以在闭包体内修改该值。 (函数和闭包的参数总是常量。)闭包表达式还指定返回类型的字符串,以指示将存储在映射的输出数组中的类型。
每次调用时,闭包表达式都会建立一个名为output的字符串。它使用余数运算符(数字%10)计算最后一位数字,并使用此数字在digitNames字典中查找适当的字符串。闭包可以用来创建任何大于零的整数的字符串表示。
注意
对digitNames字典下标的调用后面跟着一个感叹号(!),因为字典下标会返回一个可选值,以指示如果该键不存在,字典查找可能会失败。在上面的例子中,保证数字%10将始终是digitNames字典的有效下标键,因此使用感叹号来强制解开存储在下标可选返回值中的字符串值。
从digitNames字典中检索到的字符串被添加到输出的前面,从而有效地构建一个字符串版本的数字。 (表达式编号%10给出了16的值,对于58给出8,对于510给出0。)
然后将数字变量除以10.因为它是一个整数,所以在除法期间向下舍入,所以16变成1,58变成5,并且510变成51。
重复该过程直到number等于0,此时输出字符串由闭包返回,并通过map(_ :)方法添加到输出数组中。
在上面的例子中,使用尾部闭包语法在闭包支持的函数后立即封闭闭包的功能,而不需要将整个闭包封装在map(_:)的外部圆括号内。
捕捉Values
闭包可以捕获定义它的周围环境中的常量和变量。即使定义常量和变量的原始范围不再存在,闭包也可以引用并修改其正文中的那些常量和变量的值。
在Swift中,可以捕获值的闭包的最简单形式是嵌套函数,写在另一个函数的主体中。嵌套函数可以捕获任何外部函数的参数,也可以捕获外部函数中定义的任何常量和变量。
以下是一个名为makeIncrementer的函数示例,其中包含一个名为incrementer的嵌套函数。嵌套的incrementer()函数从其周围的上下文中捕获两个值,runningTotal和amount。在捕获这些值之后,incrementer由makeIncrementer返回,作为一个闭包,在每次调用时增加runningTotal量。
func makeIncrementer(forIncrementamount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
returnincrementer
}
makeIncrementer的返回类型是() - > Int。这意味着它返回一个函数,而不是一个简单的值。它返回的函数没有参数,每次调用时都会返回一个Int值。要了解函数如何返回其他函数,请参阅函数类型作为返回类型。
makeIncrementer(forIncrement :)函数定义了一个名为runningTotal的整型变量,用于存储将返回的增量器的当前运行总数。该变量初始值为0。
makeIncrementer(forIncrement :)函数具有单个Int参数,其参数标号为forIncrement,参数名称为amount。传递给此参数的参数值指定每次调用返回的增量函数时应增加runningTotal的数量。 makeIncrementer函数定义了一个称为增量器的嵌套函数,它执行实际增量。该函数只是将量添加到runningTotal中,并返回结果。
当单独考虑时,嵌套的incrementer()函数可能看起来不正常:
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
incrementmenter()函数没有任何参数,但它引用runningTotal和它的函数体内的数量。 它通过捕获对runningTotal的引用和来自周围函数的数量并在它自己的函数体内使用它们来实现这一点。 通过引用捕获可确保在对makeIncrementer的调用结束时,runningTotal和amount不会消失,并且还确保在下次调用增量函数时runningTotal可用。
注意
作为一个优化,Swift可以取而代之地捕获并存储一个值的副本,如果该值没有被闭包变异,并且该值在闭包创建后没有变异。
Swift还处理所有涉及处理变量时不再需要的内存管理。
这是一个makeIncrementer实例的例子:
letincrementByTen = makeIncrementer(forIncrement: 10)
这个例子设置了一个名为incrementByTen的常量来引用一个incrementer函数,每次调用时它都会在runTotal变量中加10。 多次调用该函数会显示这种行为:
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30
如果您创建第二个incrementer,它将拥有自己的存储引用,以指向一个新的单独的runningTotal变量:
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7
再次调用原始incrementer(incrementByTen)将继续增加其自己的runningTotal变量,但不会影响incrementBySeven捕获的变量:
incrementByTen()
// returns a value of 40
注意
如果将闭包分配给类实例的属性,并且闭包通过引用实例或其成员来捕获该实例,则将在闭包和实例之间创建一个强引用循环。 Swift使用捕获列表来打破这些强大的参考周期。有关更多信息,请参阅闭合强参考周期。
闭包是参考类型
在上面的例子中,incrementBySeven和incrementByTen是常量,但这些常量引用的闭包仍然能够增加它们捕获的runningTotal变量。这是因为函数和闭包是引用类型。
无论何时将函数或闭包分配给常量或变量,实际上都是将该常量或变量设置为函数或闭包的引用。在上面的例子中,incrementByTen指的是常量,而不是闭包本身的内容。
这也意味着,如果将一个闭包分配给两个不同的常量或变量,那么这两个常量或变量都将引用相同的闭包:
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50
逃逸关闭
当闭包被作为参数传递给函数时,闭包被称为转义函数,但在函数返回后被调用。 当你声明一个将闭包作为其参数的函数时,你可以在参数的类型之前写入@escaping来表示允许闭包被转义。
闭包可以逃脱的一种方式是存储在函数外部定义的变量中。 作为例子,许多启动异步操作的函数都将闭包参数作为完成处理程序。 该函数在开始操作后返回,但在操作完成之前不会调用闭包 - 闭包需要转义,稍后调用。 例如:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
someFunctionWithEscapingClosure(_ :)函数将闭包作为其参数,并将其添加到在函数外部声明的数组。 如果你没有用@escaping标记这个函数的参数,你会得到一个编译时错误。
用@escaping标记闭包意味着你必须在闭包中明确地引用自己。 例如,在下面的代码中,传递给someFunctionWithEscapingClosure(_ :)的闭包是一个转义闭包,这意味着它需要明确地引用自己。 相比之下,传递给someFunctionWithNonescapingClosure(_ :)的闭包是一个非转义闭包,这意味着它可以隐式引用自己。
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
class SomeClass {
varx = 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
autoclosure是一个自动创建的闭包,用于封装作为参数传递给函数的表达式。它不需要任何参数,当它被调用时,它会返回包装在其中的表达式的值。这种语法上的便利可以让你通过写一个普通的表达式而不是显式的闭包来省略函数参数的大括号。
通常调用采用自动屏蔽的函数,但实现这种功能并不常见。例如,assert(condition:message:file:line :)函数为其条件和消息参数采用autoclosure;其条件参数仅在调试版本中评估,并且仅当条件为假时才评估其消息参数。
autoclosure让你延迟评估,因为在你调用闭包之前,里面的代码不会运行。延迟评估对于有副作用或计算成本较高的代码非常有用,因为它可以让您控制代码的评估时间。下面的代码显示了封闭延迟评估的方式
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(customercustomerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"
上面列表中的serve(customer :)函数使用一个显式的闭包来返回一个客户的名字。 下面的服务(客户:)版本执行相同的操作,但不是采用显式闭包,而是通过使用@autoclosure属性标记其参数类型来采用autoclosure。 现在你可以调用函数,就好像它使用String参数而不是闭包。 该参数会自动转换为闭包,因为customerProvider参数的类型使用@autoclosure属性标记。
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customercustomerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"
注意
过度使用自动遮挡会使您的代码难以理解。 上下文和函数名称应该明确表示评估正在推迟。
如果您想要允许转义的自动关闭,请同时使用@autoclosure和@escaping属性。 上面的Escaping Closures中描述了@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 customerProviderincustomerProviders {
print("Now serving \(customerProvider())!")
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"
在上面的代码中,collectCustomerProviders(_ :)函数不是将传递给它的闭包作为其customerProvider参数进行调用,而是将闭包附加到customerProviders数组。 数组声明在函数范围之外,这意味着数组中的闭包可以在函数返回后执行。 因此,必须允许customerProvider参数的值转义该函数的作用域。