Swift 闭包:从基础语法到实战进阶
1. 什么是闭包
闭包是 Swift 中一种自包含的函数代码块,它可以捕获并存储其所在上下文中的常量和变量。简单来说,闭包就是可以像普通函数一样被调用、可以赋值给变量、可以作为参数传递的代码块。
在 Swift 中,全局函数和嵌套函数本质上也是闭包的一种特殊形式。闭包有三种形式:
- 全局函数:有名字但不会捕获任何值的闭包。
- 嵌套函数:有名字且可以捕获其所在函数体内值的闭包。
- 闭包表达式:轻量级语法,可以捕获其所在上下文中值的匿名闭包。
2. 闭包基础语法
闭包表达式的基本语法格式如下:
{ (参数列表) -> 返回类型 in 函数体代码 }下面通过一个简单的排序示例来理解闭包的基本用法。Swift 标准库中的sorted(by:)方法接收一个闭包作为参数,用于定义排序规则:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"] // 使用普通函数作为排序规则 func backward(_ s1: String, _ s2: String) -> Bool { return s1 > s2 } var reversedNames = names.sorted(by: backward) // 使用闭包表达式 reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 })上面的闭包表达式完整地写出了参数类型和返回类型,但 Swift 提供了多种简化写法,让代码更加简洁。
3. 闭包语法简化
3.1 根据上下文推断类型
因为sorted(by:)方法的参数类型是已知的,Swift 可以推断出闭包的参数类型和返回类型,因此可以省略类型声明:
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 })3.2 单表达式隐式返回
如果闭包体只有一行表达式,可以省略return关键字:
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 })3.3 简写参数名
Swift 自动为闭包提供简写参数名$0、$1、$2等,可以省略参数列表和in关键字:
reversedNames = names.sorted(by: { $0 > $1 })3.4 运算符方法
对于字符串比较这种场景,可以直接传入运算符函数,因为>本身就是一个接收两个参数并返回 Bool 的函数:
reversedNames = names.sorted(by: >)4. 尾随闭包
当闭包是函数的最后一个参数时,可以将闭包写在函数调用的括号之外,这种写法称为尾随闭包。它让代码在函数较长时更加易读:
// 不使用尾随闭包 reversedNames = names.sorted(by: { $0 > $1 }) // 使用尾随闭包 reversedNames = names.sorted() { $0 > $1 } // 如果闭包是唯一参数,可以省略函数调用的括号 reversedNames = names.sorted { $0 > $1 }尾随闭包在 SwiftUI 中非常常见,例如按钮的点击事件处理:
Button("点击我") { print("按钮被点击了") }5. 捕获值
闭包可以在其函数体内捕获并存储所在上下文中定义的常量和变量。即使这些常量和变量的原始作用域已经不存在,闭包仍然可以在其函数体内引用和修改它们。
func makeIncrementer(forIncrement amount: Int) -> () -> Int { var runningTotal = 0 func incrementer() -> Int { runningTotal += amount return runningTotal } return incrementer } let incrementByTen = makeIncrementer(forIncrement: 10) print(incrementByTen()) // 输出 10 print(incrementByTen()) // 输出 20 print(incrementByTen()) // 输出 30上面的示例中,嵌套函数incrementer捕获了runningTotal和amount两个变量。每次调用incrementByTen时,runningTotal都会在上一次的基础上继续累加。
需要注意的是,如果闭包捕获的是变量,它可以在闭包内部修改该变量的值;如果捕获的是常量,则只能读取不能修改。
6. 闭包是引用类型
在 Swift 中,函数和闭包都是引用类型。当你把一个闭包赋值给另一个变量时,实际上是将同一个闭包的引用赋值过去,两个变量指向同一个闭包:
let incrementByTen = makeIncrementer(forIncrement: 10) let alsoIncrementByTen = incrementByTen print(incrementByTen()) // 输出 10 print(alsoIncrementByTen()) // 输出 20,两个变量共享同一个闭包7. 逃逸闭包与非逃逸闭包
7.1 非逃逸闭包
默认情况下,闭包是非逃逸的。非逃逸闭包在函数返回之前就会被调用,不会在函数返回后继续存在。由于不需要考虑生命周期问题,非逃逸闭包在使用时更加高效:
func performOperation(_ operation: () -> Void) { // 闭包在函数体内被调用 operation() } performOperation { print("这是一个非逃逸闭包") }7.2 逃逸闭包
当闭包需要在函数返回之后才被调用时,需要在参数类型前加上@escaping关键字,将其标记为逃逸闭包。最常见的场景是异步网络请求:
var completionHandlers: [() -> Void] = [] func downloadData(completion: @escaping () -> Void) { // 将闭包存储到数组中,函数返回后闭包仍然存在 completionHandlers.append(completion) // 模拟异步操作 DispatchQueue.global().async { // 模拟网络请求耗时 sleep(2) // 在异步任务完成后调用闭包 completion() } } downloadData { print("数据下载完成") }逃逸闭包需要注意循环引用问题。如果闭包内部捕获了self,而self又持有该闭包,就会形成循环引用导致内存泄漏。此时需要使用[weak self]或[unowned self]来打破循环:
class DataLoader { var data: String = "" func loadData() { downloadData { [weak self] in guard let self = self else { return } self.data = "加载完成" print(self.data) } } }8. 自动闭包
自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。自动闭包不接受任何参数,当它被调用时,会返回包装在其中的表达式的值。使用@autoclosure关键字来标记:
func logIfTrue(_ condition: @autoclosure () -> Bool) { if condition() { print("条件为真") } } // 调用时不需要写花括号,直接传入表达式 logIfTrue(2 > 1) // 输出 "条件为真"自动闭包的一个典型应用是 Swift 中的assert函数。它只在调试模式下执行,并且只有在条件为假时才计算表达式,从而避免不必要的性能开销。
9. 实战案例:使用闭包实现回调
下面通过一个完整的示例来展示闭包在真实项目中的应用。假设我们要实现一个简单的网络请求工具类:
import Foundation struct User { let id: Int let name: String } enum NetworkError: Error { case invalidResponse case serverError(code: Int) } class APIClient { // 使用逃逸闭包处理异步回调 func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) { // 模拟网络请求 DispatchQueue.global().asyncAfter(deadline: .now() + 1) { // 模拟请求成功 let user = User(id: id, name: "用户\(id)") completion(.success(user)) // 模拟请求失败 // completion(.failure(NetworkError.serverError(code: 500))) } } } // 使用示例 let client = APIClient() client.fetchUser(id: 1001) { result in switch result { case .success(let user): print("获取用户成功:\(user.name)") case .failure(let error): print("获取用户失败:\(error.localizedDescription)") } }10. 实战案例:高阶函数与闭包
Swift 的数组提供了map、filter、reduce等高阶函数,它们都接收闭包作为参数,是闭包最广泛的应用场景之一。
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // map:对每个元素进行转换 let squaredNumbers = numbers.map { $0 * $0 } print(squaredNumbers) // [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] // filter:筛选符合条件的元素 let evenNumbers = numbers.filter { $0 % 2 == 0 } print(evenNumbers) // [2, 4, 6, 8, 10] // reduce:将所有元素合并为一个值 let sum = numbers.reduce(0) { $0 + $1 } print(sum) // 55 // 链式调用 let result = numbers .filter { $0 % 2 == 0 } .map { $0 * $0 } .reduce(0, +) print(result) // 22011. 总结
闭包是 Swift 中非常强大且灵活的特性,掌握闭包是写出简洁、优雅 Swift 代码的关键。本文从闭包的基础语法出发,逐步介绍了语法简化、尾随闭包、值捕获、逃逸闭包和自动闭包等核心概念,并通过实战案例展示了闭包在异步回调和函数式编程中的典型应用。
在实际开发中,建议多使用闭包的简化写法来提升代码可读性,同时注意逃逸闭包中的循环引用问题,合理使用[weak self]来避免内存泄漏。
