[翻译]Swift编程语言——闭包

来源:互联网 发布:知乎短小精悍回复 编辑:程序博客网 时间:2024/06/05 17:10

闭包

闭包是自己自足的功能代码块,能被传递和使用。Swift的闭包和 C语言和OC中的blocks、其他语言中的lambdas 类似。
在闭包的定义上下文环境中,闭包可以捕获任意的常量或者变量。 This is known as closing over those constants and variables, hence the name “closures”. Swift将全部的内存捕捉的句柄都提供给了你。
NOTE
不要担心不理解捕捉这个概念,后续有专门的capturing Values 章节会详细介绍。
全局和嵌套函数其实是闭包的特殊情况。闭包以下面三种之一的面目出现:
1全局函数是拥有名字不需要捕获任何值的闭包。
2嵌套函数是拥有名字可以捕获所在函数范围内值的闭包。
3闭包表达式是没有名字语法轻量化书写、可以捕获其所在环境上下文内的值的闭包。
Swift的闭包表达式具有干净清晰的风格,with optimizations that encourage brief, clutter-free syntax in common scenarios.有点有:
1根据上下文推断参数和返回值的类型。
2省略单表达式闭包(single-expression closures)的返回值
3简写参数名
4追踪(Trailing )闭包语法

闭包表达式

前面介绍的嵌套函数是在一个大函数中命名和定义的函数。然而有时需要一个简化的版本。这在函数作为参数的时候很必要。
闭包表达式(closure expression)是简单集中的书写内联闭包的一种方式。闭包表达式提供了若干语法优化定义闭包,简约而明晰。
下面会用sorted函数的几个不同实现来说明这些优点。

Sorted 函数

Swif标准库提供了一个叫做sorted的函数,它用来对已知类型的数组进行排序,实现过程是通过传入的排序闭包。当完成排序操作后,storted函数返回一个和原来数组长度、类型一致的新数组,新数组中的元素已经是经过排序后的了。原始的数组不会被sorted函数修改。
下面闭包表达式的例子,使用sorted函数对一个String类型的数组进行字母倒排序。这里是原始的数组:

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

sorted函数带了两个参数:
1一个已知类型的数组。
2一个闭包:带两个参数(这两个参数和数组内容的类型一致),返回一个布尔值(这个布尔值的意思是在排序时,第一个参数是排在第二个参数的之前还是之后)。进行排序时第一个参数要排在第二个参数之前,闭包返回true,否则返回false。
因为上面的数组存储的是String类型的内容,所以排序闭包需要是这样一个函数类型: (String, String) -> Bool。
一种提供闭包的反噬是书写一个正常的对应类型函数,然后将这个函数作为sorted函数的第二个参数传递:

func​ ​backwards​(​s1​: ​String​, ​s2​: ​String​) -> ​Bool​ {​ ​return​ ​s1​ > ​s2​}​var​ ​reversed​ = ​sorted​(​names​, ​backwards​)​// reversed is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

如果第一个字符(s1)比第二个字符串(s2)大,backwards函数返回true,表明在排序后的数组中s1将会出现在s2之前。对于字符串中的字符而言,“大”意味着在字母表中的顺序靠后。这意味着字母B要比字母A大,字符串Tom要比字符串Tim大。这里需要按照字母表倒排序,所以Barry应该在Alex之前。其他的字符串比较也是一样的。
然而这是一个冗长的写法,实际上它就是一个单一表达式函数(a > b).这个例子中,这种写法可以按照闭包表达式语法,写一个简短的内联闭包。

闭包表达式语法

闭包表达式语法通常是这样的:

{ (parameters) -> return type in    statements}

闭包表达式语法可以采用常量参数、变量参数、和inout参数。参数的默认值是不能使用的。可变参数可以使用,前提是你给它加了名字而且将其放在参数列表最后。元组当然也可以作为闭包的参数和返回值。
下面是闭包表达式版本的backwards函数:

reversed​ = ​sorted​(​names​, { (​s1​: ​String​, ​s2​: ​String​) -> ​Bool​ ​in​ ​return​ ​s1​ > ​s2​})

这里内联闭包的参数和返回类型定义,同backwards的定义一样。这两种情下,他们的类型都是 (s1: String, s2: String) -> Bool。然而内联闭包表达式中,参数和返回类型在花括号中被定义,而不是在外部。
注意这里的闭包体使用in关键字打头。这个关键字意思是闭包的参数和返回类型定义结束了,闭包体内容要开始了。
因为闭包体是如此的简短,所以它可以被写在一行内:

reversed​ = ​sorted​(​names​, { (​s1​: ​String​, ​s2​: ​String​) -> ​Bool​ ​in​ ​return​ ​s1​ > ​s2​ } )

这说明,sorted函数的整体调用没有变化。一对参数被包裹在作为整体的参数列表。只不过其中一个参数是一个内联闭包。

从上下文中推断类型

因为排序闭包作为参数使用,所以Swift可以根据sorted函数的第二个参数的类型推测出它的参数和返回值类型。第二个参数的类型是(String, String) -> Bool。这就意味着,(String, String)和 Bool 的类型声明在闭包表达式中可以不出现。因为所有的类型都能推测出来,所以->和包围参数的圆括号都可以省略:

