Swift文档翻译计划 -- 闭包

·  阅读 878

闭包

Swift 中的闭包类似 Objective-C 中的 Block,也类似于其他编程语言中的 lambdas,闭包可以从定义它们的上下文中捕获和存储对任何常量和变量的引用。

闭包有三种形式:

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

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

{ (parameters) -> return type in
    statements
}
复制代码

分别为参数、返回值类型和闭包主体。以 Swift 的 sorted(by:) 数组排序为例:

let names = ["Chris""Alex""Ewa""Barry""Daniella"]
复制代码

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

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
复制代码

但是,这是一种相当冗长的方式来编写本质上是一个单一表达式的函数(a > b),在这个例子中,最好使用闭包表达式语法来编写内联排序闭包,改为闭包后如下所示:

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

相比于上面的写法,内联闭包表达式的参数和返回值类型都写在了花括号内。闭包主体的开始由 in 关键字介绍。这个关键字表示闭包的参数和返回类型的定义已经完成,闭包的主体即将开始,这个格式也符合前面对闭包的定义。从这个例子可以看出,对 sorted(by:) 方法的总体调用保持不变。括号内仍然是方法的整个参数。但是,这个参数现在是一个内联闭包,而之前是一个函数。实际上函数定义时参数仍然是一个函数类型,但在调用时,可以将函数类型的参数使用闭包的形式来传递。

Swift 中的闭包拥有很多特性,下面一一列举。

从上下文推断类型

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

reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
复制代码

当将闭包作为参数的形式传递给方法时,Swift 可以推断出参数类型和返回值类型。因此,不需要写完整。

隐式返回

隐式返回可以在声明中省略return关键字:

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

因为闭包的主体需要返回一个 Bool 值的表达式 (s1 > s2),所以这个闭包必须要有一个返回值,return 关键字可以省略。

简化参数名称

Swift 自动为内联闭包提供简写的参数名称,可以使用这些名称 $0$1$2 等来引用闭包的参数值,如果在闭包中使用了简写的参数名,可以从其定义中省略闭包的参数,而简写的参数名的数量和类型将从预期的函数类型中推断出来。in 关键字也可以省略,因为闭包表达式完全由它的主体组成:

reversedNames = names.sorted(by: { $0 > $1 } )
复制代码

其中 $0$1 指闭包的第一个和第二个字符串参数。

尾随闭包

如果需要将一个闭包作为函数的最后参数传递,并且闭包很长,那么可以将它写成一个尾随闭包。尾随闭包仍然是作为函数的参数,只不过是在函数调用的圆括号之后编写一个结尾闭包,而不是像上面一样在圆括号内。当闭包足够长以至于不能将其内联写入一行时,尾部闭包最有用。一个函数调用可以有多个尾随闭包,但在函数调用时第一个闭包的参数标签会被省略。

定义一个函数,其中 closure 为一个无参的 void 类型函数:

func someFunctionThatTakesAClosure(closure: () -> Void) {
    // function body goes here
}
//不使用尾随闭包调用:
someFunctionThatTakesAClosure(closure: {
    // closure's body goes here
})

//使用尾随闭包调用:
someFunctionThatTakesAClosure() {
    // trailing closure's body goes here
}
复制代码

这样的话上面字符串排序的例子闭包可以这样写:

reversedNames = names.sorted() { $0 > $1 }
复制代码

如果一个闭包表达式作为函数或方法的唯一参数,那么函数结尾的一对括号 () 可以省略:

reversedNames = names.sorted { $0 > $1 }
复制代码

如果一个函数接受多个闭包,可以省略第一个尾随闭包的参数标签,只标记后面的尾随闭包。例如下面的函数为图片库加载一张图片:

func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
    if let picture = download("photo.jpg", from: server) {
        completion(picture)
    } else {
        onFailure()
    }
}
复制代码

在调用的时候可以省略第一个闭包的 completion 标签:

loadPicture(from: someServer) { picture in
    someView.currentPicture = picture
} onFailure: {
    print("Couldn't download the next picture.")
}
复制代码

值的捕获

OC 的 Block 可以捕获局部变量的值,在 Swift 中也同理,但它比 Block 更强大,闭包可以从定义它的周围上下文中捕获常量和变量,还可以在其主体内引用和修改这些常量和变量的值,不需要使用 __block 等修饰,即使定义这些常量和变量的原始范围不再存在也可以。

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

下面是一个名为 makeIncrementer 的函数示例,它包含一个名为 incrementer 的嵌套函数:

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}
复制代码

可以看到 incrementer 没有任何参数,但他捕获了 amount 和 runningTotal 的引用,通过引用捕获确保它们不会在 makeIncrementer 的调用结束时消失,还确保runningTotal 在下一次调用 incrementer 函数时可用。Swift 内部实际上是存储一个它们值的副本,当不再需要变量时,Swift 内部处理了所有涉及变量的内存管理。

下面是调用 makeIncrementer 的示例以及打印结果:

let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30
复制代码

逃逸闭包

Swift 的闭包专业名词是真的多,啥是逃逸闭包,满足两个条件的就是:一个是作为函数的参数传递,二是在函数返回之后再被调用。文档的描述有点晦涩,对于逃逸我理解因为这个闭包逃离了这个函数的作用域,在 OC 中类似于网络请求的 Block 回调,逃逸闭包需要在类型前加上 @escaping 来声明这个闭包允许逃逸。

var completionHandlers = [() -> Void]()
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}
复制代码

这是文档的例子,@escaping 声明了 completionHandler 是一个逃逸闭包,因为这个闭包没有在函数结束前调用,而是存入了 completionHandlers,所以必须加上 @escaping 关键字,否则编译器会报错。

在前面值的捕获例子中,常规的闭包捕获变量不需要显式声明 self,但是逃逸闭包必须要显式声明 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"
复制代码

这样做的目的是提醒我们确认 self 和闭包之间有没有产生循环引用。

自动闭包

自动闭包根据文档描述它是作为参数自动创建传递给函数,属于闭包的一个简写,通过在参数类型前添加关键字 @autoclosure 让函数调用时可以省略闭包周围的大括号,看起来就像普通表达式一样。 自动闭包和普通闭包的区别如下:

// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"

// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"
复制代码

自动闭包看起来更精简,customer 参数更像是一个字符串传递,而不是传递了一个带有花括号包装的闭包。注意不要过度使用自动闭包,否则会使代码难以理解,使用的话应该在上下文和函数名清楚地表明这是个自动闭包,它会延后执行,而不是立即执行。

自动闭包可以和逃逸闭包结合使用,在参数类型前加 @autoclosure @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 customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"
复制代码
分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改