首页 / Swift 编程语言教程 / 调试基础

Swift 编程语言教程

调试基础

本教程共 93 篇 · 第 91 篇 · 更新于 2026-08-08 · 约 7 分钟阅读

SwiftSwift 编程语言教程调试printassertprecondition断点fatalError

本节目标:学会用打印、断言、强制错误和断点这四类手段,把程序”为什么不对”这件事查清楚。

代码出错是常态,高手和新手的区别往往在排错速度。Swift 给了你一套从”随手看一眼”到”精确暂停”的工具。本章讲概念与用法,不依赖某个具体 IDE,但断点部分会以 Xcode 的体验为例说明思路。

1-1 最朴素的办法:print

想知道某个值是多少,最省事的就是 print。它把内容打到标准输出,你肉眼看。

let total = prices.reduce(0, +)
print("当前总价是 \(total)")

print 适合临时看一眼、确认流程走到哪。它简单、零成本,但缺点也明显:信息一多就刷屏,而且打印语句留在代码里会污染输出。调试完记得清理,别让它陪代码活到上线。

1-2 print 的进阶:debugPrint 与 dump

print 对大多数类型够用,但遇到自定义结构体,它打出来的可能不够”内部”。debugPrint 输出更偏向调试视角,会保留更多结构信息。

dump 则更强:它把值递归地、带缩进地全部展开,特别适合看嵌套的字典、数组、结构体里到底装了什么。

let user = User(name: "小红", age: 18, tags: ["vip", "new"])
dump(user)
Tip

想知道一个复杂对象”肚子里”有什么,dumpprint 清楚得多。排查嵌套数据时,先想到它。

1-3 断言 assert:开发期的哨兵

有些条件你认为”绝不该为假”,比如分数不能负、数组下标不能越界。用 assert 在开发时给这些假设上一道保险。

func validateMarks(_ marks: Int) {
    assert(marks >= 0, "分数不能为负数")
    print("分数有效")
}
validateMarks(345)   // 正常
validateMarks(-39)   // 断言失败,程序中止并打出信息

assert 接受一个布尔条件:为真就继续;为假就终止程序,并打出你给的信息、文件名和行号。它是为”开发期抓 bug”设计的,而且只在非优化构建(Debug)里生效——发布版本(Release)里它会被悄悄忽略,保证运行效率。

Warning

assert 失败是不可恢复的,程序直接挂。它只该用于”发生了就说明代码有 bug”的情况,别拿它做正常的错误流程处理。

1-4 precondition:生产环境也守着

preconditionassert 写法几乎一样,区别在”它不挑构建模式”。哪怕在优化后的 Release 版本里,前置条件失败也会中止程序。

func modulus(_ num: Int, by deno: Int) -> Int {
    precondition(deno != 0, "除数不能为零")
    return num % deno
}

适合哪些场景?比如除零、数组为空却要取第一个元素这类”一旦为假程序就无法正确继续”的硬约束。它比 assert 更严厉:你不希望线上因为分母为零而算出乱七八糟的结果,宁可直接停。

1-5 assert 与 precondition 怎么选

记住一句:开发期想抓、上线后可放过的,用 assert;无论何时都必须成立的底线,用 precondition

常见的坑是反着用——把只在 Debug 生效的 assert 当作生产防护,结果 Release 下检查被优化掉,问题偷偷溜到用户那。涉及安全或数据正确性的硬条件,优先 precondition

1-6 fatalError:明确”这里不该到”

有些代码路径按设计根本不该执行,比如一个 switch 在逻辑上已穷尽,却还是写了 default。这时与其默默返回,不如直接 fatalError,告诉后来人:走到这说明前面的假设错了。

enum Shape { case circle, square }

func area(_ s: Shape) -> Double {
    switch s {
    case .circle: return 3.14
    case .square: return 4.0
    default:
        fatalError("出现了未处理的形状,请检查枚举是否新增了成员")
    }
}

fatalError 永远中止,且编译器知道它之后不会返回,所以你不用在后面补返回值。它和 precondition 一样在生产环境也生效。

1-7 断点:让程序暂停看现场

打印是”事后看字”,断点是”当场叫停”。断点告诉调试器:跑到这一行先别动,把当前的变量、调用栈都亮给我看。

在 Xcode 里,你点编辑器侧边的行号槽就能下断点;运行程序,一旦执行到那行就暂停。此时你可以:

  • 看当前作用域里每个变量的值;
  • 用控制台输入表达式,临时求值;
  • 单步执行(step over / step into),一行一行跟;

这种”暂停 + 窥探”的能力,是排查复杂逻辑不可替代的。打印只能给你预设的那几个值,断点能给你整个现场。

Note

断点不改动源码,是纯调试器的能力。它特别适合”值在某一步突然变错”的诡异 bug——在疑点前后各下一个断点,对比前后状态就能锁定。

1-8 条件断点与动作

断点还能加条件:比如”只有当 i == 100 时才停”,省得循环里手动点了九十九次。Xcode 里右键断点选编辑,填条件表达式即可。

你也能给断点设”动作”:停下来的同时自动执行一条调试命令或打印变量,然后继续跑。这样既有断点的精确,又有打印的连续,适合观察某个值在一长串循环里的变化轨迹。

1-9 调试版与发布版的差异

你写的 assert 在 Debug 构建里生效、在 Release 里被忽略,这背后是”优化级别”的差别。Debug 关优化、保留断言、带调试符号,跑得慢但好查;Release 开优化、砍掉断言、体积小速度快,是给用户用的。

理解这点能解释很多”我本地好好的,上线就崩”或反过来”上线没事,本地老报错”的现象。若怀疑是断言被优化导致行为不同,临时用 preconditionfatalError 验证,它们不挑构建模式。

Note

发布前务必在 Release 配置下也跑一遍关键流程。Debug 里被 assert 拦住的问题,Release 下可能以更隐蔽的方式爆发。

1-10 用日志代替零散 print

print 适合临时看一眼,但项目一大就乱。Swift 配合 Foundation 有更正规的日志:os_log、或 Logger(import OSLog),能分级别(debug/info/error)、带子系统名、还能按级别过滤。

import OSLog
let log = Logger(subsystem: "com.example.app", category: "network")
log.info("请求开始 \(url)")

日志的好处是可开关、可聚合,不会像 print 那样一上线就满屏。调试时打开对应级别,平时静默。把 print 当一次性探针,把 Logger 当长期仪表,各司其职。

1-11 一条成熟的排错流程

遇到 bug,别急着乱改。先想”我能稳定复现吗”,复现不了就先加日志抓现场。能复现后,用 printdump 快速看可疑值,定位大致范围;范围缩小到某几行,上断点单步跟,对比前后状态。最后用 assert/precondition 把”这里不该错”的假设固化进代码,防止复发。

这套流程的核心是”先缩小范围,再精确打击”。很多人一上来就断点乱点、或到处加 print,反而被信息淹没。由粗到细,效率最高。

1-12 小结

调试四件套各有主场:print/dump 随手看现场,最快但别留垃圾;assert 是开发期哨兵、Release 自动隐身;preconditionfatalError 是生产环境的硬底线,绝不姑息;断点则让你在任意一行叫停程序、看清整个状态。排错时先从打印入手快速定位,拿不准就上断点慢慢跟,硬约束用断言守住。到这,工程化的基础工具就齐了,下面两章是速查附录。