Swift 与 Objective-C

[Swift 基础 #4] 正确使用 Swift guard:用提前退出压平毁灭金字塔

Swift guard 会先处理失败条件,让正常流程保持平坦。通过可执行示例确认它与 if 不同的编译器保证、可选绑定的有效范围,以及 return、throw、continue 的选择标准。

6 分钟阅读
[Swift 基础 #4] 正确使用 Swift guard:用提前退出压平毁灭金字塔 封面图

代码审查中经常会出现这样的意见:“这个改成 guard 吧。”既然用 if 也能完全一样地运行,为什么还要改?这看起来像是偏好问题,但实际上 guard 是 Swift 在语言层面用来推动特定代码风格的语法:提前退出,以及左对齐的正常路径。

这是 Swift 基础系列的第 4 篇。在可选值篇中,我曾简要介绍过 guard let 这一解包工具。这一次我们深入了解 guard 本身:它与 if 有什么不同、编译器能保证什么,以及什么时候应该使用 if 而不是 guard。

如果想重新了解值不存在的状态,建议先阅读 Swift 可选值的本质与解包判断标准

问题场景 — 毁灭金字塔

先来看 guard 出现之前的代码。假设这是一个注册用户的函数,需要检查很多内容:输入是否为 nil、格式是否正确,以及是否同意条款。

func signUp(email: String?, password: String?, agreed: Bool) {
    if let email = email {
        if isValidEmail(email) {
            if let password = password {
                if password.count >= 8 {
                    if agreed {
                        // 真正想做的事
                        createAccount(email, password)
                    } else {
                        showError("需要同意条款")
                    }
                } else {
                    showError("密码太短")
                }
            } else {
                showError("请输入密码")
            }
        } else {
            showError("不是有效的电子邮件格式")
        }
    } else {
        showError("请输入电子邮件")
    }
}

缩进有五层。这种形状被称为毁灭金字塔,不仅外观难看,还会带来实际的阅读成本。函数的核心工作(createAccount)被埋在最深处,而每个检查的失败处理(else)在视觉上都离对应条件很远。要回答“密码太短时会怎样?”,必须一边滚动,一边用眼睛匹配括号。

guard 的解法 — 先处理失败,让核心逻辑位于平地

用 guard 重写同一个函数后,会变成这样。

func signUp(email: String?, password: String?, agreed: Bool) {
    guard let email, isValidEmail(email) else {
        return showError("请检查电子邮件")
    }
    guard let password, password.count >= 8 else {
        return showError("请检查密码 (8 个字符以上)")
    }
    guard agreed else {
        return showError("需要同意条款")
    }

    createAccount(email, password)
}

结构反转了。每个条件与对应的失败处理被放在一起,通过所有检查的核心逻辑则没有额外缩进,平坦地位于函数底部。读者的视线只需从上到下流动一次。“前置条件,然后是核心逻辑”这一函数的逻辑结构,现在与代码的视觉结构一致了。

这种风格的优势通常被称为让正常路径左对齐。正常流程始终位于 0 级缩进,只有异常情况才会进入代码块。即使函数变长,也会保持“沿着最左边阅读就能看到正常场景”的一致性。

通过检查后,核心逻辑会位于没有缩进的平地上
通过检查后,核心逻辑会位于没有缩进的平地上

与 if 的真正区别 — 编译器强制的两项保证

接下来可能会有人问:“if 不是也能提前退出吗?”没错,可以写成 if email == nil { return }。但 guard 不只是反转后的 if,它还提供了两项由编译器强制保证的特性。

**第一,else 代码块必须离开作用域。**在 guard 的 else 中,必须使用 return、throw、continue、break 或 fatalError 离开当前作用域,否则就会编译错误。用 if 编写检查时,可能只检查条件却忘记 return;而 guard 会在编译时捕获这个错误。“如果执行通过了这个位置,条件就为真”不再是约定,而是保证。

**第二,解包后的值会在 guard 以下的整个范围内继续有效。**if let 的绑定只在 if 代码块内有效,而 guard let 的绑定可以在 guard 语句之后的整个剩余作用域中使用。通过检查的值可以一直带到函数末尾,因此不会产生“单独解包、单独使用”的别扭感。

这两项保证结合起来后,guard 也会承担文档的作用。函数开头的一组 guard 用来声明“这个函数的前置条件列表”。仅靠函数签名无法表达的契约,可以从函数体的第一行读到。

该使用 if 而不是 guard 的场景

那么,是否应该把所有 if 都改成 guard?不是。区分标准很明确:guard 用于“不是这样就无法继续”的前置条件,if 用于“这种情况这样做,另一种情况那样做”的分支。

// if适合使用 if 的场景 — 两条路径都是正常流程
if user.isPremium {
    showPremiumBadge()
} else {
    showUpgradeButton()
}

不是 premium 并不是失败。如果两个分支都会继续执行,属于正常场景,就应该使用 if。用 guard 编写反而会传达“不是 premium 就是不正常”的错误信号。

相反,如果检查失败就会结束函数,即使只有一个检查项,也适合使用 guard。语法选择本身就是意义传达。读者看到 guard 时会期待“这是前置条件”,看到 if 时会期待“这是分支”;可读性的本质就是符合这些预期。

还要指出一种反模式:在 guard else 代码块中填入复杂逻辑。如果开始在 else 中尝试恢复、修改状态或执行长时间处理,就破坏了 guard“失败后快速结束”的约定。当 else 超过三行时,最好把它视为重新审视设计的信号。如果失败处理复杂到这种程度,那它就不是前置条件,而是独立的分支,或者应该抛出错误交给调用方处理。

前置条件用 guard,两条都是正常路径用 if
前置条件用 guard,两条都是正常路径用 if

循环与异步代码中的 guard

guard 也可以在函数之外使用。在循环中,它会与 continue 搭配,用来表达“跳过这一项”。

for item in items {
    guard item.isValid else { continue }
    guard let url = item.downloadURL else { continue }
    process(url)
}

过滤条件较多时,guard 能让 for 的主体保持平坦。当然,如果条件很简单,for item in items where item.isValid或 compactMap 可能更加简洁。一个 where 子句就能完成时使用 where;如果混合了解包且条件分为多个阶段,guard 会更方便。

在异步代码中,与闭包部分常见的 [weak self] 组合,实际上已经成为惯用写法。

fetchData { [weak self] data in
    guard let self else { return }
    self.update(with: data)
}

“如果 self 已经被释放,就什么都不做”这一前置条件在第一行处理,剩余代码则在 self 已获得的平地上继续执行。这正是 guard 的提前退出理念与内存管理相遇的地方。

通过直接运行确认的结果

在 Apple Swift 6.3.3 中,我创建了一个接收 String? 输入的函数,在 guard 的 else 中拒绝 nil;如果有值,则在下一行使用绑定后的字符串。

guard=rejected,accepted:devpaw

这段输出也符合我在代码审查中推荐 guard 的标准。如果输入失败就结束当前路径,只有通过检查的值会在主体中继续使用,那么就应该用 guard。如果真和假两边都是正常的业务流程,就不要仅仅因为看起来更平坦而改成 guard,应当保留 if。

总结

  • guard 是在语言层面支持提前退出的语法,它把毁灭金字塔翻转为“列出前置条件 + 平坦的核心逻辑”结构。
  • 它与 if 的区别在于编译器保证:else 必须离开作用域,绑定会在后续整个作用域中保持有效。
  • 选择标准:失败后无法继续的前置条件用 guard,两边都是正常流程的分支用 if。语法选择本身就是意义传达。
  • else 代码块变长,说明 guard 可能被错误使用。如果失败处理很复杂,就应该用分支或抛出错误来解决。

最后一句提到了“抛出错误”。下一篇正是这个主题:throws、do-catch 和 try 的三种变体,以及 Result,完整梳理 Swift 错误处理的地图。

延伸阅读

来源与验证