Swift @escaping标记闭包参数
Swift @escaping标记闭包参数

Swift @escaping标记闭包参数

在 Swift 中,@escaping 用于标记闭包参数,表明该闭包在函数执行完之后 仍可能被调用。换句话说,带有 @escaping 标记的闭包会在函数作用域之外执行,比如在异步操作、回调中使用的闭包通常是 @escaping 的。

闭包的默认行为

在 Swift 中,闭包参数默认是非逃逸的 (@nonescaping)。

这意味着,闭包 只能在函数体内调用,不能超出函数的生命周期,也不能在函数结束后被异步调用或保存到外部变量中。

func performTask(closure: () -> Void) {
    // 只能在函数内部调用
    closure()
    print("函数结束")
}

@escaping 的必要性

默认情况下,闭包参数是非逃逸(non-escaping)的,意味着闭包会在函数执行完毕之前被调用。如果要在函数返回后延迟调用闭包(例如异步网络请求的回调),需要将闭包声明为 @escaping,否则编译器会报错

如果闭包需要在函数结束后执行,必须显式标记为 @escaping。以下场景需要 @escaping:

1、闭包被异步调用

func fetchData(completion: @escaping (String) -> Void) {
    DispatchQueue.global().async {
        let result = "数据加载完成"
        completion(result) // 异步调用闭包
    }
}

原因

fetchData 函数可能会很快返回,但闭包的执行依赖于异步任务完成。

如果闭包是默认的非逃逸 (@nonescaping),它无法被保存到异步任务中。

2、闭包被保存为属性或延迟调用

如果闭包被保存到类的属性或结构体的变量中,需要标记为 @escaping,因为闭包的生命周期会超出当前函数范围。

class TaskManager {
    var onComplete: (() -> Void)? // 持有闭包引用

    func setCompletionHandler(handler: @escaping () -> Void) {
        self.onComplete = handler // 闭包被赋值给类属性,生命周期超出函数
    }
}

3、编译器限制

Swift 编译器会自动检查闭包的作用域。如果尝试在需要逃逸闭包的场景中使用默认的非逃逸闭包,编译器会报错。

func fetchData(completion: () -> Void) {
    DispatchQueue.global().async {
        completion() // 错误:非逃逸闭包不能在函数结束后调用
    }
}

4、@escaping 不会改变闭包的生命周期

闭包本身总是存储在堆内存中。

标记为 @escaping 仅是 明确闭包的作用域可能超出当前函数。

通过 @escaping,Swift 知道它需要额外管理闭包引用,防止在函数结束后错误释放闭包。

5、为什么需要 @escaping?

语义清晰:明确闭包会在函数结束后使用,帮助开发者和编译器理解作用域。

管理生命周期:避免闭包在函数结束时被释放。

防止编译错误:为异步调用、保存闭包引用的场景提供正确的支持。

如果闭包的作用域仅限于函数内部,省略 @escaping 会更高效(不需要额外的内存管理)。但如果闭包需要在函数结束后使用,必须显式声明 @escaping。

使用 @escaping 的场景

1、异步操作:比如网络请求、数据库查询等,需要延迟执行的操作。

2、存储闭包供后续调用:如果需要将闭包存储在一个变量或数组中,以便在函数返回后再调用,这种情况下也需要 @escaping。

这里是一个带有 @escaping 闭包的简单示例:

func fetchData(completion: @escaping (String) -> Void) {
    DispatchQueue.global().async {
        // 模拟耗时的网络请求
        sleep(2) // 模拟延迟
        let result = "网络请求完成,获取数据成功"
        
        // 调用完成后的回调,把结果传给调用者
        completion(result)
    }
}
fetchData { result in
    print(result)
    // 更新 UI,例如显示获取的数据
    print("在主线程更新 UI")
}

在这个例子中:

completion 闭包参数被标记为 @escaping,因为闭包会在 performAsyncTask 返回后被调用。

DispatchQueue.global().async 创建了一个异步任务,2秒后执行 completion 闭包。这种情况下,闭包必须是 @escaping。

@escaping 的必要性

在 Swift 中,闭包默认是 非逃逸(non-escaping)的,这意味着闭包会在函数内部执行完毕,不会离开函数的作用范围。而 @escaping 标记的闭包表示这个闭包可能在函数返回之后才会执行。

由于 completion 闭包是在异步任务内调用的,而这个任务在 fetchData 返回后还在执行中,意味着闭包没有在 fetchData 函数内完成调用,而是“逃逸”到了函数之外。

@escaping 的关键在于函数作用域和生命周期管理。在这里,fetchData函数会立即返回,但异步任务在全局队列中执行,可能需要一段时间才能完成。

因为闭包会在异步任务完成后才被调用,这意味着闭包的生命周期要比 fetchData的生命周期更长。使用 @escaping 明确指出这一点,确保闭包在异步任务中能正常存活并执行,而不会被过早释放。

总结

@escaping 表示闭包逃逸出当前函数,在函数返回之后还可能会被执行。它适用于需要延迟执行、异步操作、或在函数返回后调用闭包的情况。

同时确保闭包生命周期延长,允许在函数返回后调用它,从而避免函数返回后闭包被释放。

示例扩展

可以了解更简单的闭包示例:

struct EditView: View {
    @Environment(\.dismiss) var dismiss
    var location: Location
    var onSave: (Location) -> Void
    @State private var name: String
    @State private var description: String
    
    init(location: Location,name: String, description: String,onSave: @escaping (Location) -> Void) {
        self.location = location
        self.onSave = onSave
        _name = State(initialValue: location.name)
        _description = State(initialValue: location.description)
    }
    
    ...
}

其中声明了一个onSave闭包,表示接受一个Location对象作为参数,但不返回任何值的闭包。

在初始化器中onSave被标记为@escaping:

init(location: Location,name: String, description: String,onSave: @escaping (Location) -> Void) {
        self.location = location
        self.onSave = onSave
        _name = State(initialValue: location.name)
        _description = State(initialValue: location.description)
    }

在初始化器中为什么标记为@escaping?

var onSave: (Location) -> Void

因为onSave没有初始值,所以依赖初始化器,相关文章《@State的初始化机制和Swift的编译器规则

如果不使用@escaping的话,就无法在初始化器中完成赋值。

注意:上图中的onSave闭包作用域只局限在蓝色块中

从外部传入的onSave闭包就像风一样,被吹进了init中,因为是闭包,所以这个风在这个封闭的空间里转圈,直到消失。

也像化学实验一样,将溶液倒入容器中,产生了化学反应,但是化学反应产生的激烈碰撞和刺鼻的气味无法从溶剂中挣脱出来。

闭包传入到初始化器后,任务完成就会自动结束,只会在初始化器内运行。

如果想要通过闭包在初始化器中,完成对视图属性的赋值,就必须要求闭包能够成为逃逸闭包:

因为闭包被标记为@escaping,所以可以在初始化器中,完成对于闭包的赋值。其生命周期不再被严格的局限在当前作用域。

不标记 @escaping,闭包在初始化器中任务完成后就会被销毁,无法保存到属性中或用于延迟执行(如异步操作)。

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注