Swift: Swift 闭包基础:闭包表达式与尾随闭包详解

闭包是 Swift 中自包含的函数代码块,可以捕获和存储其所在上下文的变量和常量。本课从闭包表达式开始,逐步掌握 Swift 最灵活的语言特性。

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) 基本语法

闭包表达式是编写闭包的一种简洁方式,完整语法如下:

100%
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. 值捕获

闭包可以捕获其所在上下文中的常量和变量,即使原作用域已不存在。

100%
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)

📖 小节


📝 作业

  1. 基础题: 给整数数组 [3, 7, 1, 9, 4, 6] 写三种不同写法(完整闭包、类型推断、简写参数名)的 sorted(by:) 实现降序排列。
  2. 进阶题: 写一个函数 repeatTask(times:task:),接收一个次数和一个闭包,执行闭包指定次数。然后调用它打印 5 遍 "Hello, Swift!"。
  3. 挑战题: 实现一个 makeCalculator 函数,接收运算符字符串(+-*/),返回对应的 (Int, Int) -> Int 闭包。然后用该函数创建一个加法闭包和一个乘法闭包,分别计算 (10, 5)
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