​reversed​ = ​sorted​(​names​, { ​s1​, ​s2​ ​in​ ​return​ ​s1​ > ​s2​ } )

传递一个闭包给一个函数作为内联闭包时,一定能够推断出闭包的所有类型。这样,内联闭包就根本不必要采用完整的写法。
尽管这样,如果你愿意,你仍可以根据你的意愿保留明确的类型说明。如果为了避免给你的代码读者造成困扰这样做是值得的。sorted函数的例子中,使用闭包的目的是(比不使用)更加清晰明了排序的实现,对于代码的的读者而言,假设闭包处理的是String类型的值这种行为是安全的,因为它是协助一个Stirng类型的数组进行排序。

单一表达式闭包省略return

上面的例子中,单一表达式闭包定义时可以省略去写return关键字:
​reversed​ = ​sorted​(​names​, { ​s1​, ​s2​ ​in​ ​s1​ > ​s2​ } )

参数名的简写

对于内联闭包,Swift提供了简写参数名的写法:使用0,1,$2……这样的名称指代闭包的参数。
如果你使用了简写的参数名,那么你可以连参数列表定义也省了,参数的个数和类型都可以被推测出来。in关键字也被省去了,因为闭包体被留下了:

​reversed​ = ​sorted​(​names​, { ​$0​ > ​$1​ } )

这里01指代的是第一个和第二个String类型的参数。

操作符函数

上例事实上还有一种更简练的写法。Swift的字符串类型支持用>作为一个函数表示大于,返回一个布尔值。这正好符合sorted函数的第二个参数的要求。因此你可以传递大于符号,Swift会推断出你的意图:

​reversed​ = ​sorted​(​names​, >)

更多的操作符做函数的信息,参见相关章节。

Trailing 闭包

如果你想要传递一个闭包表达式给函数的最后一个参数,同时这个闭包又非常长,这时你可以写一个Trailing 闭包作为替代。一个Trailing 闭包是被写在函数圆括号之外(或之后)的闭包表达式:

