Swift之闭包本质详解

2,056 阅读4分钟

闭包(Closures)是独立的函数代码块,能在代码中传递及使用。Swift中的闭包与C和Objective-C中的代码块及其它编程语言中的匿名函数相似。

闭包可以在上下文的范围内捕获、存储任何被定义的常量和变量引用。因这些常量和变量的封闭性,而命名为“闭包(Closures)”。Swift能够对所有你所能捕获到的引用进行内存管理。

闭包的本质是代码块,它是函数的升级版本,函数是有名称、可复用的代码块,闭包则是比函数更加灵活的匿名代码块。

1.闭包表达式-Closure Expression

闭包表达式包含参数,返回值,及函数体代码。注意参数不能有默认值。

{
   (参数列表) -> 返回值类型 in 
    函数体代码 
}

2.闭包表达式的简写

let names = ["block1","block2","block3"]
var reversedName = name.sorted(by: {(s1: String,s2: String) -> Bool in 
    return s1 > s2
})
// 输出 ["block1","block2","block3"]

简化:

swift可以推断参数类型和返回值类型,因此可以不写类型。 单表达式可以不用写return. swift中可以简写实际参数名字,可以用0,0,1表示。

尾随闭包:如果函数的最后一个参数是闭包,则闭包可以写在形参小括号的外面。为了增强函数的可读性

如果闭包表达式是函数的唯一一个参数,那么小括号也可以省略了。

var reversedName = name.sorted{ > }

3.闭包-Closure定义以及值捕获

  • 一个函数和他捕获的变量、常量环境组合起来,称为闭包。
  • 闭包一般是定义在函数内部的函数。
  • 闭包一般捕获外层函数的局部变量、常量。
typealias Sum = (Int) -> Int // 函数类型

func getSum() -> Fn {
  var num = 0
  // 这个内嵌的函数其实就是一个闭包,只不过这个闭包已func的形式表现,而不是闭包表达式的形式
  func add(_ i: Int) -> Int {
    num += i
    return num
  }
  return add // 其实返回的就是plus地址
}

/* 此时fn占16个字节 前8个字节放的是间接的add函数地址 后8个字节放的是堆空间的地址值*/
var fn = getSum() 
/*1>.执行完上面一行代码之后,即调用getFn方法之后。
  2>.按理说,getSum方法执行完,方法里面的局部变量num应该从栈空间被释放.
  3>.但是此时fn里面存储了就是add这个函数地址,
     而此时add方法中用到的这个num会被放到堆内存(值捕获),从而延长了num的生命周期
     这样num这个内存就不会在执行完getSum之后被销毁。
  4>.拓展:如果num是个全局变量,那num的存放地址就在全局数据区,就不用把num放到堆空间了,
     根本不需要捕获。

*/

print(fn(1)) // 输出 1
print(fn(5)) // 输出 6 
/*从上面的三次print输出的结果也可以知道,fn(1),fn(2)访问的是同一块内存地址,
  所以才有了累加的效果。  
*/  

类比总结:可以把闭包想象成一个类的实例对象

  • 1>.(闭包的)内存在堆空间上。
  • 2>.捕获的局部变量、常量就相当于对象的成员(存储属性),当然是在堆内存上。
  • 3>.组成闭包的函数就是相当于类内部定义的方法(就是上面的add方法)。

4.非逃逸闭包、逃逸闭包 escaping

  • 非逃逸闭包、逃逸闭包是当闭包作为一个实际参数传递给一个函数。
  • 非逃逸闭包:闭包调用发生在函数结束之前,闭包调用在函数作用域内。
  • 逃逸闭包:闭包有可能在函数结束后调用,调用逃离了函数的作用域。需要用escaping来修饰闭包是允许逃逸的。多用于网络请求。 注意:让闭包逃逸意味着必须在闭包中显式引用self.
typealias FunBlock = () -> ()

// 非逃逸闭包
func test(_ fn: FunBlock) {
  // 在函数作用域里面调用了这个闭包
  fn()
}

test {
  print(3)
}

// 逃逸闭包 

var gFn: FunBlock?

// 在函数作用域里面没有调用fn这个闭包,而是赋值给了一个变量
func test2(_ fn: @escaping FunBlock) {
  gFn = fn
}

// 异步调用fn这个闭包
func test(_ fn: @escaping FunBlock) {
  DispatchQueue.global().async {
    fn()
  }
}

5.自动闭包

  • 自动闭包是就是将一个表达式自动转成闭包。
  • autoclosure只支持 () -> T 格式的参数。
  • autoclosure并非只支持最后一个参数。 注意:使用了autoclosure的地方,这个值会被延迟执行。
func justFun(_ condition: @autoclosure ()->Bool, 
            _ message: @autoclosure ()-> String) {
     return condition() ? message() : "no String"
}


// 调用
justFun(number == 5,"number 等于 5")
// autoclosure 会自动将 number 等于 5 封装成{ number == 5 }
 

逃逸闭包和自动闭包的使用

var names = ["just","kiw","jket","lku"]
var customeFunsList: [() -> String] = [] //存放闭包的数组

// 此时闭包没有调用,需要等函数执行完调用所以要用@escaping 修饰
func customeFuns(customeFun: @autoclosure @escaping () -> String) {
    customeFunsList.append(provider)
}

// 传入的值为一个String形式,由于用autoclosure修饰,会自动转为闭包类型
//此时names.count = 4
collectCustomerProviders(customeFun: customeFunsList.remove(at: 0)) 
//此时names.count = 4
collectCustomerProviders(customeFun: customeFunsList.remove(at: 0))

for customeFun in customeFunsList {
    print(customeFun()) //此时的闭包才被执行
}
// 此时names.count = 2