Swift 中的逃逸闭包 @escaping

  • Post category:Swift

在 Swift 开发中,闭包(Closure)是非常强大的特性,但当我们处理异步操作时,通常需要用到 逃逸闭包(@escaping)。今天通过几个简单的例子来看它是什么以及如何使用。

1. 什么是逃逸闭包?

如果一个闭包被作为一个参数传递给函数,并且在函数返回之后才被调用,那么这个闭包就称为“逃逸”了(即逃离了函数的作用域)。
在 Swift 中,如果闭包可能在函数执行结束后才执行,我们必须在函数定义的参数类型前标注 @escaping

2. 代码示例

不使用 @escaping(非逃逸,默认)


func doSomething(action: () -> Void) {
    print("函数开始")
    action() // 在函数返回前被执行
    print("函数结束")
}

doSomething {
    print("闭包执行")
}
// 输出: 函数开始 -> 闭包执行 -> 函数结束

使用 @escaping(逃逸)
当我们在网络请求或延迟操作中使用闭包时:


var completionHandlers: [() -> Void] = []

// 需要用 @escaping 标记,因为 action 会在函数外被存储
func perfoemAsyncTask(completion: @escaping () -> Void) {

     //第一种情况:将闭包存储起来,在函数返回后某个时刻调用
    self.completionHandlers = completion

    // 第二种情况:异步操作:延迟 2 秒执行
    print("开始网络请求...")
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        print("网络请求完成")
        completion() // 此时函数 perfoemAsyncTask 已经结束,所以必须是逃逸的
    }
}

如果去掉 @escaping,编译器会报错:“Closure is escaping but it is not marked @escaping”。逃逸闭包意味着闭包可能在任意时间被调用,因此开发者需要确保闭包内部引用的对象在调用时仍然有效,避免访问已释放的内存。

3. 常见使用场景

典型的场景是网络请求、文件 I/O 或耗时任务的回调:


func fetchData(from url: URL, completion: @escaping (Data?, Error?) -> Void) {
    let task = URLSession.shared.dataTask(with: url) { data, response, error in
        // 这个闭包在数据返回后执行,发生在 fetchData 返回之后
        completion(data, error)
    }
    task.resume()
}

将闭包作为属性存储,以便将来调用:

class EventHandler {
    var onEvent: (() -> Void)?

    func registerHandler(_ handler: @escaping () -> Void) {
        onEvent = handler
    }
}

4.使用注意事项

逃逸闭包最常见的陷阱是循环引用。当一个类持有逃逸闭包,而闭包内部又捕获了该类的实例(即 self),就会形成强引用循环。

class ViewController {
    var request: URLSessionDataTask?
    var completion: ((Data?) -> Void)?

    func startRequest() {
        request = URLSession.shared.dataTask(with: URL(string: "https://example.com")!) { [weak self] data, _, _ in
            // 使用 weak self 打破循环引用
            self?.completion?(data)
        }
        request?.resume()
    }
}

在逃逸闭包中捕获 self 时,务必使用 [weak self] 或 [unowned self] 来打破可能的循环引用。