学编程SwiftSwift

swift闭包定义和常见用法

2021-09-09  本文已影响0人  鄂北

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

一、闭包表达式

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

{
   (参数列表) -> 返回值类型 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
}

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

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、逃逸闭包

func exec(fn: @escaping () -> ()) {
    //延迟5s
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) {
        //5s后调用闭包
        fn()
    }
    print("函数执行完毕")
}

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

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

3、自动闭包

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

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函数才会被执行

三、闭包对变量捕获

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

//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中闭包的简单使用
逃逸闭包、非逃逸闭包

上一篇 下一篇

猜你喜欢

热点阅读