跳转至

闭包

函数类型

函数本身也有自己的类型,它由形式参数类型,返回类型组成。

函数是引用类型

func addTwoInts(_ a: Int, _ b: Int) -> Int {
  return a + b
}

func addTwoInts(_ a: Double, _ b: Double) -> Double {//同名函数,参数和返回值不同
  return a + b
}

var a: (Double, Double) -> Double = addTwoInts//a变量名后面需要加函数类型
a(10, 20)
var b = a
b(20 ,30)

闭包是引用类型

func makeIncrementer() -> () -> Int {//返回值是一个函数(返回值函数:没有参数、返回值是Int)
    var runningTotal = 10
    func incrementer() -> Int {
        runningTotal += 1 //把runningTotal捕获到堆区
        return runningTotal
    }
    return incrementer
}
//makeInc 引用类型
let makeInc = makeIncrementer()

print(makeInc())//11
print(makeInc())//12
print(makeInc())//13

//如果把上面调用案例修改一下:
//makeIncrementer()这里就类似我们创建了一个实例变量
print(makeIncrementer()())//11
print(makeIncrementer()())//11
print(makeIncrementer()())//11

闭包捕获值

捕获值相当于在堆区开辟内存空间,创建一个实例变量,捕获的值当作这个实例变量的属性。

先来回顾一下 Block 捕获值的情形

- (void)testBlock{
    NSInteger i = 1;

    void(^block)(void) = ^{
        NSLog(@"block %ld:", i);
    };

    i += 1;

    NSLog(@"before block %ld:", i);
    block();
    NSLog(@"after block %ld:", i);
}

这里输出的结果是: 2, 1, 2 。这是因为当前我们的应用程序执行到 i += 1 的时候,其实当前 block 已经捕获了当前 i 的瞬时变量的值,相当与在其内做了一次:

NSInteger tmp = i = 1

那么如果我们想要外部的修改能够影响当前 block 内部捕获的值,我们只需要对当前的 i 添加 __block 修饰符

- (void)testBlock{
    __block NSInteger i = 1;

    void(^block)(void) = ^{
        NSLog(@"block %ld:", i);
    };

    i += 1;

    NSLog(@"before block %ld:", i); //2
    block();                                                //2
    NSLog(@"after block %ld:", i);  //2
}

那么对于 Swift 中的闭包同样会捕获值,这里我们把 OC的例子修改成对应 Swift 的例子来看一下:

var i = 1
let closure = {
    print("closure \(i)")
}
i += 1
print("before closure \(i)")    //2
closure()                                           //2
print("after closure \(i)")     //2

这里例子输出的结果就和 Block 中的第一个例子不同了,因为当前 Swift 值的捕获是在执行的时候再捕获,当代码执行到 closure() ,对值进行捕获,此时i的值为几,捕获到的 i 就是几。

所以下面这个打印的是多少?

var i = 1
let closure = {
    print("closure \(i)")
}

i += 1
closure()//2

i += 1
closure()//3

i += 1
closure()//4

如果在 Swift 中想要捕获当前变量的瞬时值,该怎么操作那?答案是:捕获列表

我们来看下面这段代码

var i = 1
let closure = { [i] in
    print("closure \(i)")
}
i += 1
print("before closure \(i)")
closure()
print("after closure \(i)")

这个打印出来的结果就是: 2, 1, 2 。

捕获列表

默认情况下,闭包表达式从其周围的范围捕获常量和变量,并强引用这些值。可以使用捕获列表来显式控制如何在闭包中捕获值。

在参数列表之前,捕获列表被写为用逗号括起来的表达式列表,并用方括号括起来。如果使用捕获列表,则即使省略参数名称,参数类型和返回类型,也必须使用in关键字。

var age = 0
var height = 0.0

let closure = { [age] in//捕获列表 从上下文中查找同名的变量或常量 age使用变量或常量字面量的值,闭包内部访问的是age的值。闭包内不能修改age 捕获列表默认是let。
    print(age)
    print(height)
}

age = 10
height = 1.85

closure() //输出结果为0, 1.85

创建闭包时,将初始化捕获列表中的条目。对于捕获列表中的每个条目,将常量初始化为在周围范围内具有相同名称的常量或变量的值。例如,在上面的代码中,捕获列表中包含age,但捕获列表中未包含height,这使它们具有不同的行为。

创建闭包时,内部作用域中的 age 会用外部作用域中的 age 的值进行初始化,但它们的值未以任何特殊方式连接。这意味着更改外部作用域中的age的值不会影响内部作用域中的age的值,也不会更改封闭内部的值,也不会影响封闭外部的值。相比之下,只有一个名为height的变量(外部作用域中的height),因此在闭包内部或外部进行的更改在两个地方均可见。

