防御式编程的本质是风险管理,而过度校验(Over-engineering / Defensive Overkill)则是成本管理失控。
两者的平衡点在于:在不可信的边界处严防死守,在可信的契约内保持简洁。
1. 寻找平衡点:边界法则
要找到平衡点,你需要将代码划分为不同的“信任区域”:
外部边界(必须严格防御)
- 来源:用户输入、网络 API、磁盘文件、Obj-C 混编代码。
- 策略:零信任。必须进行完整的数据清洗、类型校验和逻辑验证。
- 目标:确保非法数据在进入核心逻辑之前被拦截。
内部边界(依赖契约)
- 来源:同一个模块内的私有函数、被强类型约束的组件。
- 策略:基于契约(Design by Contract) 。如果函数的参数已经是
Non-optional或特定枚举,就不要再重复校验其内容。 - 目标:通过 Swift 的类型系统(如
struct、enum)来保证安全性,而不是通过if语句。
2. 过度使用防御式编程的副作用
过度校验不仅会让代码变得臃肿,还会带来实实在在的技术债务:
A. 隐藏深层 Bug(静默失败)
如果每个地方都用 if let 或 guard 默默返回而不处理错误,程序虽然不崩溃,但可能在错误的状态下运行。
- 代价:这让调试变得极其困难。你发现数据错了,但不知道是在哪一步被拦截并赋予了默认值。
B. 维护成本激增
当业务逻辑改变时,过度的断言和校验需要同步更新。
- 代价:代码中充满了“防御性噪音”,核心业务逻辑被淹没在海量的校验语句中,新成员接手代码时很难理清主流程。
C. 性能损耗
虽然单个 if 的开销可以忽略不计,但在高频循环(如每秒 60 帧的渲染循环)或处理海量数据流时,重复的字符串匹配和正则校验会造成明显的 CPU 损耗。
D. “掩耳盗铃”式的代码
过度依赖 ?? "" 或空实现来避开崩溃,会产生大量僵尸逻辑,导致线上出现用户点击按钮没反应、界面数据不更新却没有任何错误提示的诡异现象。
3. 最佳实践策略
为了平衡这两者,你可以采用以下原则:
| 维度 | 建议做法 | 避坑指南 |
|---|---|---|
| 错误处理 | 对预期外的错误使用 throws 或 Result | 避免使用全局 try? 忽略所有错误 |
| 解包 | 优先使用强类型属性,让编译器做功课 | 避免在函数内部重复校验已解包的参数 |
| 断言 | 在开发环境使用 assert 捕捉逻辑假设失效 | 不要将 assert 用于处理合法的业务错误(如网络断开) |
| 数据流动 | 在入口处解析(Parse),不要只是校验(Validate) | 避免将原始 Dictionary 在函数间传来传去 |
金句:Parse, don't validate. (去解析,而不是去校验)。如果能把输入解析为一个强类型的结构体,那么后续所有代码就天然拥有了防御能力,无需重复校验。
下一步建议
你可以尝试在项目中引入 SwiftLint。它有一些规则(如 cyclomatic_complexity)能帮你发现那些因为嵌套过多 guard 或 if 而变得过于复杂的函数,这些通常就是防御过度的信号。