Swift5.1 语言指南(九) 闭包

Swift闭包详解

★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
➤微信公众号:山青咏芝(shanqingyongzhi)
➤博客园地址:山青咏芝(https://www.cnblogs.com/strengthen/
➤GitHub地址:https://github.com/strengthen/LeetCode
➤原文地址:https://www.cnblogs.com/strengthen/p/9728063.html 
➤如果链接不是山青咏芝的博客园地址,则可能是爬取作者的文章。
➤原文已修改更新!强烈建议点击原文地址阅读!支持作者!支持原创!
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★

闭包是自包含的功能块,可以在代码中传递和使用。Swift中的闭包类似于C和Objective-C中的块以及其他编程语言中的lambdas。

闭包可以从定义它们的上下文中捕获和存储对任何常量和变量的引用。这被称为关闭那些常量和变量。Swift为您处理捕获的所有内存管理。

注意

如果您不熟悉捕获的概念,请不要担心。下面在捕获值中详细解释了它。

全球和嵌套函数,如推出的功能,实际上是封闭的特殊情况。闭包采用以下三种形式之一:

  • 全局函数是具有名称但不捕获任何值的闭包。
  • 嵌套函数是具有名称的闭包,可以从其封闭函数中捕获值。
  • Closure表达式是一种未命名的闭包,用轻量级语法编写,可以从周围的上下文中捕获值。

Swift的闭包表达式具有干净,清晰的风格,优化可以在常见场景中鼓励简洁,无杂乱的语法。这些优化包括:

  • 从上下文中推断参数和返回值类型
  • 单表达式闭包的隐式返回
  • 速记参数名称
  • 尾随闭包语法

关闭表达式

嵌套函数(在嵌套函数中引入)是一种方便的方法,可以将自包含的代码块命名和定义为更大函数的一部分。但是,在没有完整声明和名称的情况下编写类似函数的构造的更短版本有时是有用的。当您使用将函数作为其一个或多个参数的函数或方法时,尤其如此。

Closure表达式是一种以简短,集中的语法编写内联闭包的方法。Closure表达式提供了几种语法优化,用于以缩短的形式编写闭包,而不会丢失清晰度或意图。下面的闭包表达式示例通过sorted(by:)在几次迭代中细化该方法的单个示例来说明这些优化,每个迭代以更简洁的方式表达相同的功能。

排序方法

Swift的标准库提供了一个名为的方法sorted(by:),它根据您提供的排序闭包的输出对已知类型的值数组进行排序。完成排序过程后,该sorted(by:)方法返回一个与旧数组相同类型和大小的新数组,其元素按正确的排序顺序排列。该sorted(by:)方法不会修改原始数组。

下面的闭包表达式示例使用该sorted(by:)方法以String反向字母顺序对值数组进行排序。这是要排序的初始数组:

  1. 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",依此类推。

然而,这是一种相当冗长的方式来编写本质上是单表达式函数()。在这个例子中,最好使用闭包表达式语法内联编写排序闭包。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关键字引入。这个关键字表示闭包的参数和返回类型的定义已经完成,闭包的主体即将开始。

由于封盖的主体很短,甚至可以写在一行上:

  1. reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )

这说明对sorted(by:)方法的整体调用保持不变。一对括号仍然包装该方法的整个参数。但是,该参数现在是内联闭包。

从上下文中推断类型

因为排序闭包作为参数传递给方法,所以Swift可以推断出它的参数类型以及它返回的值的类型。该sorted(by:)方法是在一个字符串数组上调用的,因此它的参数必须是一个类型的函数。这意味着不需要将和类型作为闭包表达式定义的一部分来编写。因为可以推断出所有类型,所以也可以省略返回箭头()和参数名称周围的括号:(String, String) -> Bool(String, String)Bool->

  1. reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

在将闭包作为内联闭包表达式传递给函数或方法时,始终可以推断出参数类型和返回类型。因此,当闭包用作函数或方法参数时,您永远不需要以最完整的形式编写内联闭包。

