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) // 220
11. 总结
闭包是 Swift 中非常强大且灵活的特性,掌握闭包是写出简洁、优雅 Swift 代码的关键。本文从闭包的基础语法出发,逐步介绍了语法简化、尾随闭包、值捕获、逃逸闭包和自动闭包等核心概念,并通过实战案例展示了闭包在异步回调和函数式编程中的典型应用。
在实际开发中,建议多使用闭包的简化写法来提升代码可读性,同时注意逃逸闭包中的循环引用问题,合理使用 [weak self] 来避免内存泄漏。

1万+

被折叠的 条评论
为什么被折叠?