接下来我们在看下面的例子

struct LGTeacher {
    var age: Int
    //test()返回值是一个函数(闭包)
    func test() -> () -> Int{
        return {
            self.age
        }
    }
}

var t = LGTeacher(age: 10)
let closure = t.test()
t.age = 20
print(closure())

结构体 LGTeacher 的实例⽅法 test()  返回⼀个捕获了 self 实例本身的闭包, LGTeacher  为值类型,因此 // 1 ⾏代码执⾏完成后,闭包 closure  复制了⼀份存储在变量 t  中 LGTeacher  实例,那么当存储在变量 t 的 LGTeacher 实例改名为 b 时,闭包 closure 所捕获的 Demo 实例不变,名字仍为 a ,因此输出结果为: 10 。

如果我们把上面的例子换一下:

class LGTeacher {
    var age: Int
    init(age: Int) {
        self.age = age
    }
    func test() -> () -> Int {
        return {
            self.age
        }
    }
}

var t = LGTeacher(age: 10)
let closure = t.test() 
t.age = 20
print(closure())

此时此刻,当前闭包捕获的是 self,他是一个引用类型,所以我们修改当前 age 变量的过程中, closure() 打印出来的也会改变成 20 。这里我们也可以还原当前的捕获捕获过程看一下闭包本质:

闭包本质

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

这里我们也可以通过 IR 的分析来看一下他的底层数据结构,最终我们能得出来这样一个结果

//实例对象内存地址
struct HeapObject {
    var type: UnsafeRawPointer//metadata
    var refcount1: UInt32
    var refcount2: UInt32
}

//IR
struct Box<T> {
    var refcounted: HeapObject
    var value: T                                //捕获的值
}

//闭包本质
//ret { i8*, %swift.refcounted* } %5
//数据结构 : 闭包的执行地址  + 捕获变量堆空间的地址
struct FunctionData<BoxType>{
    var ptr: UnsafeRawPointer                           //内嵌函数的地址
    var captureValue: UnsafePointer<BoxType>//捕获的值
}

struct VoidFunction {
    var f: ()->Int
}
//
//
var f = VoidFunction(f: closure)
//
let ptr = UnsafeMutablePointer<VoidFunction>.allocate(capacity: 1)
ptr.initialize(to: f)
//
let ctx = ptr.withMemoryRebound(to: FunctionData<Box<LGTeacher>>.self, capacity: 1){
    $0.pointee
}

print(ctx.ptr)
print(ctx.captureValue)
print("end")

所以我们在赋值的过程中其实是在传递地址,所以当前的闭包是引用类型。

接下来我们在来看一个例子:

func test() {
    var age = 10
    func closure() -> () -> Int {
        return {
           age += 1
           return age
        }
    }
    let c = closure()
    print(c())
    age = 20
    print(c())
    age = 30
    print(c())
}

test()//11,21,31

这里打印出来的分别是多少?是 10,11,12 还是 11,21,31?我们来运行一下,结果是第二个。这个不就和我们在上面得出来的结论是相反的嘛?我们上面说过闭包是引用类型,所以当前闭包在运行的时候捕获变量10,放到堆区,那么接下来都是针对堆区的值进行修改。 通过 IR 分析,可以看到的是当前在调用闭包的时候确实发生了内存的分配

自动闭包

一种用来把实际参数传递给函数表达式打包的闭包,不接受任何实际参数,当其调用时,返回内部表达式的值。

好处:用普通表达式代替闭包的写法,语法糖的一种。

  • 使用@autoclosure关键字能简化闭包调用形式
  • 使用@autoclosure关键字能延迟闭包的执行

我们先来看下面这个例子

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

debugOutPrint(true, "Application Error Occured")

上述代码会在当前 conditontrue 的时候,打印我们当前的错误信息,也就意味着 false 的时候当前条件不会执行。

如果我们当前的message字符串可能是在某个业务逻辑功能中获取的,比如下面这样写:

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())

这个时候我们会发现一个问题,那就是当前的 conditon,无论是 true 还是 false ,当前的方法doSomething都会执行。如果当前的 doSomething 是一个耗时的任务操作,那么这里就造成了资源浪费。

这个时候我们想到的是把当前的参数修改成一个闭包,

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())

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

//如果是debug模式,则打印错误信息
func debugOutPrint(_ condition: Bool , _ message: @autoclosure () -> String){
    if condition {
        print("lg_debug:\(message)")
    }
}

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

//第二个参数 希望既能接受字符串 又能接受一个闭包表达式 所以需要@autoclosure修饰
debugOutPrint(true, doSomething())
debugOutPrint(true, "Application Error Occured")

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

尾随闭包

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

    //尾随闭包
    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, item2, item3 in
        return (item1 + item2 < item3)
    }