尽管如此,如果您愿意,仍然可以使类型显式化,如果它避免了代码读者的歧义,则鼓励这样做。在该sorted(by:)方法的情况下,封闭的目的是从分类发生的事实中清楚的,并且读者可以认为封闭可能与String值一起工作是安全的,因为它有助于分类。一串字符串。

单表达式闭包的隐式返回

单表达式闭包可以通过return从声明中省略关键字来隐式返回单个表达式的结果,如上一个示例的此版本中所示:

  1. reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

这里,sorted(by:)方法参数的函数类型清楚地表明Bool闭包必须返回一个值。因为闭包的主体包含一个返回值的表达式(),所以没有歧义,可以省略关键字。s1 s2Boolreturn

速记参数名称

雨燕自动提供速记参数名内联闭包,它可以使用的名称,指的是关闭的参数值$0$1$2,等等。

如果在闭包表达式中使用这些简写参数名称,则可以从其定义中省略闭包的参数列表,并且将从期望的函数类型推断缩写参数名称的数量和类型。的in关键字也可以被省略,因为封闭件表达是由完全其身体的:

  1. reversedNames = names.sorted(by: { $0 > $1 } )

在这里,$0$1参考闭包的第一个和第二个String参数。

操作员方法

实际上有一种更短的方式来编写上面的闭包表达式。Swift的String类型将其大于运算符(>)的字符串特定实现定义为具有两个类型参数的方法String,并返回type的值Bool。这与方法所需的方法类型完全匹配sorted(by:)。因此,您可以简单地传入大于运算符,Swift将推断您要使用其特定于字符串的实现:

  1. reversedNames = names.sorted(by: >)

欲了解更多有关操作方法,请参阅操作方法

尾随闭包

如果需要将闭包表达式作为函数的最终参数传递给函数,并且闭包表达式很长,则将其写为尾随闭包可能很有用。在函数调用的括号之后写入尾随闭包,即使它仍然是函数的参数。使用尾随闭包语法时,不要将闭包的参数标签写为函数调用的一部分。

  1. func someFunctionThatTakesAClosure(closure: () -> Void) {
  2. // function body goes here
  3. }
  4. // Here's how you call this function without using a trailing closure:
  5. someFunctionThatTakesAClosure(closure: {
  6. // closure's body goes here
  7. })
  8. // Here's how you call this function with a trailing closure instead:
  9. someFunctionThatTakesAClosure() {
  10. // trailing closure's body goes here
  11. }

上面的Closure Expression Syntax部分中的字符串排序闭可以sorted(by:)作为尾随闭包写在方法的括号之外:

  1. reversedNames = names.sorted() { $0 > $1 }

如果提供闭包表达式作为函数或方法的唯一参数,并且您将该表达式作为尾随闭包提供,则()在调用函数时,不需要在函数或方法的名称后面写一对括号:

  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 10digitNames

注意

digitNames字典下标的调用后跟一个感叹号(!),因为字典下标返回一个可选值,表示如果该键不存在,字典查找可能会失败。在上面的示例中,保证始终是字典的有效下标键,因此使用感叹号强制解包存储在下标的可选返回值中的值。number 10digitNamesString

从检索到的字符串digitNames辞典被添加到前面output,有效地建立反向一数目的字符串版本。(表达式给出for ,for 和for的值。)number 106168580510

number然后将变量除以10。因为它是一个整数,所以它在分割期间向下舍入,因此16变为158变为5510变为51

重复该过程直到number等于0,此时output字符串由闭包返回,并通过该map(_:)方法添加到输出数组。

在上面的示例中使用尾随闭包语法在闭包支持的函数之后立即巧妙地封装了闭包的功能,而无需将整个闭包包装在map(_:)方法的外括号中。

捕捉价值观

闭包可以从定义它的周围上下文中捕获常量和变量。然后闭包可以引用并修改其体内的常量和变量的值,即使定义常量和变量的原始范围不再存在。

在Swift中,可以捕获值的最简单形式的闭包是嵌套函数,写在另一个函数体内。嵌套函数可以捕获其外部函数的任何参数,还可以捕获外部函数中定义的任何常量和变量。

