阅读视图

发现新文章,点击刷新页面。

Swift 中的async和await

asyncawait 是 Swift 5.5 引入的用于处理异步编程的关键字,它们使得处理异步任务变得更加简单和直观。它们提供了一种新的方式来管理异步操作,相比传统的回调函数或闭包,async/await 更接近同步代码的写法,让代码更加易读和可维护。

1. 什么是异步编程

异步编程是指程序在执行任务时,不需要等待任务完成才能继续执行其他任务。传统的同步编程方式会导致程序等待某个操作完成(比如网络请求、磁盘读写等),直到任务完成后才会继续执行,可能会造成性能瓶颈。异步编程允许程序在等待某个操作时去执行其他任务,从而提高效率。

2. asyncawait 的基本概念

  • async: 用于标记一个函数为异步函数。异步函数会返回一个 Task 类型,可以在执行时暂停,直到结果准备好。
  • await: 用于暂停函数的执行,直到异步操作完成并返回结果。

3. 如何使用 asyncawait

3.1 标记异步函数

首先,你需要使用 async 关键字来定义一个异步函数,表示这个函数包含异步操作,并且可能需要一些时间来执行。

func fetchData() async -> String {
    // 模拟网络请求
    return "Data fetched"
}

在上面的例子中,fetchData() 是一个异步函数,它返回一个字符串。函数内部的操作可能会是一个耗时操作,虽然这里没有具体的异步代码,但它表示这段代码可以用异步方式进行处理。

3.2 调用异步函数

要调用一个异步函数,你必须在一个异步上下文中使用 await 关键字。await 会暂停当前的代码执行,直到异步函数返回结果。

func exampleUsage() async {
    let result = await fetchData()  // 等待异步函数返回结果
    print(result)
}

3.3 异步任务的创建

你可以使用 Task 来创建异步任务。Task 允许你在异步上下文之外执行异步代码。

Task {
    let result = await fetchData()
    print(result)
}

Task 是一个异步任务,它会自动创建一个新的异步上下文来执行异步代码。这是非常有用的,当你需要在不直接处于异步函数内部的地方执行异步代码时。

4. asyncawait 与传统的闭包回调对比

在传统的异步编程中,我们可能会使用闭包来处理异步操作的回调:

func fetchData(completion: @escaping (String) -> Void) {
    // 模拟网络请求
    DispatchQueue.global().async {
        let data = "Data fetched"
        completion(data)
    }
}

在上面的代码中,fetchData 接受一个回调闭包 completion,并通过它返回结果。调用时我们需要手动处理回调:

fetchData { result in
    print(result)
}

而使用 asyncawait 后,你可以这样写:

func fetchData() async -> String {
    // 模拟网络请求
    return "Data fetched"
}

Task {
    let result = await fetchData()
    print(result)
}

相比使用闭包,asyncawait 更加简洁、直观。

5. 错误处理

在异步函数中,错误处理通常使用 do-catch 语句来处理。你可以在异步函数中抛出错误,并使用 await 来捕获和处理它们。

enum DataError: Error {
    case invalidData
}

func fetchData() async throws -> String {
    // 模拟可能抛出错误的网络请求
    let success = false
    if !success {
        throw DataError.invalidData
    }
    return "Data fetched"
}

func exampleUsage() async {
    do {
        let result = try await fetchData()
        print(result)
    } catch {
        print("Error: \(error)")
    }
}

在上述代码中,fetchData() 可能会抛出 DataError.invalidData 错误,调用它时需要使用 try await 来捕获和处理错误。

6. 并发执行多个异步任务

你可以使用 async let 来并行执行多个异步任务,并且在最后获取它们的结果。这是一个非常强大的功能,尤其是当你需要同时处理多个异步操作时。

func fetchData1() async -> String {
    return "Data 1 fetched"
}

func fetchData2() async -> String {
    return "Data 2 fetched"
}

func exampleUsage() async {
    async let data1 = fetchData1()  // 异步并发任务
    async let data2 = fetchData2()  // 异步并发任务
    
    let result1 = await data1  // 等待结果
    let result2 = await data2  // 等待结果
    
    print(result1)
    print(result2)
}