其中闭包表达式是 Swift 语法。使用闭包表达式能更简洁的传达信息。当然闭包表达式的好处有很多:

  • 利用上下文推断参数和返回值类型
  • 单表达式可以隐式返回,既省略 return 关键字
  • 参数名称的简写(比如我们的 $0)
  • 尾随闭包表达式
    var array = [1, 2, 3]
    array.sort(by: {(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 })//省略闭包返回值类型:-> Bool
    array.sort{(item1, item2) in item1 < item2 }//单表达式可以隐式返回,既省略 return 关键字
    array.sort{ return $0 < $1 }//参数名称的简写(比如编译器起好名字 $0)
    array.sort{ $0 < $1 }
    array.sort(by: <)

逃逸闭包

当闭包作为一个实际参数传递给一个函数的时候,并且是在函数返回之后调用,我们就说这个闭包逃逸了。

当我们声明一个接受闭包作为形式参数的函数时,你可以在形式参数前写 @escaping 来明确闭包是允许逃逸的。

闭包生命周期比原函数长。

  • 作为函数的参数传递
  • 当前闭包在函数内部异步执行或者被存储
  • 函数结束,闭包被调用,生命周期结束
  • 不会产生循环引用,函数作用域内释放
  • 编译器更多性能优化 (retain, relsase)
  • 上下文的内存保存再栈上,不是堆上

注意:可选类型默认是逃逸闭包

class LGTeacher{
  var completionHandle: ((Int) -> Void)? //?可选,没有?就必须有一个初始化。闭包作为属性存储

  //@escaping表明闭包是逃逸的
  func makeIncrementer(_ amout: Int, handler: @escaping(Int) -> Void) {
    var runningTotal = 10
    runningTotal += amout
    self.completionHandle = handler

    //异步网络请求时
    DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) {
      handler(runningTotal)
    }
  }

  func dosomething(){
    self.makeIncrementer(10) {
      print($0)
    }
  }
}

let t = LGTeacher()
t.dosomething()
t.completionHandle?(10)

Escaping closure captures non-escaping parameter 'callback'

在 Swift 中,当一个闭包作为参数传递给一个函数,并且在函数结束之后仍然可以被调用时,我们需要在闭包参数前面加上 @escaping 标识符来标记它是一个逃逸闭包。

这个错误的意思是说,你正在尝试在一个逃逸闭包中捕获一个非逃逸参数 callback,这是不允许的。

要解决这个问题,你可以将 callback 参数也标记为逃逸闭包,或者在闭包内部使用 self.callback 来避免捕获它。具体的解决方案取决于你的代码逻辑和需求。

func FaceIDAuthentication(callback: (_ success: Bool)->Void) {
    let context = LAContext()
    context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "请使用Face ID登录") { success, error in
        if success {
            // Face ID验证成功
            callback(true)
        } else {
            // Face ID验证失败
            callback(false)
        }
    }
}

非逃逸闭包

func noEscaping(_ f: () -> Void) {
    f()
}

var age = 10
noEscaping {
  age += 20
}
func testBiBao() {
    var age = 20

    let closure = {
        age += 10
    }

    closure();

    {age += 30}()//非逃逸闭包,没有实例空间的分配

    print(age)//打印60
}

闭包表达式

闭包实现:

{ (param) -> (returnType) in//in之前定义 参数和返回值类型
    //函数体
}

OC 中的 Block 其实是一个匿名函数,所以这个表达式要具备

  • 作用域(也就是大括号)
  • 参数和返回值
  • 函数体(in)之后的代码

1、闭包作为参数

闭包的定义

//定义闭包
var onValueChanged: ((String) -> Void)?

//闭包实现
targetVC.onValueChanged = { [weak self] text in
                           self?.label.text = text
            }

//调用
onValueChanged?(items)
//closure类型是:参数Int 返回值Int的闭包
//=后面是赋值,闭包定义赋值。
var closure: (Int) -> Int = { (age: Int) in
    return age
}
print("closure\(closure(23))")

闭包作为函数的参数

    //闭包作为函数参数
    func test(param : () -> Int){
        print(param())
    }
    var age = 10
    test { () -> Int in
        age += 1
        return age
    }

2、闭包作为可选类型

可以把闭包声明一个可选类型:

var closure1: ((Int) -> Int)?
closure1 = nil

3、闭包作为常量

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

    //闭包 常量
    let closure2: (Int) -> Int
    closure2 = {(age: Int) in
        return age
    }
//    closure2 = {(age: Int) in//不能再改变了
//        return age
//    }

内存

vc.serialNumber = { [self] serialNumberString in
    guard let strongSelf = self else { return }
    ///里面使用strongSelf
}