本文主要介绍闭包表达式和闭包,闭包表达式简化了函数的调用,闭包可以捕获局部变量,在局部变量的作用域外也可以进行操作
主要内容:
- 闭包表达式
- 闭包使用
- 闭包原理
1、闭包表达式
1.1 闭包表达式认识
闭包表达式用来实现功能,类似于函数的作用,只是写法不一样
定义格式:
{
(参数列表) -> 返回值类型 in
函数体代码
}
代码:
/*
1、闭包表达式的写法
*/
//1.1 函数
func sum(_ v1: Int, _ v2: Int) -> Int {
v1 + v2
}
print("sum\(sum(10, 20))")
//1.2 闭包表达式
var fn = {
(v1: Int,v2: Int) -> Int in
return v1 + v2
}
let result = fn(10,20)
//1.3 匿名闭包表达式
{
(v1: Int,v2: Int) -> Int in
return v1 + v2
}(10,20)
说明:
- 函数的功能使用闭包表达式也完全可以实现。
- 函数有的元素,闭包表达式也都有,只是书写方式不一样
- 注意in是用来分割函数类型和函数体的
- 闭包表达式需要使用{}来包含进去
- 如果需要赋给一个变量,就通过变量来调用,如果直接使用,就直接调用传值
简写:
/*
2、闭包表达式的简写
*/
func test2 (){
//2.1 函数
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
print(fn(v1, v2))
}
//2.2 闭包表达式正常格式
exec(v1: 10, v2: 20, fn: {
(v1: Int, v2: Int) -> Int in
return v1 + v2
})
//2.3 简写:参数类型
exec(v1: 10, v2: 20, fn: {
v1, v2 in return v1 + v2
})
//2.4 简写:return
exec(v1: 10, v2: 20, fn: {
v1, v2 in v1 + v2
})
//2.5 简写:参数
exec(v1: 10, v2: 20, fn: { $0 + $1 })
//2.6 简写:只写操作符
exec(v1: 10, v2: 20, fn: + )
}
test2()
说明:
- 可以将参数类型省略掉,因为在声明中已经明确了类型
- 可以将return省略掉,因为函数体只有一个表达式
- 可以将参数省略掉,此时使用1来表示第一个、第二个参数(因为在声明中已经明确了参数类型,所以此时不用创建参数,直接通过占位符使用)
- 甚至可以将1省略掉,只写一个运算符(因为可以通过声明判断出两个数需要进行运算)
注意:
- 与函数不同的是闭包中不用写参数标签也可以在调用时不写参数名称
- 在简写中,也可以不写参数名称,而是通过1来表示第一个、第二个参数。此时所有的内容都是函数体了。
- 在简写中,还有一种极简方式,就是只写操作符,不写参数,这样编译器会认为是操作这两个参数的。这种就是极简形式了
- 如果不用参数,直接返回值,不能直接省略,而是应该用_来占位一下
1.2 尾随闭包
如果将一个很长的闭包表达式作为函数的最后一个实参,可读性较差,此时可以使用尾随闭包增加代码的可读性。
尾随闭包就是将闭包表达式书写在调用函数的括号外面
代码:
func test3 () {
/*
3、尾随闭包
*/
//3.1 函数定义
func exec1(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
print(fn(v1, v2))
})
//3.2 函数调用时尾随闭包
exec(v1: 10, v2: 20) { $0 + $1 }
}
test3()
说明:
- 可以看fn闭包表达式作为exec函数的参数,但是本身是放在了括号的后边,这就是所谓的尾随
- 需要注意的是,如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的语法,就不需要在函数后面写()
1.3 案例说明
以数组排序的实际案例来使用闭包表达式
代码:
/*
4、数组排序实例
*/
func test4 () {
//func sort(by areInIncreasingOrder: (Element, Element) -> Bool)
var nums = [11, 2, 18, 6, 5, 68, 45]
//4.1 sort排序函数中最后一个参数设置闭包表达式
nums.sort(by: {
(i1: Int, i2: Int) -> Bool in
return i1 < i2
})
//各种简写
nums.sort(by: { i1, i2 in return i1 < i2 })//正常
nums.sort(by: { i1, i2 in i1 < i2 })//没有return
nums.sort(by: { $0 < $1 })//没有参数
nums.sort(by: <)//只有运算符
nums.sort() { $0 < $1 }//尾随闭包
nums.sort { $0 < $1 }//最后一个参数可以不用写()
print("nums:\(nums)")//nums:[2, 5, 6, 11, 18, 45, 68]
}
test4()
说明:
- 闭包表达式实现了排序操作
- 可以看到将闭包表达式作为了参数来传递
- 这里也把前面所讲的所有简写方式都写了一遍
2、闭包
闭包就是可以捕获变量/常量的函数或闭包表达式,它有两个特点,函数/闭包表达式,捕获局部变量。因此可以看到它和OC中的block基本上是一样的了。具体关于block的认识可以看我的另一篇博客
,如果理解block,再看闭包就会觉得很简单了
我们在实际理解闭包时,可以将其看做一个类,捕获的变量是成员变量,函数/闭包表达式是类的成员函数。
2.1 闭包的使用
代码:
/*
1、闭包认识
*/
func test5 () {
//定义一个函数类型
typealias Fn = (Int) -> Int
//返回一个函数
func getFn() -> Fn {
var num = 0
//plus函数会捕获num变量
//plus函数和捕获的num变量形成了闭包
func plus(_ i: Int) -> Int {
num += i
return num
}
return plus
}
var fn1 = getFn()
var fn2 = getFn()
fn1(1) // 1
fn2(2) // 2
fn1(3) // 4
fn2(4) // 6
}
test5()
说明:
- 可以看到此时的plus就是一个闭包,因为它本身是一个函数,但是会捕获外部的num变量
- 这里就和block一样,num是捕获到了堆中,所以plus中的num和getFun()函数中的num已经不是一个地址了。他们二者之间没有关系
2.2 变量捕获的汇编分析
闭包变量包含16个字节,前8个字节是函数地址,后8个字节是变量堆空间地址
2.2.1 函数查看
代码:
汇编分析
说明:
2.2.2 不捕获变量的闭包
代码:
汇编分析
说明:
2.2.3 捕获变量的闭包
代码:
汇编分析
说明:
2.2.4 捕获变量的时机
代码:
/*
3、捕获变量的时机
*/
func test7() {
typealias Fn = (Int) -> Int
func getFn() -> Fn {
var num = 11
func plus(_ i: Int) -> Int {
num += i
return num
}
//捕获的是14,而非11
num = 14
return plus
}
var fn1 = getFn()
fn1(1)//15
}
说明:
- 可以看到只有在返回这个plus函数地址的时候才会去捕获,所以并不是在定义函数的时候捕获,而是在创建函数变量的时候捕获
- 其实这里就算没有return,只要是创建了plus函数对象就会捕获的
2.3 多变量多函数的闭包
代码:
/*
4、多变量多函数的分析
*/
func test8() {
func getFns() -> (Fn, Fn) {
var num1 = 0
var num2 = 0
func plus(_ i: Int) -> (Int, Int) {
num1 += i
num2 += i << 1
return (num1,num2)
}
func minus(_ i: Int) -> (Int, Int) {
num1 -= i
num2 -= i << 1
return (num1,num2)
}
return (plus,minus)
}
let (p, m) = getFns()
p(6)//(6,12)
p(5)//(1,2)
p(4)//(5,10)
p(3)//(2,4)
}
说明:
- 多个函数捕获同一个变量,他们捕获后是同一份
- 多个变量分开alloc,是不同的堆空间
- 闭包我们可以将其看做一个类里。捕获的变量是成员变量,函数是类的成员函数。所以对于成员变量就是同一份
- 如果有多个变量,他们需要多次alloc,并不是放在同一个堆空间。此时的fn变量的大小就应该更多了,如果是两个变量,那么就是24个字节了
2.4 数组循环闭包分析
代码:
/*
5、数组循环闭包分析
*/
func test9() {
//定义一个数组,元素是函数
var functions: [() -> Int] = []
for i in 1...3 {
functions.append{ i }
}
for f in functions {
print("\(f())")
}
}
代码分析:
- 创建一个函数数组([() -> Int]就表示存放函数的数组)
- 给数组添加三个函数,每个函数的函数体都是返回一个i
- {}是尾随闭包,append拼接函数的参数只有这一个闭包,所以就把()省略掉了。这个闭包的函数体就是返回i
- 之后循环执行数组中的函数
说明:
- 在for循环中的闭包,每次都会捕获变量
- 也就是此时会创建多个堆空间
2.5 自动闭包
如果我们传入的参数是需要作为包,但是写成包的样式可读性会很差,就可以设置成自动闭包,系统帮我们闭包
代码:
/*
6、自动闭包
使用关键字@autoclosure修饰参数
*/
func test10() {
func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
return v1 > 0 ? v1 : v2()
}
//下面三条语句等效
getFirstPositive(10, 20)//自动闭包
getFirstPositive(10, {20})
getFirstPositive(10){20}//尾随闭包
}
test10()
说明:
- 这三者是等效的,也就是说如果我再参数中加上了@autoclosure,那么在传入参数的时候就不需要传入闭包,直接传入函数体后,编译器会帮我们自动闭包
- 如果我们传入的参数是需要作为包,但是写成包的样式可读性会很差,就可以设置成自动闭包,系统帮我们闭包
- 本来我们需要{20}传入闭包,但是我们传入20后系统会判断需要自动闭包就会将这个20作为闭包的函数体进行处理
注意:
- @autoclosure设置后会自动闭包,传入的参数就不能是闭包。二者会冲突
- @autoclosure只支持() -> T格式的参数(参数为空,有返回值)
- @autoclosure并非只只支持最后一个参数
- 有@autoclosure和没有@autoclosure构成了函数重载
网友评论