Closures初步

Swift强大的闭包功能

Posted by Joker Hook on May 6, 2022

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

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

函数中引入的全局函数和嵌套函数实际上是闭包的特殊情况。关闭采取三种形式之一:

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

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

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

闭包表达式

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

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

排序方法

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

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

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

sorted(by:)方法接受一个闭包,该闭包接受两个与数组内容类型相同的参数,并返回一个Bool值,说明在对值进行排序后,第一个值应该出现在第二个值之前还是之后。如果第一个值出现在第二个值之前,排序闭包需要返回true,否则返回false

此示例是对字符串值数组进行排序,因此排序闭包需要类型为(String,String) -> Bool的函数。

提供排序闭包的一种方法是编写正确类型的正常函数,并将其作为参数传递给sorted(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),则backward(::)函数将返回true,表明s1应该出现在排序数组的s2之前。对于字符串中的字符,“大于”意味着“在字母表中出现得晚于”。这意味着字母“B”大于字母“A”,字符串“Tom”大于字符串“Tim”。这给出了一个反向字母排序,将“Barry”放在“Alex”之前,以此类过。

然而,这是一种相当冗长的方式来写入本质上是单表达式函数(a > b)。在本例中,最好使用闭包表达式语法内联编写排序闭包。

闭包表达式语法

闭包表达式语法具有以下一般形式:

{ (parameters) -> return type in
    statements
}

闭包表达式语法中的参数可以是输入输出参数,但它们不能有默认值。如果您命名变量参数,则可以使用变量参数。元组也可以用作参数类型和返回类型。

下面的示例从上面显示了backward(_:_:)函数的闭包表达式版本:

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

请注意,此内联闭包的参数声明和返回类型与backward(_:_:)函数的声明相同。在这两种情况下,它都写成(s1:String,s2:String) -> Bool。然而,对于内联闭包表达式,参数和返回类型写在花括号内,而不是在花括号内。

闭包正文的开头由关键字引入。此关键字表示闭包参数和返回类型的定义已经完成,闭包正文即将开始。

因为闭包的主体太短了,它甚至可以写在一行上:

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:)方法的情况下,从正在进行排序的事实中可以清楚地看出闭包的目的,读者可以安全地假设闭包可能与字符串值一起工作,因为它有助于对字符串数组进行排序。

来自单表达式关闭的隐式返回

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

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

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

速记参数名称

Swift会自动为内联闭包提供速记参数名称,这些名称可用于以$0、$1、$2等名称来引用闭包参数的值。

如果您在闭包表达式中使用这些速记参数名称,则可以从其定义中省略闭包的参数列表。速记参数名称的类型从预期的函数类型推断出来,您使用的最高编号的速记参数决定了闭包的参数数量。in关键字也可以省略,因为闭包表达式完全由其正文组成:

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

在这里,$0和$1指的是闭包的第一个和第二个字符串参数。由于$1是数字最高的速记参数,因此闭包被理解为两个参数。由于这里的sorted(by:)函数期望一个参数都是字符串的闭包,因此速记参数$0和$1都是String类型。

运算符方法

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

reversedNames = names.sorted(by: >)