现在位置: 首页 > Swift 教程 > 正文

Swift 编码规范

代码写出来是给人看的,顺带才让编译器读懂。

同样的功能,不同的人可以写出完全不同的 Swift 代码,可读性差别巨大。

本文先讲 Apple 官方 API 设计指南的核心要点,再落到命名、格式化这些每天都要打交道的细节,最后用一张反模式对照表把常见坏味道一次讲清。


为什么需要编码规范

规范不是审美问题,它直接影响三件事:读代码的速度、改代码的风险、协作时的沟通成本。

Swift 的语法本身就在鼓励某些写法,比如值类型、可选绑定、协议扩展,顺着语言的设计走,代码会自然变短变清楚。

下面这套约定大部分来自 Apple 官方的 API Design Guidelines,也就是设计 Foundation 和 SwiftUI 时用的同一套标准。


官方 API 设计指南要点

指南的第一条是「调用处的清晰度比简洁更重要」,其余要点都可以看作这条的展开。

命名时不要盯着定义处怎么写最短,而要想象调用处读起来是什么样子。

要点说明示例
调用处清晰让调用点一眼看懂意图,宁长勿含糊articles.filter { !$0.isPublished }
按角色命名用角色而不是类型来命名,避免 stringarray 这类词greeting 而不是 string
参数标签成句参数标签负责让调用处读起来像一句英文move(from:to:)
无副作用加 -ed / -ing返回新值的方法用形容词结尾,原地修改的用动词sorted()sort()
布尔读起来像断言布尔属性能直接放进 if 里读成一句话isPublishedhasCover
文档注释写契约- Parameter- Returns 说清输入输出与前提见下方示例

下面这段代码把「参数标签成句」和「文档注释写契约」放在一起演示。

实例

import Foundation

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)。

不要用匈牙利命名法,把类型写进名字里:strNameiCount 这类写法在 Swift 里没有意义,因为类型系统会告诉你。

布尔属性用 is / has / can 前缀

布尔属性应该读起来像一句断言,放进 if 里就是一句自然语言。

前缀语义示例
is状态是否成立isPublishedisEmpty
has是否拥有某项东西hasCoverhasNext
can是否具备某种能力canUndocanEdit
should是否应当做某事shouldShowAlert

对比一下:if article.published 读起来像名词,if article.isPublished 才像判断。

避免缩写

除社区公认的缩写(如 URL、ID、JSON)外,不要自创缩写。

viewCountvc 清楚,messagemsg 清楚,多打几个字母换来的是别人少猜一次。

下面这段代码把名词命名、布尔前缀、-ed 结尾的排序方法放在一起。

实例

import Foundation

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

实例

import Foundation

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

实例

import Foundation

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 letguard let 都支持。

实例

import Foundation

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 可选类型》。

优先使用值类型

能建模成 structenum 的,就不要用 class

值类型在赋值和传参时天然产生独立副本,不需要担心别人在别处偷偷改掉你的数据。

实例

import Foundation

// 用值类型建模数据:赋值、传参天然隔离,不用担心外部偷偷改掉
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 结构体》里的对比表。

用协议扩展共享实现

多个类型需要同一段实现时,把默认实现写进协议扩展,而不是复制粘贴到每个类型里。

一处修改,所有遵循类型同时生效。

实例

import Foundation

// 用协议扩展给所有遵循类型提供默认实现,避免每个类型重复写
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 = 3let dayCount = 3名字要能读出含义
布尔命名let published = truelet isPublished = true读起来像断言
字符串拼接"你好," + name"你好,\(name)"插值更清晰,也更少出错
空值回退x != nil ? x! : yx ?? y更短且不会崩

常见问题

下面这几个问题在团队落地规范时最常被问到。

缩进到底用 2 个还是 4 个空格?

两种都被广泛使用,官方工具 swift format 默认 2 个,Xcode 默认 4 个。

选一种写进配置文件,再用 swift format lint 在 CI 里卡住,就不用再讨论了。

guard 和 if 该怎么选?

条件不满足就要退出(return、throw、continue、break)时用 guard

两个分支地位对等、都要继续往下走时用 if

必须用 swift format 吗?

不必须,但它随工具链一起提供,零安装成本,规则也可配置。

团队已有别的格式化工具时,保持一致即可,重点是自动化而不是工具本身。

协议扩展里的默认实现为什么不生效?

因为协议扩展成员不走动态派发,只在编译期按静态类型选择实现。

把方法写进协议要求本身,或者用 @objc 标记走消息派发,都能改变这个行为。详见《Swift 协议扩展与面向协议编程》。

规范会不会和性能冲突?

基本不会。这些约定大多只影响可读性,性能上该关注的是算法复杂度和内存分配。

真遇到性能瓶颈时,先测量再优化,不要为了「看起来快」牺牲可读性。