Swift4闭包
闭包是可以在你的代码中被传递和引用的功能性独立模块。Swift 中的闭包和 C 以及 Objective-C 中的 blocks 很像,还有其他语言中的lambada表达式也类似。
闭包能够捕获和存储定义在其上下文中的任何常量和变量的引用,这也就是所谓的闭合并包裹那些常量和变量,因此被称为“闭包”,Swift 能够为你处理所有关于捕获的内存管理的操作。
全局函数和内嵌函数,实际上是特殊的闭包。闭包符合如下三种形式中的一种:
-
全局函数是一个有名字但不会捕获任何值的闭包;
-
内嵌函数是一个有名字且能从其上层函数捕获值的闭包;
-
闭包表达式是一个轻量级语法所写的可以捕获其上下文中常量或变量值的没有名字的闭包。
Swift 的闭包表达式拥有简洁的风格,鼓励在常见场景中实现简洁,无累赘的语法。常见的优化包括:
-
利用上下文推断形式参数和返回值的类型;
-
单表达式的闭包可以隐式返回;
-
简写实际参数名;
-
尾随闭包语法。
闭包表达式
闭包表达式是一种在简短行内就能写完闭包的语法。闭包表达式为了缩减书写长度又不失易读明晰而提供了一系列的语法优化。下边的闭包表达式栗子通过使用几次迭代展示sorted(by:)
方法的精简来展示这些优化,每一次都让相同的功能性更加简明扼要。
Sorted 方法
Swift 的标准库提供了一个叫做 sorted(by:)
的方法,会根据你提供的排序闭包将已知类型的数组的值进行排序。一旦它排序完成,sorted(by:)
方法会返回与原数组类型大小完全相同的一个新数组,该数组的元素是已排序好的。原始数组不会被 sorted(by:)
方法修改。
下面这个闭包表达式的栗子使用 sorted(by:)
方法按字母排序顺序来排序一个 String
类型的数组。这是将被排序的初始数组:
let names = ["Chris","Alex","Ewa","Barry","Daniella"]
sorted(by:)
方法接收一个接收两个与数组内容相同类型的实际参数的闭包,然后返回一个 Bool
值来说明第一个值在排序后应该出现在第二个值的前边还是后边。如果第一个值应该出现在第二个值之前,排序闭包需要返回 true ,否则返回 false
这个栗子对一个 String 类型的数组进行排序,因此排序闭包需为一个(String, String) -> Bool
的类型函数。
提供排序闭包的一个方法是写一个符合其类型需求的普通函数,并将它作为 sorted(by:) 方法的形式参数传入:
使用函数指针类型
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames = ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
总之,这样来写本质上只是一个单一表达函数( a > b )是非常啰嗦的。在这个栗子中,我们更倾向于使用闭包表达式在行内写一个简单的闭包。
闭包表达式语法
{参数列表 -> 返回值 in
闭包代码
}
闭包的函数整体部分由关键字in
导入,这个关键字表示闭包的形式参数类型和返回类型定义已经完成,并且闭包的函数体即将开始
初始版本
var reversedNames=names.sorted(by:{(s1:String,s2:String)->Bool in return s1>s2})
从上下文推断类型
由于sort(by:)
函数需要一个(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});
简写实际参数名
Swift 自动对行内闭包提供简写实际参数名,你也可以通过$0
,$1
,$2
等名字来引用闭包的实际参数值。而不用使用像s1
,s2
这样的自定义形式参数名
var reversedNames=names.sorted(by:{$0>$1})
运算符方法
Swift 的String类型定义了关于比较大小的特定字符串实现,让其作为一个有两个String类型形式参数的函数并返回一个Bool类型的值。这正好与sorted(by:)
方法的第二个形式参数需要的函数相匹配。因此,你能简单地传递一个大于号,并且 Swift 将推断你想使用大于号特殊字符串函数实现:
var reversedNames=names.sorted(by:>)
以上完成了对闭包表达式的简写
使用尾随闭包写闭包表达式
如果你需要将一个很长的闭包表达式作为函数最后一个实际参数传递给函数,使用尾随闭包将增强函数的可读性。尾随闭包是一个被书写在函数形式参数的括号外面(后面)的闭包表达式。
这个非常类似于Objective-C中block作为参数的用法
//定义一个接受函数类型作为参数的函数
func someFunctionThatTakesAClosure(closure:() -> Void){
//函数体
}
//不使用尾随闭包的闭包表达式写法
someFunctionThatTakesAClosure({
//闭包的表达式写在这里,注意两种方法的大小括号位置
})
//使用尾随闭包的闭包表达式的写法
someFunctionThatTakesAClosure() {
// 尾随闭包的表达式写在这里
}
之前的sort(by:)
函数使用尾随闭包的写法
var reversedNames=names.sorted(){
$0>$1
}
因为只有一个参数,所以小括号也可以省略
var reversedNames=names.sorted{
$0>$1
}
当闭包体很长的时候,使用尾随闭包的优势就能显现出来了
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
}
捕获值
闭包能够从上下文中捕获已经被定义的常量和变量。即使定义这些常量和变量的原作用域已经不在了。
在Swift中,一个能够捕获值的闭包的最简单的模型就是内嵌函数
这里有个命名为 makeIncrement 的函数栗子,其中包含了一个名叫 incrementer 一个内嵌函数。
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
makeIncrementer()
返回了一个incrementer()
函数,该函数捕获了上层函数makeIncrementer()
的runningTotal
和amount
值。
观察该函数的incrementer()
可以发现,该函数是没有任何形式参数, runningTotal
和 amount
不是来自于函数体的内部,而是通过捕获主函数的值,把它们内嵌在自身函数内部供使用。当调用 makeIncrementer 结束时通过引用捕获来确保不会消失(也就是这些被捕获的值引用计数加一),并确保了在下次再次调用 incrementer
时, runningTotal
将继续增加。
举个例子:
let incrementByTen = makeIncrementer(forIncrement: 10)
这个例子定义了一个叫 incrementByTen 的常量,该常量指向一个每次调用会加 10 的函数。调用这个函数多次得到以下结果:
incrementByTen()
//返回值为10
incrementByTen()
//返回值为20
incrementByTen()
//返回值为30
逃逸闭包和非逃逸闭包
- 逃逸闭包
概念:一个接受闭包作为参数的函数,该闭包可能在函数返回后才被调用,也就是说这个闭包逃离了函数的作用域,这种闭包称为逃逸闭包。当你声明一个接受闭包作为形式参数的函数时,你可以在形式参数前写@escaping
来明确闭包是允许逃逸的。
场景举例:网络请求结束后才调用的闭包,因为发起请求后过了一段时间后这个闭包才执行,并不一定是在函数作用域内执行的。
func requestData()->{
CCNetWorkTool.shareInstance.request(method:.Post,URLString:API_TEST parameter:nil){}
}
- 非逃逸闭包
概念:一个接受闭包作为参数的函数, 闭包是在这个函数结束前内被调用
为什么要分逃逸闭包和非逃逸闭包
为了管理内存,闭包会强引用它捕获的所有对象,比如你在闭包中访问了当前控制器的属性、函数,编译器会要求你在闭包中显示 self 的引用(其实当前对象的属性、函数隐形带了一个self参数,一旦调用他们,隐形中就使用了self),这样闭包会持有当前对象,容易导致循环引用。
非逃逸闭包不会产生循环引用,它会在函数作用域内释放,编译器可以保证在函数结束时闭包会释放它捕获的所有对象;使用非逃逸闭包的另一个好处是编译器可以应用更多强有力的性能优化,例如,当明确了一个闭包的生命周期的话,就可以省去一些保留(retain)和释放(release)的调用;此外非逃逸闭包它的上下文的内存可以保存在栈上而不是堆上。
综上所述,如果没有特别需要,开发中使用非逃逸闭包是有利于内存优化的,所以苹果把闭包区分为两种,特殊情况时再使用逃逸闭包。
闭包可以逃逸的一种方法是被储存在定义于函数外的变量里。比如说,很多函数接收闭包实际参数来作为启动异步任务的回调。函数在启动任务后返回,但是闭包要直到任务完成——闭包需要逃逸,以便于稍后调用。举例来说:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
函数 someFunctionWithEscapingClosure(_:) 接收一个闭包作为实际参数并且添加它到声明在函数外部的数组里。如果你不标记函数的形式参数为 @escaping
,你就会遇到编译时错误。
相当于Objective-C中的__block
关键字。
让闭包@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"
自动闭包
自动闭包是一种自动创建的用来把作为实际参数传递给函数的表达式打包的闭包。它不接受任何实际参数,并且当它被调用时,它会返回内部打包的表达式的值。这个语法的好处在于通过写普通表达式代替显式闭包而使你省略包围函数形式参数的括号。
自动闭包允许你延迟处理,因此闭包内部的代码直到你调用它的时候才会运行。对于有副作用或者占用资源的代码来说很有用,因为它可以允许你控制代码何时才进行求值。下面的代码展示了闭包如何延迟求值。
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Prints "5"
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"
注意 customerProvider 的类型不是 String 而是 () -> String
——一个不接受实际参数并且返回一个字符串的函数。
当你传一个闭包作为实际参数到函数的时候,你会得到与延迟处理相同的行为。
// customersInLine = ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"
上边的函数serve(customer:)
接收一个明确的返回下一个客户名称的闭包。下边的另一个版本的serve(customer:)
执行相同的任务但是不使用明确的闭包而是通过@autoclosure
标志标记它的形式参数使用了自动闭包。现在你可以调用函数就像它接收了一个 String 实际参数而不是闭包。实际参数自动地转换为闭包,因为 customerProvider
形式参数的类型被标记为 @autoclosure
标记。
// customersInLine = ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0)) //注意传入的参数不是一个闭包,而是一条语句。
// Prints "Now serving Ewa!"
如果你想要自动闭包允许逃逸,就同时使用 @autoclosure 和 @escaping 标志。 @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!"