Swift: Swift 输入输出与调试:print 和断点调试技巧
写好代码只完成了一半工作——本课教你用 print、断言和断点找到并修复代码中的错误。
1. 你将学到
- print 函数的高级参数:separator 和 terminator
- debugPrint 和 dump 的区别与使用
- assert 断言和 precondition 前置条件
- 字符串格式化与对齐输出
- Playground 实时预览与断点调试基础
2. 一个自学新手的真实故事
(1) 痛点:代码逻辑正确但结果错误
Mike 是一名自学 Swift 的新手,他在编写一个计算平均成绩的程序:
SWIFT
let scores = [85, 92, 78, 90, 88]
var total = 0
for i in 0...5 {
total += scores[i]
}
let average = total / 5
print("平均分:\(average)")
程序运行后直接崩溃——Mike 搞不懂为什么。他在网上搜了 30 分钟,试了各种修改方案都没用。每次修改后都要重新运行,但问题到底出在哪?
(2) 使用 print 调试法找到问题
Mike 的朋友建议他先用 print 输出中间变量,定位问题:
SWIFT
let scores = [85, 92, 78, 90, 88]
print("数组长度:\(scores.count)") // 输出 5
var total = 0
for i in 0..<scores.count { // 改用 count 而非硬编码
print("正在处理索引 \(i):值 = \(scores[i])")
total += scores[i]
}
print("总分:\(total)")
let average = total / 5
print("平均分:\(average)")
输出:
TEXT 📖 仅展示数组长度:5 正在处理索引 0:值 = 85 正在处理索引 1:值 = 92 正在处理索引 2:值 = 78 正在处理索引 3:值 = 90 正在处理索引 4:值 = 88 总分:433 平均分:86
通过 print 输出中间变量,Mike 立刻发现原来数组索引是 0-4,而他之前用了 0...5(包含了索引 5)。
(3) 收益:掌握调试后解决问题效率对比
| 维度 | 之前(猜错法) | 之后(print 调试) |
|---|---|---|
| 定位问题时间 | 30 分钟起步 | 1-2 分钟 |
| 修改准确率 | 50%(经常改错) | 95% |
| 对代码的理解 | "不知道为什么就崩了" | "清楚每一步的状态" |
| 解决新问题的信心 | 3/10 | 8/10 |
3. print 高级用法
print 是 Swift 最常用的调试工具,但它的参数远不止一个值这么简单。
graph TB
A[print 函数] --> B["items: 要输出的值"]
A --> C["separator: 分隔符"]
A --> D["terminator: 结束符"]
A --> E["to: 输出目标"]
B --> F[多个值逗号分隔]
C --> G["默认空格"]
C --> H["可改为 | 或 , 等"]
D --> I["默认换行"]
D --> J["可改为空字符串"]
| 参数 | 类型 | 默认值 | 作用 |
|---|---|---|---|
items |
Any... |
必填 | 要输出的内容 |
separator |
String |
" " |
多项内容之间的分隔符 |
terminator |
String |
"\n" |
结尾的换行符 |
to |
TextOutputStream |
nil |
输出目标(默认控制台) |
(1) separator 和 terminator
SWIFT
// 默认:空格分隔,换行结尾
print("Hello", "Swift", "World")
// Hello Swift World
// 自定义分隔符
print("Hello", "Swift", "World", separator: ", ")
// Hello, Swift, World
// 自定义结束符(不换行)
print("Loading", terminator: "...")
print("Done")
// Loading...Done
// 组合使用
print("A", "B", "C", separator: " | ", terminator: ".\n")
// A | B | C.
(2) debugPrint 与 dump
debugPrint 输出调试信息(带引号和类型),dump 输出详细结构:
SWIFT
let name = "Alice"
let numbers = [1, 2, 3]
print(name) // Alice
debugPrint(name) // "Alice"
dump(name) // - "Alice"
print(numbers) // [1, 2, 3]
debugPrint(numbers) // [1, 2, 3]
dump(numbers)
// - 1
// - 2
// - 3
| 函数 | 用途 | String 输出 | 数组输出 |
|---|---|---|---|
print |
常规输出 | Alice | [1, 2, 3] |
debugPrint |
调试输出(显式类型信息) | "Alice" | [1, 2, 3] |
dump |
详细结构输出 | - "Alice" | 每行一个元素 |
▶ 示例:格式化日志输出
SWIFT
// ============================================
// 模拟系统日志输出
// 演示 print 高级参数和 debugPrint
// ============================================
let event = "USER_LOGIN"
let user = "Alice"
let statusCode = 200
let duration = 0.045
// 1. 使用 separator 格式化日志
print("[\(event)]", user, "Status: \(statusCode)", separator: " | ", terminator: "")
print(" (\(duration)s)")
// [USER_LOGIN] | Alice | Status: 200 (0.045s)
// 2. 输出表格数据
print()
print("=== Report ===")
print("Item", "Price", "Qty", separator: " | ")
print("-----", "-----", "---", separator: " | ")
print("Book", "12.99", "3", separator: " | ")
print("Pen", "1.50", "10", separator: " | ")
print("Bag", "49.99", "1", separator: " | ")
// 3. debugPrint 用于开发调试
let input: String? = "test"
debugPrint("Debug: input = \(input)")
// "Debug: input = Optional(\"test\")"
输出:
TEXT 📖 仅展示[USER_LOGIN] | Alice | Status: 200 (0.045s) === Report === Item | Price | Qty ----- | ----- | --- Book | 12.99 | 3 Pen | 1.50 | 10 Bag | 49.99 | 1 Debug: input = Optional("test")
4. 断言与 precondition
断言和 precondition 是 Swift 内置的防御性编程工具,在开发阶段及早发现逻辑错误。
graph LR
A[运行时检查] --> B[assert]
A --> C[precondition]
B --> D[仅 Debug 模式]
B --> E[开发阶段发现问题]
C --> F[Debug + Release]
C --> G[不可恢复的错误]
| 函数 | 生效范围 | 用途 | 示例 |
|---|---|---|---|
assert |
仅 Debug | 开发阶段的内部一致性检查 | assert(age > 0) |
assertionFailure |
仅 Debug | 无条件触发断言 | assertionFailure("不应到达") |
precondition |
所有模式 | 前置条件检查 | precondition(!name.isEmpty) |
preconditionFailure |
所有模式 | 无条件终止 | preconditionFailure("致命错误") |
(1) assert 调试断言
assert 只在 Debug 模式下生效,Release 模式下被移除,不影响性能:
SWIFT
func calculateDiscount(price: Double, percent: Double) -> Double {
assert(price > 0, "价格必须大于 0")
assert(percent >= 0 && percent <= 100, "折扣必须在 0-100 之间")
let discount = price * percent / 100.0
return price - discount
}
let finalPrice = calculateDiscount(price: 100.0, percent: 20)
print(finalPrice) // 80.0
// 以下会在 Debug 模式下触发断言失败:
// calculateDiscount(price: -10, percent: 20) // ❌ assert 失败
// calculateDiscount(price: 100, percent: 150) // ❌ assert 失败
(2) precondition 前置条件
precondition 在 Debug 和 Release 模式下都生效——当条件不可能满足时立即终止程序:
SWIFT
func sendEmail(to address: String, message: String) {
precondition(address.contains("@"), "无效的邮件地址:\(address)")
precondition(!message.isEmpty, "消息不能为空")
print("发送邮件到 \(address):\(message)")
}
sendEmail(to: "alice@example.com", message: "Hello!")
// 发送邮件到 alice@example.com:Hello!
// 以下会触发 precondition 失败(所有模式):
// sendEmail(to: "invalid", message: "Hi")
💡 提示: assert 是"内部检查"(确保自己的代码正确),precondition 是"对外契约"(确保调用者满足条件)。Debug 阶段多用 assert,公共 API 用 precondition。
▶ 示例:参数合法性检查
SWIFT
// ============================================
// 用户注册参数检查
// 演示 assert 和 precondition 的使用
// ============================================
import Foundation
func registerUser(name: String, age: Int, email: String) {
// precondition:公共 API 契约(所有模式生效)
precondition(name.count >= 2, "用户名至少 2 个字符")
precondition(age >= 18, "用户必须年满 18 岁")
precondition(email.contains("@"), "邮箱格式无效")
// assert:内部逻辑检查(仅 Debug)
let emailParts = email.split(separator: "@")
assert(emailParts.count == 2, "邮箱应包含一个 @ 符号")
let domain = String(emailParts[1])
assert(domain.contains("."), "邮箱域名无效")
// 正式注册逻辑
print("注册成功:\(name), \(age)岁")
print("确认邮件已发送至:\(email)")
}
// 合法调用
registerUser(name: "Alice", age: 28, email: "alice@example.com")
print("---")
// 触发 precondition 失败的调用(注释掉以避免崩溃)
// registerUser(name: "A", age: 20, email: "test@test.com")
输出:
TEXT 📖 仅展示注册成功:Alice, 28岁 确认邮件已发送至:alice@example.com ---
5. Playground 调试
Playground 提供了比 print 更强大的调试手段,包括实时预览和断点调试。
(1) Playground 实时预览
Playground 的右侧面板会实时显示每行代码的计算结果:
SWIFT
// 在 Playground 中运行,右侧显示每步结果
let name = "Alice" // "Alice"
var score = 0 // 0
score += 85 // 85
score += 92 // 177
let average = score / 2 // 88
graph TB
A[Playground 调试] --> B[实时结果面板]
A --> C[断点调试]
A --> D[值历史记录]
B --> E[每行代码显示结果]
C --> F[暂停/单步/继续]
D --> G[变量值变化曲线]
| 功能 | 如何操作 | 用途 |
|---|---|---|
| 实时预览 | 写代码后右侧自动显示 | 快速查看每步结果 |
| 断点调试 | 点击行号添加断点 | 暂停执行,逐行跟踪 |
| 值历史 | 鼠标悬停变量 | 查看变量随时间的变化 |
| 表达式预览 | 选中代码 | 快速计算选中的表达式 |
(2) 断点调试
在代码行号左侧点击设置断点,程序执行到该行时暂停,可以查看当前所有变量的值:
SWIFT
func calculateTotal(items: [Double], tax: Double) -> Double {
var subtotal = 0.0
// 在此行设置断点
for item in items {
subtotal += item
}
let taxAmount = subtotal * tax
let total = subtotal + taxAmount
return total
}
let cart = [29.99, 49.99, 15.00]
let final = calculateTotal(items: cart, tax: 0.08)
print("Total: $\(final)")
💡 提示: 在 Xcode 或 Playgrounds 中,点击行号即可添加断点。断点触发时,底部调试区会显示所有变量的当前值,你可以选择"继续"或"单步执行"。
▶ 示例:Playground 调试实战
SWIFT
// ============================================
// 练习 Playground 调试技巧
// 将此代码粘贴到 Playground 中运行
// ============================================
import Foundation
// 1. 在下面这行设置断点,观察变量值
let data: [String: Any] = [
"product": "Swift Book",
"price": 39.99,
"quantity": 3,
"inStock": true
]
// 2. 逐步执行下面的代码
let productName = data["product"] as? String ?? "Unknown"
let price = data["price"] as? Double ?? 0.0
let quantity = data["quantity"] as? Int ?? 0
let inStock = data["inStock"] as? Bool ?? false
print("Product: \(productName)")
print("Price: $\(price)")
print("Quantity: \(quantity)")
print("In Stock: \(inStock)")
// 3. 观察条件判断的结果
if inStock {
let totalCost = price * Double(quantity)
print("Total Cost: $\(totalCost)")
} else {
print("Item is out of stock")
}
// 4. 使用 dump 查看复杂数据
print("\n=== Debug Info ===")
dump(data)
输出:
TEXT 📖 仅展示Product: Swift Book Price: $39.99 Quantity: 3 In Stock: true Total Cost: $119.97 === Debug Info === [3 key/value pairs] [0]: (2 elements) .key: "product" .value: "Swift Book" [1]: (2 elements) .key: "price" .value: 39.99 [2]: (2 elements) .key: "quantity" .value: 3
6. 完整示例:成绩分析调试工具
SWIFT
// ============================================
// 成绩分析工具
// 功能:print 调试 + 断言检查 + 格式化输出
// ============================================
import Foundation
// 1. 学生成绩数据
let studentName = "Alice"
let scores = [85.0, 92.0, 78.0, 90.0, 88.0]
// 2. debug 断言检查数据合法性
assert(scores.count > 0, "成绩列表不能为空")
for score in scores {
assert(score >= 0 && score <= 100, "成绩必须在 0-100 之间:\(score)")
}
// 3. 计算统计数据
var total = 0.0
var highest = scores[0]
var lowest = scores[0]
// 添加断点观察循环过程
for (index, score) in scores.enumerated() {
print("[DEBUG] 索引 \(index):\(score)")
total += score
if score > highest { highest = score }
if score < lowest { lowest = score }
}
let average = total / Double(scores.count)
// 4. 格式化输出结果
print(String(repeating: "=", count: 35))
print("学生:\(studentName)")
print(String(repeating: "-", count: 35))
print("成绩", "分数", "评级", separator: " | ")
print(String(repeating: "-", count: 35))
for (index, score) in scores.enumerated() {
let grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "D"
print("科目\(index + 1)", score, grade, separator: " | ")
}
print(String(repeating: "-", count: 35))
print("总分:\(total)")
print("平均分:\(String(format: "%.1f", average))")
print("最高分:\(highest)")
print("最低分:\(lowest)")
print(String(repeating: "=", count: 35))
// 5. precondition 确保结果合理
precondition(average >= 0 && average <= 100, "平均分超出预期范围")
precondition(highest >= lowest, "最高分不应低于最低分")
输出:
TEXT 📖 仅展示[DEBUG] 索引 0:85.0 [DEBUG] 索引 1:92.0 [DEBUG] 索引 2:78.0 [DEBUG] 索引 3:90.0 [DEBUG] 索引 4:88.0 =================================== 学生:Alice ----------------------------------- 成绩 | 分数 | 评级 ----------------------------------- 科目1 | 85.0 | B 科目2 | 92.0 | A 科目3 | 78.0 | C 科目4 | 90.0 | A 科目5 | 88.0 | B ----------------------------------- 总分:433.0 平均分:86.6 最高分:92.0 最低分:78.0 ===================================
❓ 常见问题
Q print 和 debugPrint 到底有什么区别?
A print 输出适合人类阅读的格式(如
Alice),debugPrint 输出更适合调试的格式(如 "Alice" 带引号)。自定义类型可以分别实现 CustomStringConvertible 和 CustomDebugStringConvertible 协议来控制两者的输出。Q assert 在 Release 模式下会被移除吗?
A 是的。assert 只在 Debug 模式(-Onone)下生效,Release 模式下编译器会跳过 assert 的求值和执行。因此不要在 assert 中放有副作用的关键逻辑。
Q 什么场景用 precondition 而不是 assert?
A precondition 在所有模式下都生效,用于不可恢复的致命错误。例如:数组越界前检查、必需参数为 nil、分支逻辑不可能到达。公共 API 的参数校验建议用 precondition。
Q Playgrounds 断点怎么用?
A 在行号左侧点击即可设置断点。程序执行到断点行会暂停,此时鼠标悬停任意变量可查看其值。按继续按钮或 F6 单步执行下一行。
Q 为什么我用了 print 但控制台看不到输出?
A 检查你的 Playground 是否开启了".enableResults"模式。在 Xcode 中,确保调试区(Debug Area)已打开(Shift+Cmd+Y)。在 Swift Playgrounds app 中,输出在底部面板。
📖 小节
- print 的 separator 自定义多项之间的分隔符,terminator 控制结尾字符
- debugPrint 显示带类型信息的输出,dump 显示详细结构
- assert 在 Debug 模式下检查内部逻辑一致性
- precondition 在所有模式下检查不可恢复的前置条件
- Playground 右侧面板实时显示每行代码结果
- 断点调试允许逐行执行代码,观察变量值变化
📝 作业
- 基础题: 用 print 生成一个简单的乘法表(1-3),用 separator 和 terminator 控制格式,输出成表格样式。
- 进阶题: 写一个函数
divide(_ a: Double, by b: Double) -> Double,用 precondition 检查除数不为 0,用 assert 检查结果在合理范围内。然后用 print 格式化输出。 - 挑战题: 模拟一个 ATM 取款机程序。用 assert 检查取款金额为 100 的倍数,用 precondition 检查余额充足。用 print 的 separator 和 terminator 格式化输出交易明细,包括时间、金额、余额。