swift闭包定义和常见用法

1、闭包是自包含的功能代码块,跟C和Objective-C中的代码块(blocks)和其他一些语言中的匿名函数相似。
2、闭包可以作为函数的参数也可以作为函数的返回值。
3、可以像oc中用于回调和反向传值

一、闭包表达式

闭包表达式可以理解为闭包的表现形式
语法形式为

{
   (参数列表) -> 返回值类型 in 函数体代码
}
  • 参数可以有多个,多个参数用,隔开
  • 参数列表的小括号可以省略
  • 返回值类型也可以省略
  • 当没有参数时in可以省略
  • in可以看作是一个分隔符,将函数体和前面的参数、返回值分割开来

1、有参有返回值

// 有参有返回值
let testOne: (String,String) -> String = {(str1,str2) -> String in
    return str1 + str2
}
print(testOne("test","One"))

:的右边是闭包的类型,=右边就是一个闭包的表达式,也可以理解为一个闭包。
=右边是严格按照了闭包表达式来写的,有参数,有括号,有返回值。下面再看下闭包表达式的简写

let testOne = {str1,str2 in
    return str1 + str2
}
print(testOne("test","One"))

这个跟上一个是等价的,闭包表达式省去了参数的括号和返回值。:右边的闭包类型省去了是因为swift编译器能自动根据=右边去判断类型

2、无参无返回值

//无参无返回值
let testThree: () -> Void = {
    print("testThree")
}
testThree()

:右边类型省去后

let testThree = {
    print("testThree")
}
testThree()

因为没有参数in可以省略

二、闭包作为函数参数

func exec(fn: (Int, Int) -> Int, v1: Int, v2: Int) {
    print(fn(v1, v2))
}

这个函数有三个参数,第一个参数是一个函数
下面是exec函数的调用

exec(fn: { a, b in
    return a + b
}, v1: 1, v2: 2)

exec函数调用时,{}里就是一个闭包表达式,可以看作是第一个参数函数的实现。
这样的函数调用形式看起来很不友好,如果闭包表达式有很多行的话,会更加不友好,不利于代码的阅读。swift提供了一个尾随闭包的概念

1、尾随闭包

  • 当函数的最后一个参数是函数时,在函数调用时可以把闭包表达式写在()外面
  • 尾随闭包是一个书写在函数括号之后的闭包表达式
  • 如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
    print(fn(v1, v2))
}

这次把exec函数的第一个参数fn放到了最后,下面可以看下调用方式和上次的有什么不同

exec(v1: 1, v2: 2) { a, b in
    return a + b
}

跟上一次的函数调用对比,这次是把闭包表达式写在了函数括号之后,增强了代码的可读性,这就是尾随闭包。

  • 可以使用简化参数名,如$0, $1(从0开始,表示第i个参数...)
exec(v1: 1, v2: 2) {
    return $0 + $1
}

还有很多的简写就不一一列举了,太简写了也不利于代码阅读

  • 如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的语法,可以将函数名后边的圆括号省略
func exec(fn: (Int, Int) -> Int) {
    print(fn(1,2))
}

exec { a, b in
    return a + b
}

2、逃逸闭包

  • 如果一个闭包被作为一个函数的参数,并且在函数执行完之后才被执行,那么这种情况下的闭包就被称为逃逸闭包
  • 在参数名的:后面用@escaping来修饰说明逃逸闭包
    一般在涉及到异步操作时,闭包放在异步线程里,在这种情况下就会出现逃逸闭包,特别是在网络请求时会出现这种情况
func exec(fn: @escaping () -> ()) {
    //延迟5s
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) {
        //5s后调用闭包
        fn()
    }
    print("函数执行完毕")
}

exec {
    print("闭包执行完毕")
}

这段代码会先打印"函数执行完毕",5秒后再执行闭包打印"闭包执行完毕"

3、自动闭包

  • 在参数名的:后面用@autoclosure来修饰说明自动闭包
  • @autoclosure会自动将20封装成闭包{20}
  • @autoclosure只支持() -> T格式的参数
  • @autoclosure并非只支持最后1个参数
  • 有@autoclosure和无@autoclosure构成了函数重载
  • 空合并运算符??使用了@autoclosure技术

先分析为什么会有自动闭包,自动闭包能实现什么作用
先看下下面的一个函数

func getFirstPositive(_ a: Int, _ b: () -> Int) -> Int {
    return a > 0 ? a : b()
}

getFirstPositive(5) {
    return 20
}

getFirstPositive函数调用时使用了一个尾随闭包,当参数满足() -> T这个格式时可以写成自动闭包,会使代码阅读起来更直观