这是一个调用函数的示例makeIncrementer,其中包含一个名为的嵌套函数incrementer。嵌套incrementer()函数捕获两个值,runningTotal并且amount,从它的周围环境。捕获这些值后,incrementermakeIncrementer作为一个闭包返回runningTotalamount每次调用时它都会递增。

  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是。这意味着它返回一个函数,而不是一个简单的值。它返回的函数没有参数,每次调用时都返回一个值。要了解函数如何返回其他函数,请参见函数类型作为返回类型() -> IntInt

makeIncrementer(forIncrement:)函数定义了一个名为的整数变量runningTotal,用于存储将返回的增量器的当前运行总数。此变量初始化为值0

makeIncrementer(forIncrement:)函数有一个Int参数标签为forIncrement的参数,参数名称为amount。传递给此参数的参数值指定runningTotal每次调用返回的增量函数时应递增多少。该makeIncrementer函数定义了一个名为的嵌套函数incrementer,它执行实际的递增。此功能只是增加了amountrunningTotal,并返回结果。

单独考虑时,嵌套incrementer()函数可能看起来不常见:

  1. func incrementer() -> Int {
  2. runningTotal += amount
  3. return runningTotal
  4. }

incrementer()函数没有任何参数,但它在其函数体内引用runningTotal和引用amount。它通过捕获做到这一点参考,以runningTotalamount从周围的功能和其自身的函数体中使用它们。通过参考捕捉保证runningTotalamount不消失的时候调用makeIncrementer结束,而且也保证了runningTotal可用下一次incrementer函数被调用。

注意

作为优化,如果该值未被闭包变异,并且在创建闭包后该值未发生变化,则Swift可以代之以捕获并存储值的副本

Swift还处理在不再需要变量时处理变量所涉及的所有内存管理。

这是一个实际的例子makeIncrementer

  1. 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. incrementByTen()
  5. // 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. class SomeClass {
  5. var x = 10
  6. func doSomething() {
  7. someFunctionWithEscapingClosure { self.x = 100 }
  8. someFunctionWithNonescapingClosure { x = 200 }
  9. }
  10. }
  11. let instance = SomeClass()
  12. instance.doSomething()
  13. print(instance.x)
  14. // Prints "200"
  15. completionHandlers.first?()
  16. print(instance.x)
  17. // Prints "100"

Autoclosures

一个autoclosure是自动创建来包装被真实作为参数传递给函数的表达式的封闭件。它不接受任何参数,当它被调用时,它返回包含在其中的表达式的值。这种语法方便使您可以通过编写普通表达式而不是显式闭包来省略函数参数周围的大括号。

这是常见的来电称取autoclosures的功能,但它不是常见的实现那种功能。例如,该assert(condition:message:file:line:)函数为其conditionmessage参数采用autoclosure ; 它condition仅在调试参数进行评估,并建立其message仅在参数评估conditionfalse

autoclosure允许您延迟评估,因为在您调用闭包之前,内部代码不会运行。延迟评估对于具有副作用或计算成本高昂的代码非常有用,因为它可以让您控制何时评估该代码。下面的代码显示了关闭延迟评估的方式。

  1. var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
  2. print(customersInLine.count)
  3. // Prints "5"
  4. let customerProvider = { customersInLine.remove(at: 0) }
  5. print(customersInLine.count)
  6. // Prints "5"
  7. print("Now serving \(customerProvider())!")
  8. // Prints "Now serving Chris!"
  9. print(customersInLine.count)
  10. // 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. print("Collected \(customerProviders.count) closures.")
  9. // Prints "Collected 2 closures."
  10. for customerProvider in customerProviders {
  11. print("Now serving \(customerProvider())!")
  12. }
  13. // Prints "Now serving Barry!"
  14. // Prints "Now serving Daniella!"

在上面的代码中,函数将闭包附加到数组,而不是调用作为customerProvider参数传递给它collectCustomerProviders(_:)的闭包customerProviders。数组声明在函数范围之外,这意味着在函数返回后可以执行数组中的闭包。因此,customerProvider必须允许参数的值转义函数的作用域。

转载于:https://www.cnblogs.com/strengthen/p/9728063.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值