swift 闭包与闭包表达式

2021-03-19  本文已影响0人  三国韩信

闭包与闭包表达式

在swift里闭包大家都很熟悉,相当于oc中的block。闭包表达式又是啥?很多人把闭包表达式等同于闭包,认为是同一个东东,其实严格上来说,闭包和闭包表达式是不一样的

闭包表达式

1、什么是闭包表达式
闭包表达式其实就是函数,是对函数的另一种定义方式。在swift中,可以通过func定义一个函数,也可以通过闭包表达式定义一个函数。
闭包表达一个函数的格式是这样的:

{
    (参数列表)->返回值类型  in 
    函数体代码
}
// 普通func表达一个函数
func sum(_v1: Int, _v2: Int) -> Int {
    return v1+v2
}
//执行这个函数 
sum(1,2)

// 通过闭包表达式来表达一个函数
var fn = {
    (v1: Int, v2: Int) -> Int  in
    return v1 + v2
}
//执行这个闭包表达式 
fn(1,2)

2、闭包表达式的简写
闭包表达式更多的情况下是作为普通函数的参数来使用,比如下面这个函数,第三个入参就是一个函数fn

func exec(v1: Int, v2: Int, fn:(Int, Int) -> Int) {
    return fn(v1+v2)
}
// 这里exec 函数有3个参数,最后一个参数传递的是一个函数,用闭包表达式来表示第三个参数。

由于swift的编译器特别智能,闭包表达式可以简写,编译也能识别出来。比如上面的函数在调用的时候,就可以有如下的简写:

// 正常的闭包表达式写法
exec(v1:10, v2:20, fn:{
    (v1:Int, v2:Int) -> Int in 
    return v1 + v2 
})

// 可以把参数类型和返回值类型省略
exec(v1:10, v2:20, fn:{
    v1, v2 in return v1 + v2 
})

// 把return都省略
exec(v1:10, v2:20, fn:{
    v1, v2 in v1 + v2 
})

// 用$0和$1表示第一个入参和第二个入参
exec(v1:10, v2:20, fn: {$0 + $1}

// 甚至$0和$1都可以省略,只留一个+号
exec(v1:10, v2:20, fn: { + } 

注:虽然Swift的编译器很智能,当个人不建议写的很省略,可读性不是很好滴。

如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性。尾随闭包是一个被书写在函数调用括号外面(后面)的闭包表达式。类似上面的例子中exec函数的最后一个参数是一个闭包表达式,那么可以用尾随闭包的方式,如下:

// 正常的闭包表达式写法
exec(v1:10, v2:20, fn:{
    (v1:Int, v2:Int) -> Int in 
    return v1 + v2 
})

// 尾随闭包的写法
exec(v1:10, v2:20) {
    (v1:Int, v2:Int) -> Int in 
    return v1 + v2 
}

// 尾随闭包的简写
exec(v1:10, v2:20) {
    $0 + $1 
}

如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的语法,那就不需要在函数名后边写圆括号。

// 闭包表达式是函数的唯一实参
func exec(fn:(Int, Int) -> Int) {
    return fn(v1+v2)
}

// 正常的写法
exec(fn: {
    (v1,v2) in 
    return v1 + v2
})

// 精简的写法
exec(fn:{$0 + $1})

// 尾随闭包的写法
exec() {
  $0+$1
}

// 更精简的写法
exec { $0+$1 }
// 最后这种写法是表示一个函数只有一个参数,且执行这个函数使用了尾随闭包的表示方式。
闭包

1、定义
一个函数和它所捕获的变量\常量环境组合起来,称为闭包。一般指定义在函数内部的函数,一般它捕获的是外层函数的局部变量\常量;

typealias Fn = (Int) -> Int
func getFn () -> Fn {
    var num = 0
    func plus(_i: Int ) -> Int {
        num += i
        return num 
    }
    return plus
}

/* plus函数是定义在函数内且捕获了外部的变量num,整个plus函数就构成了一个闭包。
如果plus函数不捕获num变量的话,严格意义上也不算是闭包。*/

var fn1 = getFn()
fn1(1)  //1 
fn1(3)  //4 
fn1(5)  //9 

/*
从fn1的3次调用结果可以看出num在fn1里是一直存在的。
实际上num已经从原来的函数内的栈空间被移到堆空间了,而且一直被fn1 持有。
实际上整个fn1闭包16个字节,前8个字节是函数的地址,后8个字节是num的地址。
*/

可以把闭包想象成是一个类的实例对象;
内存在堆空间;
捕获的局部变量\常量就是对象的成员(存储属性);
组成闭包的函数就是类内部定义的方法;
class Closure {
    var num = 0 
    func plus(_i: Int) -> Int {
        num += i 
        return num 
    }
}

2、闭包的内存结构
闭包的内存结构结论: 16个字节,前8个字节是函数的地址(不是直接的地址,间接能找到的地址),后8个字节是对应捕获的变量的堆空间的地址。(以上面的getFn为例。)
3、闭包的循环引用
和oc中的block类似,在闭包中也会强引用着捕获的外部变量,如果闭包本身也被外部变量强引用着,那么此时也会构成循环引用。在实际开发过程中也要用[weak self]来打破循环引用。

class Student {
    var name: String?
    var giveAnswerClosure: ((Int) -> Void)?
    
    // 学生回答问题
    func giveAnswer() {
        // 调用闭包给出答案
        giveAnswerClosure?(1)
    }
    
    deinit {
        print("deinit---Student")
    }
}

class Teacher: NSObject {
    var student: Student?
    var isRight: Bool? // 答案是否正确
    
    override init() {
        super.init()
    
        student = Student()
        // 闭包回调
        student?.giveAnswerClosure = { answer in
            // 答案是1
            self.isRight = answer == 1 ? true : false
        }
    }
    
    // 提问问题
    func askQuestion() {
        // 学生回答
        student?.giveAnswer()
    }
    
    deinit {
        print("deinit---Teacher")
    }
}

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
       
        let teacher = Teacher()
        teacher?.askQuestion()
    }
}

上面的例子中就存在着循环引用的情况,student持有者giveAnswerClosure这个闭包,而teacher由持有着student对象,在giveAnswerClosure闭包内又捕获了teacher对象(持有teacher)。这样就构成了teacher -> student -> giveAnswerClosure -> teacher的循环引用,导致teacher和student对象都不能释放。 打破循环引用的方式就是在闭包内用[weak self]。

// 写法一
student?.giveAnswerClosure = { [weak self] answer in
    self?.isRight = answer == 1 ? true : false
}

// 写法二
weak var weakSelf = self
student?.giveAnswerClosure = { answer in
    weakSelf?.isRight = answer == 1 ? true : false
}

更多的内存管理的请移步 https://www.jianshu.com/p/12688f7b9daf

上一篇下一篇

猜你喜欢

热点阅读