func​ ​someFunctionThatTakesAClosure​(​closure​: () -> ()) {​ ​// function body goes here​}​​// here's how you call this function without using a trailing closure:​​someFunctionThatTakesAClosure​({​ ​// closure's body goes here​})​​// here's how you call this function with a trailing closure instead:​​someFunctionThatTakesAClosure​() {​ ​// trailing closure's body goes here​}

NOTE
如果一个闭包表达式作为一个函数的唯一参数同时这个闭包表达式又是一个Trailing 闭包,这种情况下调用函数时你不需要在函数名字后写一对圆括号了。
上面例子使用Trailing 闭包的写法如下:

​reversed​ = ​sorted​(​names​) { ​$0​ > ​$1​ }

当闭包足够长,不能在一行写完时,Trailing 闭包是非常有用的。有一个例子,Swift的数组类型有一个叫做map的方法,它接受一个闭包作为唯一的参数。对于数组中的每一个元素,都会调用闭包一次,闭包的返回该元素的映射值(或者其他什么的)。具体的映射方式和返回类型由闭包指定。
在对每个数组中的元素应用闭包后,map函数返回一个包括了每个元素对应映射的新数组,两个数组的对应顺序一致。
这里有个例子你可以采用一个trailing闭包使用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数组,通过传递一个闭包表达式(trailing闭包)给数组的map方法。记得在调用number.map时,不需要使用圆括号,因为map函数只有一个参数,这个参数是一个trainling闭包:

let​ ​strings​ = ​numbers​.​map​ {​ (​var​ ​number​) -> ​String​ ​in​ ​var​ ​output​ = ​""​ ​while​ ​number​ > ​0​ {​ ​output​ = ​digitNames​[​number​ % ​10​]! + ​output​ ​number​ /= ​10​ }​ ​return​ ​output​}​// strings is inferred to be of type [String]​// its value is ["OneSix", "FiveEight", "FiveOneZero"]

map函数对数组中的每个元素都调用了闭包表达式。你不必指定闭包的参数number的类型,因为它的类型可以从数组的内容类型中推断得到。
这个例子中,闭包的number参数被定义为了一个可修改参数,所以number的值可以在闭包体内被修改,这样就不比在定义一个新的变量来接受number的值了。闭包表达式同样定义了返回的类型是String,意味着保存在映射操作过后存储到新数组中的数据类型是String。
闭包表达式在它被调用时构造了了一个叫做ouput的字符串。接下来会对number取它每一位上数字,在根据这个数字去字典中找对应的英文名字。这个闭包可以将任何一个大于0的正数转成对应的英文。
NOTE
使用下表i访问字典digitNames时,后面跟了一个叹号。因为字典下标在找不到key对应的内容时返回一个可选值。上面的例子中可以确保number % 10做下标的时候字典都有值返回,所以使用叹号表示要强制解包这个可选值。
从digitName字典中渠道的字符串被添加到output之前,从而构造出了对应数字的字符串。(表达式number%10根据16得到6,根据58得到8,根据510得到0)
number变量被处以10,因为是整形,所以舍入后16变成了1,58变成了5,510变成了51.
上面的处理过程知道number/=10 等于0,那时output会被闭包返回,被存储在map函数返回的结果数组中。
上面使用trailing闭包的写法,在函数之后接着就完成了闭包的功能,没有将闭包包裹在函数的圆括号之中,更加整洁。

捕获值

闭包可以在他的定义上下文环境中捕获常量或者变量。闭包体内可以引用和修改这些值,尽管这些常量和变量的原来定义作用域已经不复存在了。
Swift中,最简单可以捕获值的闭包形式就是嵌套函数了,嵌套函数只可以捕获它外层的函数的参数,同时可以捕获外层函数体内的定义的常量和变量。

这里有个例子一个叫做makeIncrementor的函数,它里面含有一个嵌套函数叫做incrementor。incrementor函数从它的环境中捕获了两个值,runningTotal 和amount。捕获了这些值后,makeIncrementor 返回incrementor作为闭包(每次调用会给runningTotal加上amount)

func​ ​makeIncrementor​(​forIncrement​ ​amount​: ​Int​) -> () -> ​Int​ {​ ​var​ ​runningTotal​ = ​0​ ​func​ ​incrementor​() -> ​Int​ {​ ​runningTotal​ += ​amount​ ​return​ ​runningTotal​ }​ ​return​ ​incrementor​}

makeIncrementor 的返回类型是() -> Int。这意味着它返回一个函数而不是一个简单的值。返回的函数没有返回值,每次被调用时会返回一个 Int值。
makeIncrementor 函数定义了一个叫做runningToatl的整型变量,来存储当前增加 到了多少,并返回该值。这个变量初始化的时候被赋值0。
makeIncrementor 函数只有一个 参数,它的外部名字叫做forIncrement,它的本地名叫做amount。这个参数告诉incrementor函数每次调用时要给runningTotal的值加多少。
makeIncrementor 定义 了一个嵌套函数叫做incrementor,incrementor才实际上是做添加的操作。incrementor函数向runningTotal添加amount,并且返回runningTotal。
单独看这个嵌套函数incrementor,有些不寻常:

func​ ​incrementor​() -> ​Int​ {​ ​runningTotal​ += ​amount​ ​return​ ​runningTotal​}

incrementor 函数没有任何参数,但是却可以在其函数体内使用runningTotal 和amount 。这是因为它有从它外部函数中捕获上面两个参数的技能。
因为incrementor 没有修改amount, incrementor实际上是存储了一份amount的副本。这个值随同incrementor函数被存储。
然而,因为incrementor在每次被调用时都修改了runningTotal的值,所以incrementor 捕获了当前runningTotal 变量的引用而不是它初始值的副本。捕获引用使得runningTotal 不会在makeIncrementor 函数被调用完毕后就消失,使得闭包在下次被调用时runningTotal 仍然有效。
NOTE
Swift决定到底是 捕获引用还是捕获值的副本。你不必给amount或者runningTotal添加额外的说明,来表述他们在闭包内会被如何使用。Swift会管理runningTotal的内存占用,当不再被嵌套函数使用时,将会被清除。
下面是一个调用makIncrementor的例子:

let​ ​incrementByTen​ = ​makeIncrementor​(​forIncrement​: ​10​)

上面给一个叫做incrementByTen的常量赋值一个这样函数(每次调用给runningTotal添加10)。调用它几次后的表现:

incrementByTen​()​// returns a value of 10​incrementByTen​()​// returns a value of 20​incrementByTen​()​// returns a value of 30

如果你创建第二个增加函数,它将有自己的对引用的存储,runningTotal的引用和第一个是不同的:

let​ ​incrementBySeven​ = ​makeIncrementor​(​forIncrement​: ​7​)​incrementBySeven​()​// returns a value of 7

再次调用第一个增加函数(incrementByTen),它仍会在自身的runningTotal引用基础上增加,不受incrementBySeven被调用的影响:

​incrementByTen​()​// returns a value of 40

NOTE
如果你给一个类实例的属性赋予了一个闭包,同时那个闭包捕获类实例(通过引用类实例或者类实例的成员),那么就你在类实例和闭包之间创建了了一个强引用循环。Swift使用捕获列表破除这种强引用循环。详见:Strong Reference Cycles for Closures

闭包是引用类型

上面例子中incrementBySeven 和incrementByTen 是常量,但是这些常量可以对捕获到的runningTotal变量进行累加。这是因为函数和闭包是引用类型的。
不管你将一个函数 还是闭包赋值给一个常量或变量,你其实做的是将函数/闭包的引用赋值给了常量/变量。上面的例子中,是将incrementByTen闭包指向了那个常量,而不是闭包自身内容。
这同时也意味着如果你将一个闭包赋值给两个不同的变量/常量,这两个变量/常量指向的是一个闭包:

​let​ ​alsoIncrementByTen​ = ​incrementByTen​alsoIncrementByTen​()​// returns a value of 50
0 0