闭包
- 闭包是自包含的函数代码块,可以在代码中被传递和使用。
Swift
中的闭包与C
和Objective-C
中的代码块(blocks)以及其他一些编程语言中的匿名函数比较相似。 - 闭包可以捕获和存储其所在上下文中任意常量和变量的引用,被称为包裹常量和变量。
Swift
会为你管理在捕获过程中涉及到的所有内存操作。 - 也可以说闭包是一个捕获了上下文的常量和变量的函数。
- 闭包的表现形式:
- 1,全局函数是一个有名字但不会捕获任何值的闭包
- 2,嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包
- 3,闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包
闭包表达式
- 完整的闭包表达式要具备:
- 作用域{}
- 参数和返回值
- 函数体-(in)之后的代码
{ (parameters) -> return type in
statements
}
-
Swift
中的闭包可以当做变量,也可以当做参数传递,也可以将它声明为一个可选类型,还可以通过let
声明为一个常量,也可以作为函数的参数使用
// 声明为一个变量
var closure : (Int) -> Int = { (age: Int) in
return age
}
// 声明为一个可选项
// 错误写法
var closure : (Int) -> Int?
closure = nil
// 正确写法
var closure : ((Int) -> Int)?
closure = nil
// 声明为一个常量
let closure: (Int) -> Int
closure = {(age: Int) in
return age
}
// 闭包作为参数
func test(param : () -> Int){
print(param())
}
var age = 10
test { () -> Int in
age += 1
return age
}
- 闭包表达式是一种利用简洁语法构建内联闭包的方式。闭包表达式提供了一些语法优化,使得撰写闭包变得简单明了。下面闭包表达式的例子通过使用几次迭代展示了
sorted(by:)
方法定义和语法优化的方式。每一次迭代都用更简洁的方式描述了相同的功能。
var array = [4, 2, 3]
array.sort(by: {(item1 : Int, item2: Int) -> Bool in return item1
< item2 })
👇
array.sort{(item1 : Int, item2: Int) -> Bool in return item1 < item2 }
👇
array.sort(by: {(item1, item2) -> Bool in return item1 < item2 })
👇
array.sort(by: {(item1, item2) in return item1 < item2 })
👇
array.sort{(item1, item2) in item1 < item2 }
👇
array.sort{ return $0 < $1 } //self
👇
array.sort{ $0 < $1 }
👇
array.sort(by: <)
根据上下文推断类型
- 因为排序闭包函数是作为
sorted(by:)
方法的参数传入的,Swift
可以推断其参数和返回值的类型。sorted(by:)
方法被一个整型数组调用,因此其参数必须是(Int, Int) -> Bool
类型的函数。这意味着(Int, Int)
和Bool
类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断,返回箭头(->)和围绕在参数周围的括号也可以被省略:
array.sort(by: { item1, item2 in return item1 < item2 } )
- 实际上,通过内联闭包表达式构造的闭包作为参数传递给函数或方法时,总是能够推断出闭包的参数和返回值类型。这意味着闭包作为函数或者方法的参数时,你几乎不需要利用完整格式构造内联闭包。
- 尽管如此,你仍然可以明确写出有着完整格式的闭包。如果完整格式的闭包能够提高代码的可读性,官方也鼓励采用完整格式的闭包。而在
sorted(by:)
方法这个例子里,显然闭包的目的就是排序。由于这个闭包是为了处理整型数组的排序,因此读者能够推测出这个闭包是用于整型处理的。
单表达式闭包隐式返回
- 单行表达式闭包可以通过省略
return
关键字来隐式返回单行表达式的结果,如上版本的例子可以改写为:
array.sort(by: { item1, item2 in item1 < item2 } )
参数名称缩写
-
Swift
自动为内联闭包提供了参数名称缩写功能,你可以直接通过$0,$1,$2
来顺序调用闭包的参数,以此类推。 - 如果你在闭包表达式中使用参数名称缩写,你可以在闭包定义中省略参数列表,并且对应参数名称缩写的类型会通过函数类型进行推断。
in
关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:
array.sort(by: { $0 < $1 } )
运算符方法
- 实际上还有一种更简短的方式来编写上面例子中的闭包表达式。
array.sort(by: <)
尾随闭包
- 如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。在使用尾随闭包时,你不用写出它的参数标签:
func someFunctionThatTakesAClosure(closure: () -> Void) {
// 函数体部分
}
// 以下是不使用尾随闭包进行函数调用
someFunctionThatTakesAClosure(closure: {
// 闭包主体部分
})
// 以下是使用尾随闭包进行函数调用
someFunctionThatTakesAClosure() {
// 闭包主体部分
}
- 上文的排序方法可以简写
array.sort(){ $0 < $1 }
- 如果闭包表达式是函数或方法的唯一参数,则当你使用尾随闭包时,你甚至可以把 () 省略掉:
array.sort{ $0 < $1 }
- 当闭包非常长以至于不能在一行中进行书写时,尾随闭包变得非常有用。举例来说,
Swift
的Array
类型有一个map(_:)
方法,这个方法获取一个闭包表达式作为其唯一参数。该闭包函数会为数组中的每一个元素调用一次,并返回该元素所映射的值。具体的映射方式和返回值类型由闭包来指定。 - 当提供给数组的闭包应用于每个数组元素后,
map(_:)
方法将返回一个新的数组,数组中包含了与原数组中的元素一一对应的映射后的值。
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
}
print(strings)
// strings 常量被推断为字符串类型数组,即 [String]
// 其值为 ["OneSix", "FiveEight", "FiveOneZero"]
值捕获
- 闭包可以在其被定义的上下文中捕获常量和变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。
-
Swift
中,可以捕获值的闭包的最简单形式是嵌套函数,也就是定义在其他函数的函数体内的函数。嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 10
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
let closure = makeIncrementer(forIncrement: 10)
print(makeIncrementer(forIncrement: 10)())
print(makeIncrementer(forIncrement: 10)())
print(makeIncrementer(forIncrement: 10)())
print(closure())
print(closure())
print(closure())
// 打印结果
20
20
20
20
30
40
-
makeIncrementer
返回类型为() -> Int
。这意味着其返回的是一个函数,而非一个简单类型的值。该函数在每次调用时不接受参数,只返回一个Int
类型的值。 -
incrementer()
函数并没有任何参数,但是在函数体内访问了runningTotal
和amount
变量。这是因为它从外围函数捕获了runningTotal
和amount
变量的引用。捕获引用保证了runningTotal
和amount
变量在调用完makeIncrementer
后不会消失,并且保证了在下一次执行incrementer
函数时,runningTotal
依旧存在。
为了优化,如果一个值不会被闭包改变,或者在闭包创建后不会改变,Swift 可能会改为捕获并保存一份对值的拷贝。
Swift 也会负责被捕获变量的所有内存管理工作,包括释放不再需要的变量。
- 我们通过sil看看发生了什么,通过
alloc_box
创建一个空间给变量runningTotal
,后面还有对它的内存管理,alloc_box
就是在堆区分配一块内存空间存储值,会调用swift_allocObject
闭包是引用类型
- 上面的例子中,
closure
是常量,但是这些常量指向的闭包仍然可以修改其捕获的变量的值。这是因为函数和闭包都是引用类型。 - 无论你将函数或闭包赋值给一个常量还是变量,你实际上都是将常量或变量的值设置为对应函数或闭包的引用。上面的例子中,指向闭包的引用
closure
是一个常量,而并非闭包内容本身。 -
这也意味着如果你将闭包赋值给了两个不同的常量或变量,两个值都会指向同一个闭包:
通过IR来分析
IR的一些简单语法
- 我们这里只介绍我们用得到的语法
- 数组
[<elementnumber> x <elementtype>] // example alloca [24 x i8], align 8 //24个i8都是0
- 结构体
%T = type {<type list>} %swift.refcountd = type {%swift.type*, i64 }
- 指针类型
<type> * i64* // 64位的整型
-
getelementptr
指令,LLVM中我们获取数组和结构体的成员,通过getelementptr
,语法规则如下:
<result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <id x>}* <result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
- 通过一个例子来理解
getelementptr
指令
struct munger_struct {
int f1;
int f2;
};
void munge(struct munger_struct *P) {
P[0].f1 = P[1].f1+P[2].f2;
}
struct munger_struct array[3];
-
cd
到main.c
目录下,输入命令clang -emit-llvm -S main.c -o main.ll
,打开main.ll
,%13
就是数组的首个元素,%14
就是取出结构体的第一个元素也就是P[0].f1
分析上文中的closure
- 将上文的代码转为
IR
代码,可以看到makeIncrementer
函数返回了一个结构体,第一个元素为void *
,第二个元素为%swift.refcounted*
-
%swift.refcounted*
的定义,它是一个结构体指针
- 再来看下上面结构体的赋值,可以看到第一个元素里面存的就是内嵌函数的地址
%12 = insertvalue { i8*, %swift.refcounted* } { i8* bitcast (i64 (%swift.refcounted*)* @"$s4main15makeIncrementer12forIncrementSiycSi_tF11incrementerL_SiyFTA" to i8*), %swift.refcounted* undef }, %swift.refcounted* %8, 1
-
第二个参数的结构
- 知道了它们的结构体之后,我们将它转化为对应的结构体如下:
struct HeapObject{
var type: UnsafeRawPointer
var refCount1: UInt32
var refCount2: UInt32
}
//
struct FuntionData<T>{
var ptr: UnsafeRawPointer // 内嵌函数的地址
var captureValue: UnsafePointer<T> // 捕获值的结构体
}
struct Box<T> {
var refCounted: HeapObject
var valueBox: UnsafeRawPointer
var value: T
}
// 由于编译器不能识别FuntionData,所以我们将它绑定到一个具体的结构体上
struct VoidIntFun {
var f: () ->Int
}
-
验证我们上面的结论是否正确。
-
在终端中查找打印的地址,可以看到它就是我们的内嵌函数
-
通过
lldb
查看内存,我们直接查看closure
的内存,可以看到并没有打印出我们想象中的结果
-
将返回值绑定在一个结构体上
函数也是一种引用类型
- 函数是一个独立的代码块,用来执行特定的任务。同时我们函数也可以被当做参数被传递,也可以赋值变量,这里我们定义一个简单的函数来看一下:
func makeFunc(param:Int) -> Int {
var runningTotal = 10
return runningTotal + param
}
var m = makeFunc
-
查看它的IR代码,可以看到和闭包的结构类似,只是赋值的时候第二个值为空
-
我们也可以通过定义结构体的方式打印它
- 函数的本质也是一个结构体,不过这个结构体里只保存了函数的地址
逃逸闭包
- 当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。当你定义接受闭包作为参数的函数时,你可以在参数名之前标注
@escaping
,用来指明这个闭包是允许“逃逸”出这个函数的。 - 一种能使闭包“逃逸”出函数的方法是,将这个闭包保存在一个函数外部定义的变量中。举个例子,很多启动异步操作的函数接受一个闭包参数作为
completion handler
。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。在这种情况下,闭包需要“逃逸”出函数,因为闭包需要在函数返回之后被调用。例如:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
-
someFunctionWithEscapingClosure(_:)
函数接受一个闭包作为参数,该闭包被添加到一个函数外定义的数组中。如果你不将这个参数标记为@escaping
,就会得到一个编译错误。 - 默认的闭包都是非逃逸的,函数的生命周期和闭包的生命周期是一样的,函数结束之后闭包的生命周期也就结束了;逃逸闭包出现的情况一般是延迟调用闭包,或者将它作为属性存储。
自动闭包
- 自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。
- 我们经常会调用采用自动闭包的函数,但是很少去实现这样的函数。举个例子来说,
assert(condition:message:file:line:)
函数接受自动闭包作为它的condition
参数和message
参数;它的condition
参数仅会在debug
模式下被求值,它的message
参数仅当condition
参数为false
时被计算求值。 - 自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。延迟求值对于那些有副作用
(Side Effect)
和高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。下面的代码展示了闭包如何延时求值。
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// 打印出 "5"
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// 打印出 "5"
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// 打印出 "4"
- 尽管在闭包的代码中,
customersInLine
的第一个元素被移除了,不过在闭包被调用之前,这个元素是不会被移除的。如果这个闭包永远不被调用,那么在闭包里面的表达式将永远不会执行,那意味着列表中的元素永远不会被移除。请注意,customerProvider
的类型不是String
,而是() -> String
,一个没有参数且返回值为String
的函数。
过度使用 autoclosures 会让你的代码变得难以理解。上下文和函数名应该能够清晰地表明求值是被延迟执行的。