闭包
闭包是自包含的函数代码块,可以在代码中被传递和使用
Swift
中的闭包与C
和Objective-C
中的代码块(blocks
)以及其他一些编程语言中的匿名函数比较相似
闭包可以捕获和存储其所在上下文中任意常量和变量的引用。被称为包裹常量和变量。 Swift 会为你管理在捕获过程中涉及到的所有内存操作。
在函数章节中介绍的全局和嵌套函数实际上也是特殊的闭包
闭包的形式:
- 全局函数是一个有名字但不会捕获任何值的闭包
- 嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包
- 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包
闭包可以进行一定语法优化,常见优化:
- 利用上下文推断参数和返回值类型
- 隐式返回
单表达式
闭包,即单表达式闭包可以省略return
关键字 - 参数名称缩写
- 尾随闭包语法
闭包表达式
闭包表达式是一种利用简洁语法构建内联闭包的方式。
sorted方法
Swift
的sorted(by:)
方法根据提供的排序的闭包函数.将数组排序后返回一个同样大小排序后的数组,原数组不变
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
对于数组这个用于排序的闭包函数类型为(String, String) -> Bool
通过返回bool值决定第一个参数在前还是在后
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames 为 ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
为了写return s1 > s2
这么简单的表达式却需要写函数太过繁琐,我们可以使用闭包表达式语法
闭包表达式语法
{ (parameters) -> returnType in
statements
}
闭包表达式参数 可以是in-out
参数,但不能设定默认值。可以使用可变参数但是要放到参数最后
闭包的函数体部分由关键字in
引入。该关键字表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。(in
前为闭包定义,后为闭包函数体)
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
根据上下文推断类型
因为这个闭包函数的作为方法参数传入,因此其类型已经确定为(String, String) -> Bool
,因此我们可以省略这些类型声明,因为可以被正确推断出来
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
//我们将闭包作为函数或方法的参数时,其类型总是可以推断出来的
//推荐完整格式闭包,增加代码可读性
单表达式闭包隐式返回
如果闭包内只有一行单一表达式,并且明确了需要的返回类型,可以省略return
关键字
//
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
参数名称缩写
Swift
自动为内联闭包提供了参数名称缩写功能,你可以直接通过$0
,$1
,$2
来顺序调用闭包的参数
如果使用参数名称缩写,那么在闭包定义中也可以省略参数列表,对应的参数名称缩写的类型也可以推断出来,这样的话in
关键字也可以省略
//此时闭包表达式只剩下闭包函数体
reversedNames = names.sorted(by: { $0 > $1 } )
注意
只有在必要中
运算符方法
Swift
的String
类型定义了关于大于号( > )
的字符串实现,其作为一个函数接受两个String
类型的参数并返回Bool
类型的值。而这正好与sorted(by:)
方法的参数需要的函数类型相符合。因此,你可以简单地传递一个大于号,Swift 可以自动推断出你想使用大于号的字符串函数实现:
reversedNames = names.sorted(by: >)
尾随闭包
如果要将一个很长的闭包作为函数最后一个参数,可以使用尾随闭包
的方式来调用函数
尾随闭包是书写在函数圆括号之后的闭包表达式,使用尾随闭包可以不用写出它的参数标签
func someFunctionThatTakesAClosure(closure: () -> Void) {
// 函数体部分
}
// 以下是不使用尾随闭包进行函数调用
someFunctionThatTakesAClosure(closure: {
// 闭包主体部分
})
// 以下是使用尾随闭包进行函数调用
someFunctionThatTakesAClosure() {
// 闭包主体部分
}
reversedNames = names.sorted() { $0 > $1 }
//如果闭包函数是函数的唯一参数我们还可以省略()
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
}
// strings 常量被推断为字符串类型数组,即 [String]
// 其值为 ["OneSix", "FiveEight", "FiveOneZero"]
值捕获
闭包可以在被定义的上下文捕获常量或变量,即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值
最简单的捕获值的闭包形式是嵌套函数
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
makeIncrementer
的返回类型是() -> Int
说明要返回一个函数
对于嵌套函数incrementer()
并没有参数,而是捕获了runningTotal
和amount
变量的引用.捕获引用保证了runningTotal
和amount
变量在调用完makeIncrementer
后不会消失,并且保证了在下一次执行incrementer
函数时,runningTotal
依旧存在。
注意
为了优化,如果一个值不会被闭包改变,或者在闭包创建后不会改变,Swift 可能会改为捕获并保存一份对值的拷贝。
Swift 也会负责被捕获变量的所有内存管理工作,包括释放不再需要的变量。
let incrementByTen = makeIncrementor(forIncrement: 10)
incrementByTen()// 返回的值为10
incrementByTen()// 返回的值为20
incrementByTen()// 返回的值为30
这个常量函数每次调用都会将runningTotal
变量增加10
闭包是引用类型
函数
和闭包
都是引用类型,因此我们设置的常量或者变量是设置的对应函数或闭包的引用,而不是闭包内容本身
逃逸闭包
将闭包作为参数传递到另一个函数中,但是这个闭包在函数返回后才执行,则这个闭包从函数中逃逸
,我们可以通过在函数名之前标注@escaping
表示这个闭包允许逃逸
一种能使闭包“逃逸”出函数的方法是,将这个闭包保存在一个函数外部定义的变量中。例如:逃逸闭包我们常用于在一个函数需要异步执行操作,通常会让函数返回,在异步操作完成后再调用执行这个闭包,因为闭包需要在函数返回之后被调用,所以需要逃逸闭包
逃逸闭包必须将函数参数标记为@escaping
,否则会得到一个编译错误
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
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)
// 打印出 "200"
completionHandlers.first?()
print(instance.x)
在逃逸闭包中 你需要显式的引用self
自动闭包
一种自动创建的闭包,用于包装传递给函数作为参数的表达式。不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这样帮我们省略掉花括号,用一个普通表达式代替显式的闭包
我们经常会调用采用自动闭包的函数,但是很少去实现这样的函数。举个例子来说,assert(condition:message:file:line:)
函数接受自动闭包作为它的condition
参数和 message
参数;它的condition
参数仅会在debug
模式下被求值,它的message
参数仅当 condition
参数为false
时被计算求值。
自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// 打印出“5”
//这句返回一个闭包 因此不会立即执行删除操作
//customerProvider 的 类型并不是 String 而是 ()=>String
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// 打印出“5”
//执行闭包
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// 打印出“4”
当使用@autoclosure
标记为接收自动闭包,此时可以将该函数当做接受String类型参数的函数来调用
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
//未使用自动闭包
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// 打印出 "Now serving Alex!"
//使用自动闭包
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
注意
过度使用 autoclosures 会让你的代码变得难以理解。上下文和函数名应该能够清晰地表明求值是被延迟执行的
//可以通过同时加上@autoclosure @escaping 自动闭包可以“逃逸”
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
customerProviders.append(customerProvider)
}