距离上次更新博客已经过去了整整十个月,最近打算把更新博客这个习惯捡起来。这篇博客主要是翻译Swift中介绍闭包的官方文档Swift Closures
闭包
swift中的闭包和objective-c中的block相似,闭包可以捕获和存储对常量和变量的引用。
全局和嵌套函数是闭包的特殊形式,闭包有以下三种形式:
- 1.全局函数是闭包,它有名字且不捕获任何常量和变量。
- 2.嵌套函数是闭包,它有名字并且可以从封闭函数内捕获值。
- 3.闭包表达式是一种轻量级语法的没有名字的闭包,它能从上下文中捕获值。
swift中的闭包表达式有非常简洁干净的形式,在形式上鼓励如下优化:
- 1.从上下文中推断参数和返回值类型。
- 2.对于只有一个表达式的闭包,可以隐藏返回值。
- 3.可以简写参数名。
- 4.尾随闭包语法。
闭包表达式
嵌套函数是在一个大的函数中命名和定义一个代码块的简便方式,但是有时候一个没有完整的名字和申明的类函数结构也很有用,当我们的方法或函数中将函数作为参数时,这种结构就很有用。
闭包表达式在不损失语义的情况下提供了几种简写闭包的语法优化。下面将通过sorted(by:)
这个方法介绍闭包表达式中的语法优化。
sorted(by:)
swift标准库中提供了一个方法sorted(by:)
,该方法按照开发者提供的排序闭包的输出对数组中的值进行排序。排序工作完成后,该方法返回一个与之前数组同样大小的正确排序的数组,原数组则没有被修改。
下面的闭包表达式例子将使用sorted(by:)
方法将下面数组中的字符串按照字母逆序进行排序:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
sorted(by:)
方法接受这样一个闭包作为其参数:有两个与数组元素类型相同的参数,返回一个布尔值来说明在排好序的数组中第一个参数值是否应该出现在第二个参数值之前。如果第一个参数值应该出现在第二个参数值之前,则该排序闭包应该返回true
。否则返回false
。
上面的数组names
中元素类型是String
,应该排序闭包应该是一个函数该类型是(String, String) -> Bool
。
一种提供排序闭包的方法是写一个符合(String, String) -> Bool
的普通函数,然后将其作为参数传递给sorted(by:)
方法:
func backward(s1: String, s2: String) -> Bool {
return s1 > s2
}
var reversedNames = names.sorted(by: backward)
如果第一个字符串s1
比第二个字符串s2
大,则函数backward(_:_:)
返回true
,否则返回false
。但是我们很容易发现这种写法其实非常冗余,我们只是为了写一个简单的表达式(s1 > s2)
,在以上例子中使用闭包表达式语法将会更加合适。
闭包表达式语法
闭包表达式的通用形式如下:
{ (parameters) -> (return type) in
statements
}
闭包表达式中的参数可以是in-out
参数,但是不能带有默认值,可以使用可变参数作为其参数,元组也可以作为参数类型或返回值类型。
下面的例子是使用闭包表达式版本的backward(_:_:)
函数:
var reversedNames = names.sorted(by: {(s1: String, s2: String) -> Bool in
return s1 > s2})
以上说明对方法sorted(by:)
的调用还是一样的,一对括号包裹着方法的参数,只是现在参数吧变成了一个内联闭包。
从上下文中推断类型
由于排序闭包被当做了参数传入了方法中,因此swift不难推断出闭包的参数类型和返回值类型。sorted(by):
方法是被字符串类型的数组调用,因此该方法的参数类型一定是(String, String) -> Bool
。由于参数类型和返回值类型都可从上下文中推断出,因此在闭包表达式中可以省略(String, String)
和返回值类型Bool
,由于所有参数都可以推断出,因此返回的箭头->
和括号()
也都可以省略:
var reversedNames = names.sorted(by: {s1, s2 in
return s1 > s2})
当我们把一个闭包作为内联函数表达式传递给方法或函数时,该闭包的参数和返回值类型往往是可以推断出来的。因此当一个内联闭包用做方法或函数的参数时,我们永远不需要写出该闭包的完整形式。
尽管如此,如果你想清楚的表达参数或返回值的类型,或者让其他的更容易阅读你的代码,你仍然可以写出闭包的完整表达形式。
隐藏只有一个表达式的闭包的返回值
单表达式闭包可以通过省略return
关键字隐藏闭包的返回表达式,以下是隐藏闭包返回表达式的表达方式:
var reversedNames = names.sorted(by: {s1, s2 in s1 > s2})
这里sorted(by:)
方法中的参数类型使得我们明确知道闭包表达式中必须返回一个Bool
类型,由于闭包表达式中包含一个返回Bool
类型的单表达式(s1 > s2)
,因此不会产生模棱两可的情况,return
关键字可省略。
简化参数名称
在内联闭包中swift自动提供了简化的参数名称,我们可以使用$0
,$1
,$1
等来引用闭包中的参数值。
如果你在自己的闭包表达式中使用简写的参数名,那么你就可以在定义中省略闭包的参数列表,简写的参数的数量和类型会从闭包类型中推断出来。这里in
关键字也可以省略:
var reversedNames = names.sorted(by: { $0 > $1})
上面的代码中,$0
,$1
分别引用了闭包中第一个和第二个String
类型的参数。
运算符方法
上面的闭包表达式方法还可以继续简化。swift的String
类型重载了大于运算符>
,将其实现为需要两个String
类型参数,并返回Bool
类型的方法,这完全符合sorted(by:)
方法中对参数类型的要求。因此我们可以简单的传入一个大于运算符>
:
var 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
}
上面的排序闭包可以使用尾随闭包的语法将闭包表达式写在sorted(by:)
方法的括号外面:
reversedNames = names.sorted() { $0 > $1 }
如果一个闭包表达式是一个方法或函数的唯一一个参数,并且我们使用尾随闭包语法,则当我们调用函数时不需要写方法或函数后面的括号:
reversedNames = names.sorted { $0 > $1 }
当闭包表达式非常长时尾随闭包语法将会非常有用,例如swift中的Array
类型有一个方法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]
上面的代码创建了一个数组和英文之间映射的字典,并创建了一个整数类型的数组,该数组将被转化为字符串类型的数组。
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
}
numbers
数组中的每个元素会调用一次闭包表达式,这里我们不需要指定闭包吧表达式中参数number
的类型,因为这个类型可以推断出来一定是Int
型。
在上面的例子中变量number
是通过闭包参数number
进行初始化赋值的,因此其值在闭包内部可以被修改,闭包指定了返回类型为String
,指明了输出数组中的元素类型。
每次闭包表达式被调用时会创建一个名为output
的数组,闭包表达式会通过取余的方式计算number
的最后一个数,然后去字典中寻找这个数对应的值。
上面的例子中使用的尾随闭包语法整洁的将闭包封装在紧随方法之后,不需要将整个闭包包含在map(_:)
方法的括号中。
捕获值
闭包可以从常量或变量定义的上下文环境中捕获其值,然后可以在闭包内部引用或修改该常量或变量的值,即使定义常量或变量的范围已经不再存在。
在swift中能够捕获常量和变量的值得最简单的闭包形式是内嵌函数,内嵌函数是写在其他函数的函数体中的函数。一个内嵌函数可以捕获外部函数的参数值,还可以捕获外部函数体中定义的常量和变量。
下面有一个函数名为makeIncrementer
,它包含一个嵌套函数名为incrementer
,incrementer()
嵌套函数捕获了两个变量,unningtotal
和mount
。捕获了这些值之后,incrementer
被makeIncrementer
作为闭包返回了:
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
makeIncrementer
方法的返回值类型是() -> Int
,说明其返回的是一个函数,而不是一个值。返回的这个函数没有参数,返回值是Int
类型。
makeIncrementer(forIncrement:)
方法定义了一个整型变量runningTotal
来存储增加器Incrementer
的总运行次数,这个变量被初始化为0。
makeIncrementer(forIncrement:)
方法仅有一个Int
类型的参数,该参数的标签为forIncrement
,参数名为amount
。传给这个参数的参数值定义了返回的闭包每次被调用时runningTotal
应该增加多少。makeIncrementer
函数定义了一个嵌套函数名为incrementer
,这个嵌套函数会执行真正的增加操作,它简单的对runningTotal
增加amount
,最后返回runningTotal
作为结果。
当我们将incrementer()
方法单独拿出来看时会显得非同寻常:
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
incrementer()
函数没有任何参数,但是它引用了嵌套它的函数内部的runningTotal
和amount
两个变量,并在自己的函数体内使用它们。捕获runningTotal
和amount
这两个变量的引用确保了当makeIncrementer()
调用结束时这两个变量不会消失,同时确保了下次调用incrementer()
时这两个变量依然可用。
作为一种优化策略,当一个值不会被闭包改变且在闭包创建后该值不会改变,那么swift会捕获和存储该值的拷贝。
下面是使用makeIncrementer
的例子:
let incrementByTen = makeIncrementer(forIncrement: 10)
上面的例子中创建了一个常量,并让这个常量引用makeIncrementer
的返回值,其返回值是函数类型,因此incrementByTen
也是函数类型,多次调用该函数:
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30
引用类型的闭包
在上面的例子中incrementByTen
是常量,但是这个常量引用的闭包依然可以修改其所捕获的变量runningTotal
,这是因为函数和闭包都是引用类型。
任何时候当我们把一个函数或闭包赋值给一个常量或变量时,我们实际上是在内存中分配了一块地址,这个内存地址中是存放着一块内存地址,也就是该内存是一个指针,指针指向函数或闭包。
以上事实说明当我们将一个闭包赋值给多个常量或变量时,这些常量或变量实际指向的是同一个闭包。
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50
incrementByTen()
// returns a value of 60
上面的例子说明我们调用alsoIncrementByTen
和调用incrementByTen
实际是调用的同一个闭包,因为它们都是引用的同一个闭包。
逃逸闭包
当闭包作为函数参数但是是在函数返回之后调用的,被称为从函数逃逸。当我们申明了一个函数其中有一个参数是闭包类型时,我们可以在该参数类型前面加上@escape
关键字来说明允许闭包逃逸。
闭包可以逃逸的一种方式是闭包被定义在函数的外面,例如许多执行异步操作的函数以闭包作为参数成为异步操作完成的代码块,这时函数在任务一开始时就返回了,但是在任务完成前闭包都没有被调用——闭包需要逃逸,此后被调用,下面是例子:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
someFunctionWithEscapingClosure(_:)
方法把一个闭包作为其参数,并把这个闭包加入到函数外面申明的一个数组中,如果我们不用@escaping
关键字,将产生编译时错误。
将一个闭包标志位@escaping
意味着我们需要在闭包内显式引用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"
以上翻译比较随意,如有误处,欢迎指出。