Swift进阶杂谈7:闭包

626 阅读3分钟

什么是闭包

闭包是一个捕获了上下文的常量或者变量的函数。

func test(){
    print("test") 
}

上面的函数是一个全局函数,也是一种特殊的闭包,只不过当前的全局函数并不捕获值。

func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    print("----")
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}

上面的incrementer我们称之为内嵌函数,同时从上层函数makeIncrementer中捕获变量runnungTotal

let closure: (Int) -> Int

closure = {(age: Int) in
    return age
}

这种就属于我们比较熟知的闭包表达式,是一个匿名函数,而且从上下文中捕获变量和常量。 其中闭包表达式是Swift语法。使用闭包表达式能更简洁的传达信息。当然闭包表达式的好处有很多:

  • 利用上下文推断参数和返回值类型
  • 单表达式可以隐式返回,即省略return关键字
  • 参数名称的简写(比如我们的$0)
  • 尾随闭包表达式

闭包表达式

我们先来一起回顾一下闭包表达式的定义。

{ (param) -> ReturnType  in
    //方法体 to something
}

首先按照我们之前的知识积累,OC中的Block其实是一个匿名函数,所以这个表达式要具备

  • 作用域(也就是大括号)
  • 函数和返回值
  • 函数题(in)之后的代码 Swift中的闭包既可以当做变量,也可以当做参数传递,这里我们看一下下面的例子熟悉一下:
let closure: (Int) -> Int

closure = {(age: Int) in
    return age
}

同样的我们也可以把我们的闭包声明一个可选类型:

 //错误的写法
  var closure : (Int) -> Int?
  closure = nil
 //正确的写法
  var closure : ((Int) -> Int)? 
  closure = nil

还可以通过let关键字将闭包声明为一个常量(也就意味着一旦赋值之后就不能改变了)

 let closure: (Int) -> Int
closure = {(age: Int) in
    return age
}
//再次赋值会报错 改成var声明就不会了
 closure = {(age: Int) in
    return age
}

同时也可以作为函数的参数

func test(param : () -> Int){
    print(param())
}

var age = 10

test { () -> Int in
    age += 1
    return age
}

尾随闭包

当我们把闭包表达式作为函数的最后一个参数,如果当前的闭包表达式很长,我们可以通过尾随闭包的书写方式来提高代码的可读性。

func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item2: Int, _ item3: Int) -> Bool) -> Bool{
   return  by(a, b, c)
}

test(10, 20, 30, by: {(_ item1: Int, _ item2: Int, _ item3: Int) -> Bool in
    
    return (item1 + item2 < item3)
})


如果上面的参数再长一点,这里我们看一个函数调用是不是就非常费劲,特别是在代码量多的时候

test(10, 20, 30){(_ item1: Int, _ item2: Int, _ item3: Int) -> Bool in
   return (item1 + item2 < item3)
}

这样一眼看上去就知道一个函数调用,后面是一个闭包表达式。大家看这个写法,当前闭包表达式{}放在了函数外面 其实如下array.sorted其实就是一个尾随闭包,而且这个函数就只有一个参数。可以逐渐简化

var array = [1, 2, 3]

array.sort{(item1 : Int, item2: Int) -> Bool in return item1 < item2 }

array.sort(by: {(item1, item2) -> Bool in return item1 < item2 })

array.sort(by: {(item1, item2) in return item1 < item2 })

array.sort{(item1, item2) in item1 < item2 }

array.sort{ return $0 < $1 } //self

array.sort{ $0 < $1 }

array.sort(by: <)

捕获值

这里我们借助官方文档中的例子来具体说明:

func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
let makeInc = makeIncrementer()

print(makeInc())
print(makeInc())
print(makeInc())

可以思考下,上面的print输出的都是什么? 这里每次都会在上次函数执行的基础上累加。按道理来说runningTotal是一个临时变量,每次进来的时候应该是10,这里每次却会累加,所以我们通过SIL来看一看发生了什么? 我们在SIL的文档中来搜索一下: 这里我们也可以通过断点来看一下,确实调用了swift_allocObject这个方法。 总结:

  • 一个闭包能够从上下文捕获已被定义的常量和变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍能够在其函数体内引用和修改这些值。
  • 当我们每次修改的捕获值的时候,修改的是堆区的value
  • 当每次重新执行当前函数的时候,都会重新创建内存空间、

