12 闭包

作者: 程序小胖 | 来源:发表于2020-01-23 20:56 被阅读0次

    要说明的是,【闭包】和【闭包表达式】不是一回事,你可将【闭包表达式】看作是【闭包】的一种形式。因为闭包还有其他两种形式存在:【全局函数】和【嵌套函数】。一般的,我们所说的闭包就是闭包表达式。
    总结:闭包的表现形式:【闭包表达式】、【全局函数】、【嵌套函数】

    那么,下面我就说说闭包表达式:

    一,闭包表达式

    1. 语法
    { (参数列表) -> 返回类型 in
        实现
     }
    // 简单实现
    { 实现 } 
    
    2. 各种省略写法
    // 声明一个含有闭包入参的函数
    func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
        print(fn(v1, v2))
    }
    
    // 2.1 常规调用 in 前面是声明,后边是实现
    exec(v1: 10, v2: 20, fn: {
        (v1: Int, v2: Int) -> Int in
        return v1 + v2
    })
    // 2.2 省略类型
    exec(v1: 10, v2: 20, fn: {
        (v1, v2) -> Int in
        return v1 + v2
    })
    // 2.3 省略小括号()
    exec(v1: 10, v2: 20, fn: {
        v1, v2 -> Int in
        return v1 + v2
    })
    // 2.4 省略返回值类型
    exec(v1: 10, v2: 20, fn: {
        v1, v2 in
        return v1 + v2
    })
    // 2.5 省略return
    exec(v1: 10, v2: 20, fn: {
        v1, v2 in v1 + v2
    })
    // 2.6 使用简化参数名 (用$0 $1... 代替参数列表)
    // 表示v1和v2相加并且返回
    exec(v1: 10, v2: 20, fn: { $0 + $1 })
    // 为什么可以直接用$0 和 $1 代表呢? 因为在函数定义时,是有明确类型的,所以系统会自动帮忙推导
    
    

    二, 尾随闭包

    • 如果将一个很长的闭包表达式,作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性
    • 尾随闭包是一个被书写在函数调用括号外面的闭包表达式
    • 如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的写法,那就不需要在函数后面写圆括号了
    • 一般的,开发者联想出来的就是尾随闭包的写法
    // 在我们敲下exec时,系统会自动联想,然后敲回车键,此时出来的就是尾随闭包的写法
    exec(v1: 20, v2: 30) { (<#Int#>, <#Int#>) -> Int in
    }
    
    // 尾随闭包使用简化参数名
    exec(v1: 10, v2: 30) { $0 + $1 }
    
    // 唯一实参,并且是尾随闭包
    func closure1(fn: (Int, Int) -> Int) {
        print(fn(1, 2))
    }
    // 可以直接省略小括号()
    closure1 { $0 + $1 }
    // 可以忽略实参 _
    closure1 { (_, _) -> Int in
        20
    }
    

    【注意】尾随闭包的写法,是不是似曾相识,对了,就是我们常用的网络请求

    // 如果这个表达式这样写,是不是就是咋们常见的网络请求
    // 请求的实现
    func request(finish: (String) -> Void ) {
        finish("我是数据请求")
    }
    
    // 请求调用
    request { (response) in
        print(response)
    }
    // 当然可以直接这样简写
    request {
        print($0)
    }
    

    三,自动闭包

    • 自动闭包关键字 @autoclosure
    • 只支持 () -> T 格式的闭包
    • 空合并运算符 ?? 本质就是 自动闭包
    • 自动闭包的值,不一定执行
    // 栗子
    func getNum() -> Int {
        return 20
    }
    
    // 1. 简单入参
    func getFirst(v1: Int, v2: Int) -> Int {
        return v1 > 0 ? v1 : v2
    }
    getFirst(v1: 10, v2: 20)
    getFirst(v1: -1, v2: 20)
    getFirst(v1: 0, v2: -1)
    // 如果 v1大于0了,其实就没必要执行getNum() 了,但是实际上getFirst都会执行一遍
    getFirst(v1: 20, v2: getNum())
    
    // 2. 改造下 如何实现:当v1大于0了,就不会执行v2的代码了
    // 将入参改成闭包
    func getFirst2(v1: Int, v2:  () -> Int) -> Int {
        return v1 > 0 ? v1 : v2()
    }
    getFirst2(v1: -10, v2: {
        print("--------")
        return 20
    })
    // 尾随闭包
    getFirst2(v1: 10) { () -> Int in
        print("--------")
        return 20
    }
    
    // 3. 再次改造,加入  @autoclosure 关键字
    // @autoclosure 在声明时加上这个标志,可以直接传入int了
    func getFirst2(v1: Int, v2: @autoclosure () -> Int) -> Int {
        return v1 > 0 ? v1 : v2()
    }
    // 下面的写法不会报错
    getFirst2(v1: 20, v2: 30)
    // 下面这个不是简写
    getFirst2(v1: 20, v2: { () -> Int in
        return 20
    }())
    
    

    四,逃逸闭包

    • 当一个闭包传入函数中时,在这个函数返回后才去执行闭包,那么我们就说这个闭包是逃逸的
    • 简单讲,我们常见的网络请求就是【逃逸闭包】
    func reqestHomeData(finish: @escaping (String) -> Void) {
        DispatchQueue.global().async {
            // 模拟数据加载
            
            // 数据加载完后,回调到主线程
            DispatchQueue.main.async {
                
                finish("我是异步请求数据")
            }
        }
    }
    
    func reqestHomeDataNoEscaping(finish: (String) -> Void) {
        finish("我是同步请求数据")
    }
    
    var number = 10
    request { (response) in
        // 这里面需用到 number , 就必须用到 self,显示调用
        self.number = 20
        print(response)
    }
    reqestHomeDataNoEscaping { (response) in
        // 在非逃逸闭包里面,不需要self,隐式调用
        number = 20
        print(response)
    }
    

    五,闭包捕获值

    • 一个函数和他所捕获的变量或者常量环境组合起来,成为闭包
    • 闭包就是定义在函数内部的函数
    • 闭包捕获的是外层函数的局部变量或常量
    • 可以将闭包想象成类的实例对象
    func cfn() -> () -> () {
        var a = 10
        // cfn1 就是闭包,函数里面的函数
        func cfn1() {
            // 函数里面用到了函数外层用到的局部变量量
            a = 11
        }
        return cfn1
    }
    
    // 举个例子
    typealias cfn = (Int) -> Int
    
    func getCfn() -> cfn {
        var num = 0
        // 如果这个Num是全局变量,那么也是可以用的,全局变量在全局段,全局只有一份,不会销毁
        // 但是如果是局部变量,那么在调用函数结束时,num就会被释放,在栈空间
        // 按理说,这个num应该被释放,但是为啥好像没被释放呢
        // 其实,num确实在函数调用结束后释放掉了,那么为啥还会在plus中可以使用呢,这就是闭包的捕获
        
        // 用函数表示一个闭包
        func plus(i: Int) -> Int {
            // 只在这里用到num时,申请了一块堆空间,这样就不会销毁了
            // 如果没有用到,就不会开辟
            // 其实本质上就是 将num move 到了堆空间(汇编知识)
            num += i
            return num
        }
        // 返回的plus 和 num 形成了 闭包
        return plus
    }
    
    // 获取一个函数
    // fn 的内存地址中,肯定还有一份内存是存放plus的
    var fn = getCfn() // 调用一次,就申请一块堆空间
    print(fn(1)) // 1
    print(fn(2)) // 3
    print(fn(3)) // 6
    print(fn(4)) // 10
    
    var fn = getCfn() // 调用一次,num 就申请一块堆空间
    var fn1 = getCfn() // 调用一次,num 就申请一块堆空间
    // 总共有两份堆空间
    print(fn(1)) // 1
    print(fn1(2)) // 2
    print(fn(3)) // 4
    print(fn1(4)) // 6
    
    // 闭包表达式表示一个闭包
    func testFn() -> cfn {
        var num = 0
        // 利用闭包表达式来表示函数
        let fn = { (v1: Int) -> Int in
            num += v1
            return num
        }
        return fn
        
    // 简写
    //    return {
    //        num += $0
    //        return num
    //    }
    }
    
    var testfn = testFn()
    print(testfn(3))
    
    以上就是我对闭包的理解,说实在的,我是学完语法,然后做了一段时间的项目,再回头看语法时,才稍微理解了点这个闭包,因为我们常用的就是一个尾随的逃逸闭包,其他的很少接触。没有项目经验,刚接触肯定会蒙圈,只要多写多练多思考,就好了

    过年快乐

    相关文章

      网友评论

          本文标题:12 闭包

          本文链接:https://www.haomeiwen.com/subject/mmteactx.html