一:闭包表达式的定义
在 Swfit 中,可以通过func
定义一个函数,也可以通过闭包表达式定义一个函数.闭包表达式的格式为:
{
(参数列表) -> 返回值类型 in
方法体
}
比如说我们使用func
定义一个add
方法:
func add(a: Int, b: Int) -> Int{
a + b
}
print(add(a: 10, b: 20))
用闭包表达式也可以定义:
let fn = {
(a: Int , b: Int) -> Int in
a + b
}
print(fn(10,20))
二:闭包表达式的简写
定义一个函数如下:
// 接受两个 Int 类型的参数 , 和一个函数类型的参数
func exec(a: Int, b: Int, fn: (Int, Int) -> Int){
print(fn(a,b))
}
使用闭包表达式完整调用如下:
exec(a: 10, b: 20, fn: {
(v1: Int, v2: Int) -> Int in
return v1 + v2
})
另外还有4中简写方式:
- 省略参数类型 和 返回值类型
因为闭包表达式已经明确定义了参数类型和返回值类型都是 Int 类型,所以在调用闭包表达式的时候,编译器能自动类型推断,知道具体的类型.所以类型可以省略
exec(a: 10, b: 20) {
v1, v2 in
return v1 + v2
}
- 省略 return
因为函数体代码是一个单一表达式,所以我们可以省略 return
exec(a: 10, b: 20) {
v1, v2 in v1 + v2
}
- 省略参数,
$0
$1
分别代表第一个,第二个参数
exec(a: 10, b: 20) {
$0 + $1
}
- 直接 +
如果直接写一个运算符,编译器也知道是拿两个参数直接参与运算
exec(a: 10, b: 20, fn: +)
三:尾随闭包
如果一个函数的最后一个实参是一个闭包表达式,并且这个闭包表达式的函数体代码很长,为了增强函数的可读性,这个闭包表达式可以采用尾随闭包.
尾随闭包
:尾随闭包是一个书写在函数调用括号外面的闭包表达式.
像上面的exec
函数,它的最后一个参数是一个闭包表达式,那我们我们可以使用尾随闭包来增强可读性:(事实上,编译器自动敲出来的就是尾随闭包)
func exec(a: Int, b: Int, fn: (Int, Int) -> Int){
print(fn(a,b))
}
// 尾随闭包
exec(a: 10, b: 20) { (v1, v2) -> Int in
return v1 + v2
}
如果函数只有一个参数,而且这个参数是个闭包表达式,并且使用了尾随闭包的写法,那么在调用的时候可以省略小括号()
:
func add(fn: (Int, Int) -> Int){
print(fn(20,10))
}
//正常写法
add(fn: {$0 - $1})
//尾随闭包写法
add(){$0 - $1}
//省略小括号()
add{$0 - $1}
示例:
Swift 中的Array
有一个排序方法:
func sort(by areInIncreasingOrder: (Self.Element, Self.Element) throws -> Bool)
接收两个参数,返回一个布尔值.假设第一个参数是v1
,第二个参数是v2
,它的意思是:如果返回true
,v1
排在v2
前面;如果返回false
,v1
排在v2
后面.
常规写法是传入一个函数:
func arraySort(){
//排序规则函数
func comp(v1: Int, v2: Int) -> Bool{
//从大到小排序
v1 > v2
}
var array = [4,2,10,19,13,8,22]
//传入 comp 函数
array.sort(by: comp(v1:v2:))
print(array)
}
也可以使用尾随闭包:
//闭包表达式示例 数组排序
func arraySort(){
var array = [4,2,10,19,13,8,22]
//闭包表达式常规写法
array.sort(by: {
(v1: Int, v2: Int) -> Bool in
return v1 > v2
})
//因为是最后一个参数,可以采用尾随闭包写法
array.sort(){
v1,v2 in v1 > v2
}
//因为是最后一个参数,并且是唯一一个参数,可以省略小括号
array.sort{
v1,v2 in v1 > v2
}
//使用 $ 替代参数
array.sort{
$0 > $1
}
//直接使用 >
array.sort(by: >)
print(array)
}
四:闭包
闭包和闭包表达式完全是两个概念:
闭包表达式: 闭包表达式是定义函数的一种方式;
-
闭包: 闭包是一个函数和它所捕获的变量/常量环境组合起来,称作闭包.
一般指定义在函数内部的函数
一般它捕获的是外部函数的局部变量/常量
思考一下以下代码的打印结果是什么:
typealias Fn = (Int) -> Int
func getFn() -> Fn{
var num = 0
func sum(_ v1: Int) -> Int{
num += v1
return num
}
return sum
}
var fn = getFn()
print(fn(1))
print(fn(2))
print(fn(3))
print(fn(4))
结果如下:
怎么会这样呢?num
是在getFn
函数内部定义的局部变量,在 149 行调用完getFn()
后,num
的内存应该被回收了,为什么打印的结果都是在同一块内存上累加的呢?
其实这就是闭包,sum 函数捕获了 num 变量,并且把 num 变量存在了堆空间,所以即使函数调用完毕, num 变量依然没有销毁.
我们来看一下它的汇编代码:
向堆空间申请内存然后我们再修改一下sum
函数的代码,使其不访问外部变量,再看看它的汇编,对比一下:
汇编如下:
可以看到,当sum
函数没有访问外部变量的时候,它的汇编代码很简单.也没有调用allocObject
向堆空间申请内存.并且getFn
返回的直接就是sum
函数的地址.
现在,我们大概也知道了,当 sum 函数访问了外层函数的局部变量 num 时,会调用 allocObject 函数向堆空间申请内存,把 num 变量存储在堆空间,保住 num 变量的命.也就是所谓的 捕获.
下面我们将通过汇编代码验证我们的结论.
如果所示,打两个断点:
断点运行代码:
getFn 函数返回值
上面没有访问外部变量时,getFn
直接返回的就是sum
函数的地址.但是这里不一样了,因为访问了外部变量,getFn
返回的是一个堆空间的地址.这个地址中有可以找到sum
函数地址的线索,并且还要存储捕获的num
变量.
继续跳到下一个断点:
执行fn(1)执行完fn(1)
,内存中的值变成了 1.
继续跳过断点,会执行fn(2)
:
执行fn(3)
:
执行fn(4)
:
通过上面的分析,已经很清晰的知道了当函数访问了外部函数的局部变量后,会向堆空间申请内存,用来捕获外部函数的变量.
思考一下,调用两次getFn()
下面代码打印结果是什么?
我们看看它的汇编代码:
调用两次getFn()返回了两个Fn
对象:
执行 fn1(1)
:
执行 fn2(2)
:
执行 fn1(3)
:
执行 fn2(4)
:
可以看到,执行多少次getFn()
,就会创建多少个Fn
对象,并且每个对象之间互不影响.他们都有自己的内存空间,都有自己的捕获对象.
上面的fn1 , fn2
和它捕获的变量组合在一起就是一个闭包,其实闭包和类的实例对象很相似,我们通过两张图对比一下:
另外他们都存储在堆空间,并且内存布局也一样,所以我们可以把闭包想象成一个实例对象,这样更容易理解.
网友评论