逃逸闭包

逃逸闭包的定义:当闭包作为一个实际参数传递给一个函数的时候,并且在函数返回之后调用,我们就说明这个闭包逃逸了。当我们声明一个接受闭包作为形式参数时,你可以在形式参数前写@escaping来明确闭包是允许逃逸的。 Swift3.0之后,系统默认闭包参数是被@noescaping,这里我们可以通过SIL看出来 如果我们用@escaping修饰闭包之后,我们必须显示的闭包中使用self。我们来看第一种情况:

class LGTeacher{

    var complitionHandler: ((Int)->Void)?

    func makeIncrementer(amount: Int,  handler: @escaping (Int) -> Void){
        var runningTotal = 0
        runningTotal += amount

        self.complitionHandler = handler
    }

    func doSomething(){
        self.makeIncrementer(amount: 10) {
            print($0)
        }
    }

    deinit {
        print("LGTeaher deinit")
    }

}

var t = LGTeacher()

t.doSomething()

当前我们的complitionHandler作为当前LGTeacher是在当前方法makeIncrementer调用完成之后才会调用,这个时候闭包的生命周期是要比当前方法的生命周期长,所以我们说complitionHandler这个闭包逃逸了。 我们再接着看另外一个例子:

class LGTeacher{

    var complitionHandler: ((Int)->Void)?

    func makeIncrementer(amount: Int,  handler: @escaping (Int) -> Void){
        var runningTotal = 0
        runningTotal += amount

//        self.complitionHandler = handler
        DispatchQueue.global().asyncAfter(wallDeadline: .now() + 0.1) {
            handler(runningTotal)
        }
        
        print("makeIncrementer")
        
    }

    func doSomething(){
        self.makeIncrementer(amount: 10) {
            print($0)
            
        }
        print("doSomething")
    }

    deinit {
        print("LGTeaher deinit")
    }

}

var t = LGTeacher()

t.doSomething()

t.complitionHandler?(10)

这里的例子同样的,当前方法执行的过程中不会等待闭包执行完成之后再执行,而是直接返回,所以当前闭包的生命周期比方法长,这里我们要将闭包声明成逃逸闭包的方式。

自动闭包

我们先来看下面这个例子

func debugOutPrint(_ condition: Bool , _ message: String){ 
    if condition {
        print("lg_debug:\(message)")
    }
}
 debugOutPrint(true, "Application Error Occured")

上面代码会在当前condition为true的时候,打印我们当前的错误信息,也就意味着false的时候当前条件不会执行。 如果我们当前的字符串可能是在某个业务逻辑功能中获取的,比如瞎main这样:

func debugOutPrint(_ condition: Bool , _ message: String){ 
    if condition {
        print("lg_debug:\(message)")
    } 
}

func doSomething() -> String{
    //do something and get error message 
    return "NetWork Error Occured"
 }

 debugOutPrint(true, doSomething())

这时候我们发现一个问题,那就是当前的condition无论是true还是false,当前的doSomething方法都会执行。如果当前的doSomething是一个耗时的任务操作,那么这里是不是就造成了一定的资源浪费。 这个时候我们想到的是把当前的参数修改成一个闭包,

func debugOutPrint(for condition: Bool , _ message: () -> String){
    if condition {
        print(message())
    }
}

func doSomething() -> String{
    //do something and get error message
    print("doSomething")
    return "NetWork Error Occured"
}

debugOutPrint(for: false, doSomething())

这样的活是不是就能够正常在当前条件满足的时候调用我们当前的doSomething的方法啊。同样的问题又随之而来,那就是这里是一个闭包,如果我们这个时候就是传入一个String怎么办呢?

func debugOutPrint(for condition: Bool , _ message: @autoclosure () -> String){
    if condition {
        print(message())
    }
}

func doSomething() -> String{
    //do something and get error message
    print("doSomething")
    return "NetWork Error Occured"
}

debugOutPrint(for: false, doSomething())

debugOutPrint(for: true, "Application Error Occured")

上面我们使用@autoclosure将当前的表达式声明成了一个自动闭包,不接收任何参数,返回值是当前内部表达式的值。所以实际上我们传入的String就是放入到一个闭包表达式中,在调用的时候返回。