func getFirstPositive(_ a: Int, _ b: @autoclosure () -> Int) -> Int {
    return a > 0 ? a : b()
}

getFirstPositive(1, 10)

需要注意的是闭包会有推迟执行的特点,会在函数内部调用时才会执行

func getFirstPositive(_ a: Int, _ b: @autoclosure () -> Int) -> Int {
    return a > 0 ? a : b()
}

func exec() -> Int {
    print("执行了exec")
    return 20
}

getFirstPositive(1, exec())

看getFirstPositive(1, exec())函数调用时,很容易误以为exec()就已经执行了函数exec,其实并没有,exec内部没有执行,没有输出执行了exec。这是因为闭包有延迟执行的特点,getFirstPositive函数内部因为a>0返回的结果为a的值,并没有调用到b(),所以exec函数没有执行。只有当getFirstPositive函数内部调用到了b(),exec函数才会被执行

三、闭包对变量捕获

  • 闭包可以对外部函数的变量\常量进行捕获
  • 闭包捕获时机是在函数执行完,return时再去捕获
  • 当函数里有多个闭包时,只会对变量\常量捕获一次,多个闭包对捕获的变量\常量共享
  • 闭包不会对全局变量进行捕获

下面由几份代码来说明这几个结论

//MARK: 局部变量捕获
typealias fn = (Int) -> ()
func exec() -> fn {
    var num = 0
    return {a in
        num += a
        print(num)
    }
}

let fn1 = exec()
fn1(1)
fn1(2)
fn1(3)

fn1、fn2、fn3输出的结果分别是1、3、6。
这是一个函数中返回了一个闭包,在闭包里对num进行了累加并输出结果。

1、第一次调用fn1时,num为0,0加上参数1=1
2、第二次调用fn1时,闭包里的num的值是第一次fn1里的累加结果1,1加上参数2=3
3、第三次调用fn1时,闭包里num是第二次fn1里累加的结果3,3加上参数3=6
从三次调用fn1来看,闭包里num都是保存了上次调用后num的值,这是因为闭包捕获了外部的num,并重新在堆上分配了内存,当执行let fn1 = exec()时,把闭包的内存地址给了fn1,所以每次调用fn1都是调用的同一块内存,同一个闭包,闭包里有保存中捕获后的num的内存地址,所以每次调用都是同一个num

可以把闭包想象成是一个类的实例对象,内存在堆空间,捕获的局部变量\常量就是对象的成员(存储属性),组成闭包的函数就是类内部定义的方法

typealias fn = (Int) -> ()
func exec() -> fn {
    var num = 0
    return {a in
        num += a
        print(num)
    }
}

let fn1 = exec()
fn1(1)
let fn2 = exec()
fn2(1)

将上面的代码稍微改一下,将exec分别赋值给fn1和fn2,输出的结果为1和1。这个为什么不是跟上面一样累加呢,因为exec分别赋值给了fn1和fn2,fn1和fn2指向的是两个不一样的地址,当每调用一次exec()函数,num会初始化为0

typealias fn = (Int) -> ()
func exec() -> fn {
    var num = 0
    func plus(a: Int) {
        num += a
        print(num)
    }
    num = 6

    return plus
}

let fn1 = exec()
fn1(1)

上面这份代码输出的num又是多少呢?答案是7

这还是一个局部变量捕获的问题,闭包会在函数执行完,return的时候才会去捕获num,此时num已经由0变为6,所以执行fn1(1)输出结果为7

typealias fn = (Int) -> ()
func exec() -> (fn, fn) {
    var num = 0
    func plus(a: Int) {
        num += a
        print("plus:", num)
    }
    
    func minus(a: Int) {
        num -= a
        print("minus:", num)
    }

    return (plus, minus)
}

let (p, m) = exec()
p(5)
m(4)

这份代码函数返回了一个元祖,元祖里是两个闭包,两个闭包里面都调用了num,输出的结果为:
plus: 5
minus: 1
因为当函数里有多个闭包时,只会对变量\常量捕获一次,多个闭包对捕获的变量\常量共享

因为当函数里有多个闭包时,只会对变量\常量捕获一次,多个闭包对捕获的变量\常量共享。在调用m(4)时,前面已经调用过p(5),此时num已经变为5,所以当调用m(4),输出结果为1。

四、闭包中的循环引用及解决办法

大家可以看这位大神的文章Swift中闭包的简单使用,文中有详细的解析

参考文章

Swift 闭包的定义和使用
Swift中闭包的简单使用
逃逸闭包、非逃逸闭包

你可能感兴趣的:(swift闭包定义和常见用法)