Swift 编码规范
代码写出来是给人看的,顺带才让编译器读懂。
同样的功能,不同的人可以写出完全不同的 Swift 代码,可读性差别巨大。
本文先讲 Apple 官方 API 设计指南的核心要点,再落到命名、格式化这些每天都要打交道的细节,最后用一张反模式对照表把常见坏味道一次讲清。
为什么需要编码规范
规范不是审美问题,它直接影响三件事:读代码的速度、改代码的风险、协作时的沟通成本。
Swift 的语法本身就在鼓励某些写法,比如值类型、可选绑定、协议扩展,顺着语言的设计走,代码会自然变短变清楚。
下面这套约定大部分来自 Apple 官方的 API Design Guidelines,也就是设计 Foundation 和 SwiftUI 时用的同一套标准。
官方 API 设计指南要点
指南的第一条是「调用处的清晰度比简洁更重要」,其余要点都可以看作这条的展开。
命名时不要盯着定义处怎么写最短,而要想象调用处读起来是什么样子。
| 要点 | 说明 | 示例 |
|---|---|---|
| 调用处清晰 | 让调用点一眼看懂意图,宁长勿含糊 | articles.filter { !$0.isPublished } |
| 按角色命名 | 用角色而不是类型来命名,避免 string、array 这类词 | greeting 而不是 string |
| 参数标签成句 | 参数标签负责让调用处读起来像一句英文 | move(from:to:) |
| 无副作用加 -ed / -ing | 返回新值的方法用形容词结尾,原地修改的用动词 | sorted() 与 sort() |
| 布尔读起来像断言 | 布尔属性能直接放进 if 里读成一句话 | isPublished、hasCover |
| 文档注释写契约 | 用 - Parameter、- Returns 说清输入输出与前提 | 见下方示例 |
下面这段代码把「参数标签成句」和「文档注释写契约」放在一起演示。
实例
struct RunoobPoint {
var x: Int
var y: Int
}
/// 计算从 start 到 end 的位移向量。
/// - Parameters:
/// - start: 起点坐标。
/// - end: 终点坐标。
/// - Returns: 由 end 减 start 得到的新坐标。
func move(from start: RunoobPoint, to end: RunoobPoint) -> RunoobPoint {
return RunoobPoint(x: end.x - start.x, y: end.y - start.y)
}
// 调用处读起来就是「从 A 移到 B」,参数标签承担了可读性
let delta = move(from: RunoobPoint(x: 1, y: 1), to: RunoobPoint(x: 4, y: 5))
print("位移:\(delta.x), \(delta.y)")
extension RunoobPoint {
// 参数标签 by 让调用处读成「按 factor 缩放」
func scaled(by factor: Int) -> RunoobPoint {
return RunoobPoint(x: x * factor, y: y * factor)
}
}
print(RunoobPoint(x: 2, y: 3).scaled(by: 4))
位移:3, 4 RunoobPoint(x: 8, y: 12)
注意:Swift 3 起所有参数默认只有局部名,首参数也不例外。需要外部标签时必须显式写 func f(label name: Type),不能再按 Swift 2 的规则调用。
命名约定
命名是规范里收益最高的一项,改一个名字的成本极低,但它会被阅读成百上千次。
大小写与词法
类型、协议、枚举用大驼峰(UpperCamelCase),属性、方法、参数、局部变量用小驼峰(lowerCamelCase)。
不要用匈牙利命名法,把类型写进名字里:strName、iCount 这类写法在 Swift 里没有意义,因为类型系统会告诉你。
布尔属性用 is / has / can 前缀
布尔属性应该读起来像一句断言,放进 if 里就是一句自然语言。
| 前缀 | 语义 | 示例 |
|---|---|---|
| is | 状态是否成立 | isPublished、isEmpty |
| has | 是否拥有某项东西 | hasCover、hasNext |
| can | 是否具备某种能力 | canUndo、canEdit |
| should | 是否应当做某事 | shouldShowAlert |
对比一下:if article.published 读起来像名词,if article.isPublished 才像判断。
避免缩写
除社区公认的缩写(如 URL、ID、JSON)外,不要自创缩写。
viewCount 比 vc 清楚,message 比 msg 清楚,多打几个字母换来的是别人少猜一次。
下面这段代码把名词命名、布尔前缀、-ed 结尾的排序方法放在一起。
实例
struct Article {
let title: String // 名词:描述角色,不叫 titleString
var viewCount: Int // 数量:用 count 结尾
var isPublished: Bool // 布尔:is 前缀
var hasCover: Bool // 布尔:has 前缀
}
let articles = [
Article(title: "Swift 入门", viewCount: 320, isPublished: true, hasCover: true),
Article(title: "Swift 进阶", viewCount: 180, isPublished: false, hasCover: false),
Article(title: "Swift 并发", viewCount: 520, isPublished: true, hasCover: true)
]
// 布尔属性直接当条件读,像一句英文:articles.filter { !$0.isPublished }
let drafts = articles.filter { !$0.isPublished }
print(drafts.map(\.title))
// 返回新数组的排序方法用 -ed 结尾的 sorted(by:)
let byViews = articles.sorted { $0.viewCount > $1.viewCount }
print(byViews.map(\.title))
["Swift 进阶"] ["Swift 并发", "Swift 入门", "Swift 进阶"]
格式化与缩进
格式化是最不值得争论的问题,用工具定死,团队里每个人就都不用再想它。
Swift 工具链自带 swift format,可以格式化代码,也可以只做检查。
$ swift format lint RunoobPoint.swift # 检查风格问题,只报错不改文件 $ swift format format RunoobPoint.swift # 把格式化结果写到标准输出 $ swift format format --in-place RunoobPoint.swift # 原地改写文件
假设源文件里缩进用了 4 个空格、冒号后没有空格,检查会给出这样的提示。
RunoobPoint.swift:1:8: warning: [TypeNamesShouldBeCapitalized] rename the struct 'runoobPoint' using UpperCamelCase; for example, 'RunoobPoint' RunoobPoint.swift:2:1: warning: [Indentation] unindent by 2 spaces RunoobPoint.swift:2:11: warning: [Spacing] add 1 space RunoobPoint.swift:3:1: warning: [Indentation] unindent by 2 spaces RunoobPoint.swift:3:11: warning: [Spacing] add 1 space
格式化后的结果如下,注意它把缩进统一成了 2 个空格,并给冒号后补上了空格。
struct runoobPoint {
var x: Int
var y: Int
}
注意:swift format 默认缩进是 2 个空格,而 Xcode 默认是 4 个空格。两者都不算错,关键是团队统一。可以在项目根目录放一份 .swift-format 固定下来。
实例
"indentation": { "spaces": 4 },
"lineLength": 100
}
上面这份配置把缩进改成 4 个空格,并把单行长度上限设为 100 个字符。
除了缩进,下面这几条是 Swift 社区基本没有争议的排版约定。
| 项目 | 约定 | 示例 |
|---|---|---|
| 大括号 | 开括号不换行,else 与右括号同行 | } else { |
| 冒号 | 冒号紧贴变量名,后面留一个空格 | var x: Int |
| 逗号 | 逗号后面留一个空格,前面不留 | f(a, b, c) |
| 运算符 | 二元运算符两侧各一个空格 | let sum = a + b |
| 分号 | 不写分号结尾 | 直接换行 |
| 导入 | 每个模块一行,按字母排序 | import Foundation |
惯用法
规范解决「怎么写不难看」,惯用法解决「怎么写更像 Swift」。
下面五条是日常代码里出现频率最高的,用熟了代码量会明显下降。
优先使用 let
值一旦确定就不再改变,就用 let 声明。
这不是洁癖:let 让编译器帮你拦住意外的重新赋值,也让读代码的人少一个需要追踪的状态。
用 guard 提前退出
前置条件不满足就立刻返回,主流程留在最外层,避免一层套一层的 if。
实例
struct User {
let name: String
let email: String?
let age: Int
}
// 不推荐:判断一层套一层,主流程被推到最深处
func welcomeNested(_ user: User) -> String {
if user.age >= 18 {
if let email = user.email {
if !email.isEmpty {
return "欢迎,\(user.name)(\(email))"
} else {
return "邮箱为空"
}
} else {
return "缺少邮箱"
}
} else {
return "未成年"
}
}
// 推荐:guard 提前退出,主流程留在最外层
func welcomeGuard(_ user: User) -> String {
guard user.age >= 18 else { return "未成年" }
guard let email = user.email, !email.isEmpty else { return "邮箱无效" }
return "欢迎,\(user.name)(\(email))"
}
let runoob = User(name: "Runoob", email: "dev@runoob.com", age: 20)
print(welcomeNested(runoob))
print(welcomeGuard(runoob))
欢迎,Runoob(dev@runoob.com) 欢迎,Runoob(dev@runoob.com)
两个函数结果完全一样,但 guard 版本没有嵌套,异常分支一眼可见。
循环里同样适用:不满足条件就 continue,循环体不用再包一层 if。
实例
let raw = ["Runoob", "", "www.runoob.com", " ", "RUNOOB"]
// guard 也常用于循环:不满足条件就 continue,避免 if 里再包一大段
for name in raw {
guard !name.trimmingCharacters(in: .whitespaces).isEmpty else { continue }
print("有效条目:\(name)")
}
有效条目:Runoob 有效条目:www.runoob.com 有效条目:RUNOOB
用 if let 同名简写
当可选值变量名和要绑定的常量名相同时,可以省略 = 变量名 这一半。
这个简写从 Swift 5.7 起可用(SE-0345),if let 和 guard let 都支持。
实例
var siteName: String? = "www.runoob.com"
// if let 同名简写:不需要再写 if let siteName = siteName
if let siteName {
print("站点:\(siteName)")
} else {
print("站点未设置")
}
// 优先 let:值不会再变就用 let,编译器会拦住意外赋值
let welcome = "欢迎访问 \(siteName ?? "RUNOOB")"
print(welcome)
// guard let 同样支持同名简写
func show(_ url: String?) -> String {
guard let url else { return "无链接" }
return "链接:\(url)"
}
print(show("https://www.runoob.com"))
print(show(nil))
站点:www.runoob.com 欢迎访问 www.runoob.com 链接:https://www.runoob.com 无链接
更多可选类型的写法见《Swift 可选类型》。
优先使用值类型
能建模成 struct 或 enum 的,就不要用 class。
值类型在赋值和传参时天然产生独立副本,不需要担心别人在别处偷偷改掉你的数据。
实例
// 用值类型建模数据:赋值、传参天然隔离,不用担心外部偷偷改掉
struct RunoobPoint {
var x: Int
var y: Int
}
var a = RunoobPoint(x: 1, y: 2)
var b = a // 值语义:b 是一份独立副本
b.x = 100
print(a.x, b.x)
1 100
什么时候才需要 class?需要身份(identity)、需要继承、或者需要被多处共享同一个可变状态时。详见《Swift 结构体》里的对比表。
用协议扩展共享实现
多个类型需要同一段实现时,把默认实现写进协议扩展,而不是复制粘贴到每个类型里。
一处修改,所有遵循类型同时生效。
实例
// 用协议扩展给所有遵循类型提供默认实现,避免每个类型重复写
protocol Named {
var name: String { get }
}
extension Named {
func greeting() -> String {
return "你好,\(name)"
}
}
struct Site: Named { var name: String }
print(Site(name: "Runoob").greeting())
print(Site(name: "www.runoob.com").greeting())
你好,Runoob 你好,www.runoob.com
注意:协议扩展里的成员默认不走动态派发。如果类型自己也实现了同名方法,通过协议类型调用时可能仍然走到扩展里的版本。这个坑在《Swift 协议扩展与面向协议编程》里详细展开。
反模式对照表
下面这些写法都能编译通过,但会让代码更难读或更脆弱。
把它们当成一份速查清单,写代码时随手对照。
| 场景 | 不推荐 | 推荐 | 原因 |
|---|---|---|---|
| 可变性 | 用 var 声明不再改变的值 | 用 let | 编译器帮忙检查,也表明意图 |
| 前置条件 | 多层 if 嵌套 | guard 提前退出 | 主流程留在最外层 |
| 可选绑定 | if let x = x { | if let x { | 同名简写更短,语义相同 |
| 数据建模 | 能用 struct 却用 class | 优先 struct / enum | 值语义,避免共享可变状态 |
| 代码复用 | 复制粘贴到多个类型 | 抽到协议扩展 | 一处修改,多处生效 |
| 强制解包 | 到处写 ! | if let / guard let / ?? | 避免运行时崩溃 |
| 命名 | let d = 3 | let dayCount = 3 | 名字要能读出含义 |
| 布尔命名 | let published = true | let isPublished = true | 读起来像断言 |
| 字符串拼接 | "你好," + name | "你好,\(name)" | 插值更清晰,也更少出错 |
| 空值回退 | x != nil ? x! : y | x ?? y | 更短且不会崩 |
常见问题
下面这几个问题在团队落地规范时最常被问到。
缩进到底用 2 个还是 4 个空格?
两种都被广泛使用,官方工具 swift format 默认 2 个,Xcode 默认 4 个。
选一种写进配置文件,再用 swift format lint 在 CI 里卡住,就不用再讨论了。
guard 和 if 该怎么选?
条件不满足就要退出(return、throw、continue、break)时用 guard。
两个分支地位对等、都要继续往下走时用 if。
必须用 swift format 吗?
不必须,但它随工具链一起提供,零安装成本,规则也可配置。
团队已有别的格式化工具时,保持一致即可,重点是自动化而不是工具本身。
协议扩展里的默认实现为什么不生效?
因为协议扩展成员不走动态派发,只在编译期按静态类型选择实现。
把方法写进协议要求本身,或者用 @objc 标记走消息派发,都能改变这个行为。详见《Swift 协议扩展与面向协议编程》。
规范会不会和性能冲突?
基本不会。这些约定大多只影响可读性,性能上该关注的是算法复杂度和内存分配。
真遇到性能瓶颈时,先测量再优化,不要为了「看起来快」牺牲可读性。