在上面的代码中,data1data2 会并行执行,最终我们使用 await 来获取它们的结果。

7. 总结

asyncawait 是处理异步操作的核心工具,它们通过提供一种类似同步代码的结构,使得异步编程更加简单和清晰。使用这些特性,你可以:

  • 简化代码,使其更加易读和维护。
  • 避免回调地狱(callback hell)和嵌套的闭包。
  • 更容易进行错误处理。
  • 使并发执行变得简单,减少手动管理异步任务的复杂度。

Swift 中重要特性——逃逸闭包@escaping

@escaping 是 Swift 中一个非常重要的特性,通常用于闭包(closure)的参数,尤其是在处理异步操作或回调时。它用于标记闭包参数“逃逸”出了函数的作用域,即闭包的生命周期超出了函数的执行范围。

为什么需要 @escaping

在 Swift 中,闭包默认是非逃逸(non-escaping)的,也就是说,闭包只能在函数调用过程中执行,并且不会保存到外部的变量或常量中。这样做的目的是提高性能,因为闭包不需要被保持,编译器可以进行优化。

然而,在处理异步操作时,比如网络请求或定时器,我们需要将闭包传递出去,让它在函数执行完毕后(甚至在函数退出后)继续执行。这时,闭包需要逃逸出函数的作用域,这时就需要使用 @escaping 来显式标记这个闭包参数。

@escaping 的作用

@escaping 表示闭包会逃逸出函数的作用域,可以在函数返回后被执行。这通常用于处理异步回调或者其他延迟执行的场景。

关键点

  • 非逃逸闭包(non-escaping closure):闭包只能在函数内部执行,并且会在函数返回前执行完毕。默认情况下,函数的闭包参数是非逃逸的。
  • 逃逸闭包(escaping closure):闭包可以在函数返回之后仍然执行,通常用于异步回调。

例子:非逃逸闭包

如果没有使用 @escaping,闭包是非逃逸的,不能存储到函数外部。

func performTask(task: () -> Void) {
    task()  // 这里闭包被执行并且在函数内完成
}

例子:逃逸闭包

当闭包需要在函数执行完毕后仍然执行,通常会标记为 @escaping。最典型的例子是异步操作,例如网络请求或定时器。

func fetchData(completion: @escaping (Data?) -> Void) {
    DispatchQueue.global().async {
        // 模拟网络请求
        let data = Data()
        completion(data)  // 闭包会在函数返回后执行
    }
}

在上面的例子中,completion 闭包会逃逸出 fetchData 函数,因为它是在一个异步线程中执行的,函数返回后闭包才会被调用。

逃逸闭包与内存管理

由于逃逸闭包的生命周期可能超过函数的执行时间,它可能会导致内存管理问题。逃逸闭包会被持有到函数执行完成后,因此需要特别小心避免强引用循环(retain cycles)。

通常,为了避免强引用循环,我们会将闭包声明为 weakunowned,从而防止闭包持有对象的强引用。

使用 weakunowned 避免循环引用

func fetchData(completion: @escaping (Data?) -> Void) {
    DispatchQueue.global().async { [weak self] in
        // 使用 weak 或 unowned 防止循环引用
        guard let self = self else { return }
        let data = Data()
        completion(data)
    }
}

使用 @escaping 的实际场景

@escaping 主要用于异步操作或回调函数,它的作用是使闭包可以在函数执行完毕后,甚至在函数返回后继续执行。

  1. 网络请求回调:在网络请求成功或失败后执行回调操作。
  2. 定时器回调:在定时器触发时执行闭包操作。
  3. UI 更新回调:例如,在多线程中更新 UI,闭包可能需要在主线程执行。

总结

  • @escaping 标记闭包为逃逸闭包,即它可能在函数返回后被调用。
  • 逃逸闭包通常用于处理异步操作、回调等情况。
  • 逃逸闭包的生命周期可能会超过函数的作用域,因此需要注意内存管理,避免出现强引用循环。
❌