Swift: Swift 闭包基础:闭包表达式与尾随闭包详解
闭包是 Swift 中自包含的函数代码块,可以捕获和存储其所在上下文的变量和常量。本课从闭包表达式开始,逐步掌握 Swift 最灵活的语言特性。
1. 你将学到
- 闭包表达式的简洁语法
- 尾随闭包的写法规则
- 使用简写参数名
$0、$1等 - 闭包的值捕获机制
- 用闭包自定义排序规则
2. 一个数据分析师的真实故事
(1) 痛点:排序逻辑无法复用且代码冗长
Alice 需要对用户数据进行排序——按姓名、按年龄、按注册时间。她一开始的做法是为每种排序写一个独立函数:
SWIFT
func sortByName(_ a: [String: Any], _ b: [String: Any]) -> Bool {
return (a["name"] as! String) < (b["name"] as! String)
}
func sortByAge(_ a: [String: Any], _ b: [String: Any]) -> Bool {
return (a["age"] as! Int) < (b["age"] as! Int)
}
每次排序规则变化就要写新函数,代码膨胀严重。排序逻辑分散在多个函数里,无法在调用时动态指定。
(2) 闭包的解法
Swift 的闭包允许在调用 sorted(by:) 时直接内联定义排序规则:
SWIFT
let sortedByAge = users.sorted { $0["age"] as! Int < $1["age"] as! Int }
let sortedByName = users.sorted { $0["name"] as! String < $1["name"] as! String }
排序逻辑从"写函数 → 传递"变成了"在哪里用,在哪里定义"。
(3) 收益:内联定义,灵活切换
| 维度 | 独立函数 | 闭包内联 |
|---|---|---|
| 代码行数 | 每个排序 3-5 行 | 1 行 |
| 可读性 | 需查找函数定义 | 逻辑在调用处可见 |
| 灵活度 | 需预定义全部策略 | 调用时临时定义 |
| 上下文访问 | 不能捕获外部变量 | 可捕获上下文状态 |
3. 闭包表达式
(1) 基本语法
闭包表达式是编写闭包的一种简洁方式,完整语法如下:
graph LR
A[闭包表达式] --> B["{ (参数) -> 返回值类型 in"]
A --> C[" 语句"]
A --> D["}"]
B --> E["{ (a: Int, b: Int) -> Bool in"]
C --> F[" return a < b"]
D --> G["}"]
| 组成部分 | 是否必需 | 说明 |
|---|---|---|
{ } |
✅ | 闭包使用花括号包裹 |
| 参数列表 | ❌ | 可用简写参数名省略 |
-> 返回值 |
❌ | 可自动推断 |
in |
⚠️ | 有参数或返回值时必须写 |
▶ 示例:闭包表达式三种写法
SWIFT
// ============================================
// sorted(by:) 使用的闭包三种写法
// ============================================
let numbers = [5, 2, 8, 1, 9]
// 写法 1:完整闭包表达式
let sorted1 = numbers.sorted(by: { (a: Int, b: Int) -> Bool in
return a < b
})
// 写法 2:类型推断(省略参数类型和返回值)
let sorted2 = numbers.sorted(by: { a, b in return a < b })
// 写法 3:省略 return(单表达式隐式返回)
let sorted3 = numbers.sorted(by: { a, b in a < b })
print("sorted3: \(sorted3)")
输出:
TEXT 📖 仅展示sorted3: [1, 2, 5, 8, 9]
4. 尾随闭包与简写参数名
(1) 尾随闭包
当闭包是函数的最后一个参数时,可以将闭包移到括号外:
| 写法 | 示例 |
|---|---|
| 标准 | sorted(by: { a, b in a < b }) |
| 尾随 | sorted { a, b in a < b } |
| 多参数尾随 | animate(duration: 1) { ... } |
▶ 示例:尾随闭包简化调用
SWIFT
// ============================================
// 尾随闭包的简洁写法
// ============================================
let scores = [78, 92, 85, 68, 99]
// 标准写法
let ascending1 = scores.sorted(by: { a, b in a < b })
// 尾随闭包写法
let ascending2 = scores.sorted { a, b in a < b }
print("升序:\(ascending2)")
// 多个闭包参数时也可使用
func performRequest(url: String, onSuccess: (String) -> Void, onError: (Error) -> Void) {
// 模拟网络请求
onSuccess("Data received")
}
performRequest(url: "https://api.example.com") { data in
print("Success: \(data)")
} onError: { error in
print("Error: \(error)")
}
输出:
TEXT 📖 仅展示升序:[68, 78, 85, 92, 99] Success: Data received
(2) 简写参数名
Swift 自动为闭包的内联参数提供简写名称 $0、$1、$2...
| 完整写法 | 简写 |
|---|---|
{ a, b in a < b } |
{ $0 < $1 } |
{ name in print(name) } |
{ print($0) } |
{ a, b, c in a + b + c } |
{ $0 + $1 + $2 } |
▶ 示例:简写参数名实战
SWIFT
// ============================================
// 简写参数名的逐级简化
// ============================================
let words = ["banana", "apple", "cherry", "date"]
// 完整写法
let sortedA = words.sorted(by: { (a: String, b: String) -> Bool in
return a < b
})
// 简写参数名 — 最简洁
let sortedB = words.sorted(by: { $0 < $1 })
// 更简单——直接传运算符函数
let sortedC = words.sorted(by: <)
print(sortedC)
输出:
TEXT 📖 仅展示["apple", "banana", "cherry", "date"]🔥 易错: 使用简写参数名时,闭包体只包含一个表达式时可以省略
return。如果有多条语句,必须写return。
5. 值捕获
闭包可以捕获其所在上下文中的常量和变量,即使原作用域已不存在。
graph TB
A[外部函数 makeIncrementer] --> B["声明变量 total = 0"]
A --> C["返回闭包 { total += amount; return total }"]
C --> D[闭包捕获 total 的引用]
D --> E[即使 makeIncrementer 已返回]
E --> F["total 仍然存活在闭包中"]
▶ 示例:闭包捕获上下文变量
SWIFT
// ============================================
// 闭包值捕获——生成累加器
// ============================================
func makeIncrementer(step: Int) -> () -> Int {
var total = 0
let incrementer: () -> Int = {
total += step // 捕获 total 和 step
return total
}
return incrementer
}
let incrementByTwo = makeIncrementer(step: 2)
print(incrementByTwo()) // 2
print(incrementByTwo()) // 4
print(incrementByTwo()) // 6
输出:
TEXT 📖 仅展示2 4 6💡 提示: 值捕获是闭包最强大的特性之一。Swift 会自动管理捕获变量的内存——闭包会持有被捕获变量的强引用,直到闭包本身被释放。
▶ 示例:自定义排序规则
SWIFT
// ============================================
// 使用闭包自定义排序规则
// ============================================
struct Product {
let name: String
let price: Double
let rating: Int
}
let products = [
Product(name: "Laptop", price: 1299, rating: 5),
Product(name: "Mouse", price: 29, rating: 4),
Product(name: "Keyboard", price: 99, rating: 5),
Product(name: "Monitor", price: 399, rating: 3)
]
// 按价格升序
let byPrice = products.sorted { $0.price < $1.price }
print("最便宜:\(byPrice.first!.name)")
// 按评分降序,评分相同按价格升序
let byRatingThenPrice = products.sorted {
if $0.rating != $1.rating {
return $0.rating > $1.rating
}
return $0.price < $1.price
}
print("最佳性价比:\(byRatingThenPrice.first!.name)")
输出:
TEXT 📖 仅展示最便宜:Mouse 最佳性价比:Keyboard
6. 完整示例:用户列表排序器
SWIFT
// ============================================
// 完整示例:用户列表排序器
// 功能:闭包表达式 + 尾随闭包 + 简写参数名 + 值捕获
// ============================================
import Foundation
// 1. 用户模型
struct User {
let name: String
let age: Int
let score: Double
}
let users = [
User(name: "Alice", age: 28, score: 92.5),
User(name: "Bob", age: 22, score: 85.0),
User(name: "Charlie", age: 35, score: 95.5),
User(name: "Diana", age: 19, score: 78.0)
]
// 2. 排序器工厂——捕获排序方向
func makeSorter(ascending: Bool) -> (User, User) -> Bool {
return { ascending ? $0.score < $1.score : $0.score > $1.score }
}
// 3. 使用简写参数名排序
let ascendingSorter = makeSorter(ascending: true)
let sortedAsc = users.sorted(by: ascendingSorter)
print("升序:")
for u in sortedAsc {
print(" \(u.name): \(u.score)")
}
// 4. 尾随闭包内联排序
let sortedDesc = users.sorted { $0.score > $1.score }
print("降序:")
for u in sortedDesc {
print(" \(u.name): \(u.score)")
}
输出:
TEXT 📖 仅展示升序: Diana: 78.0 Bob: 85.0 Alice: 92.5 Charlie: 95.5 降序: Charlie: 95.5 Alice: 92.5 Bob: 85.0 Diana: 78.0
❓ 常见问题
Q 闭包和函数有什么区别?
A 闭包是匿名函数,语法更简洁。函数有名字、可以重复调用;闭包通常用于一次性场景。闭包可以捕获上下文变量,嵌套函数也可以,但普通全局函数不能。
Q 什么时候必须写
in 关键字?A 当闭包表达式声明了参数列表或返回值类型时,必须用
in 分隔声明部分和函数体。如果使用简写参数名(如 $0)且省略全部类型标注,则不需要 in。Q
$0 和 $1 的顺序是怎么确定的?A
$0 是闭包的第一个参数,$1 是第二个,以此类推。顺序由闭包被调用时传入的参数顺序决定,与参数名无关。Q 闭包捕获的值是拷贝还是引用?
A 闭包捕获变量是引用捕获(类似 inout),多个闭包共享同一个变量时,修改会互相影响。捕获常量是值拷贝。
Q 闭包太长影响代码可读性怎么办?
A 如果闭包体超过 5-10 行,建议提取为独立的函数或命名闭包变量,再传入。例如
let sorter = { a, b in ... }; sorted(by: sorter)。📖 小节
- 闭包表达式使用
{ }包裹,参数和返回值在in关键字之前声明 - 尾随闭包允许省略最后一个参数的括号,让调用更自然
- 简写参数名
$0、$1、$2大大简化闭包书写 - 单表达式闭包可以省略
return关键字(隐式返回) - 闭包可以捕获和修改其所在上下文的变量和常量
sorted(by:)是最常见的闭包使用场景之一
📝 作业
- 基础题: 给整数数组
[3, 7, 1, 9, 4, 6]写三种不同写法(完整闭包、类型推断、简写参数名)的sorted(by:)实现降序排列。 - 进阶题: 写一个函数
repeatTask(times:task:),接收一个次数和一个闭包,执行闭包指定次数。然后调用它打印 5 遍 "Hello, Swift!"。 - 挑战题: 实现一个
makeCalculator函数,接收运算符字符串(+、-、*、/),返回对应的(Int, Int) -> Int闭包。然后用该函数创建一个加法闭包和一个乘法闭包,分别计算(10, 5)。