翻译自raywenderlich Swift Style Guide,raywenderlich团队专门制作高质量的编程指导文章和书籍,发现了这篇关于 Swift 代码规范的文章,尝试翻译过来。由于这篇文章内的部分规范只适用于他们指导文章的代码段或DEMO,我做了部分删减。
以下为正文:
正确性 Correctness
确保代码编译没有警告。包括很像 #selector
的样式的使用。
命名 Naming
清晰和一致的代码命名会使得软件代码更加易读。使用 Swift 在API Design Guidelines所描述的规范。
- 力争简洁
- 使用驼峰式
- 类型和协议大写, 其他小写
- 删除不需要的单词
- 使用基于角色的名称,而不是类型
- 补偿若类型信息
- 流利通顺
- 工厂方法以
make
开头 - 根据方法的功能命名
- 使用 -ed, -ing 为非mutating动词方法命名
- 为mutating 方法增加
formX
前缀 - boolean 类型的方法名要像断言
- 名词性的协议定义为 what something is
- 功能性的协议应该以 -able 或 -ible 结尾
- 命名不应让其他人产生困惑
- 勿使用缩写
- 首选方法和属性,然后才是自用函数
- 首字母缩略字全部大写,如:URL
- 为具有同样语义的方法,以相同的命名开始
- 避免返回类型重载
- 良好的参数命名和良好的注释说明
- 为闭包和多参数命名
- 为方法的参数设置默认值
Class 前缀
Swift 元素类型的命名空间是 module 管理的,所以不需再在文件前加入类似 XX
这样的前缀。 如果来个模块里的命名产生了冲突,可以在名字前加上模块名。这样,需要关心的只有模块名了。
1 | import SomeModule |
Delegates
当创建代理方法时,第一个参数要是 delegate 的 source。 (UIKit 里的例子)
推荐:
1 | func namePickerView(_ namePickerView: NamePickerView, didSelectName name: String) |
不推荐:
1 | func didSelectName(namePicker: NamePickerViewController, name: String) |
Use Type Inferred Context
使用编译器的语境推理,让代码更短. (Also see Type Inference.)
推荐:
1 | let selector = #selector(viewDidLoad) |
不推荐:
1 | let selector = #selector(ViewController.viewDidLoad) |
范型
范型参数需要首字母大写,若所定义的范型没有特殊意义,再使用T
、 U
、 V
。
推荐:
1 | struct Stack<Element> { ... } |
不推荐:
1 | struct Stack<T> { ... } |
代码组织
使用 extensions 将具有相同逻辑的代码封装起来。 每个 extension 需要使用 // MARK: -
注释来更好的管理。
协议实现
特别是在为一个模型增加协议实现时,推荐使用 extension。
推荐:
1 | class MyViewController: UIViewController { |
不推荐:
1 | class MyViewController: UIViewController, UITableViewDataSource, UIScrollViewDelegate { |
未使用的代码
未使用的代码,包括Xcode的模版代码和占位注释,以及默认的协议实现,都应该删除。
推荐:
1 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { |
不推荐:
1 | override func didReceiveMemoryWarning() { |
简化 import
简化引用框架,例如如果引用 Foundation
满足需求,就不要引入 UIKit
。
空格
- 方法和(
if
/else
/switch
/while
等) 的{
应在同一行,}
在新的一行。 - 提示: 你可以选中部分代码(或 ⌘A 选中全部)重新整理缩进 然后
Control-I
(或菜单栏 Editor\Structure\Re-Indent)。
推荐:
1 | if user.isHappy { |
不推荐:
1 | if user.isHappy |
- 冒号左侧不应该有空格,但右侧应该有。 除了三元运算符
? :
, 空字典[:]
和#selector
的无参数名语法(_:)
.
推荐:
1 | class TestDatabase: Database { |
不推荐:
1 | class TestDatabase : Database { |
- 每行80个字符左右就应该换行,非硬性规定。
注释
使用Xcode的 Control+Command+/
为代码添加注释。
- 方法注释要说明方法作用、参数描述、返回参数描述
- 涉及与server对接的模块,若有文档的话,附上文档链接
if
等业务逻辑判断的地方要注释说明
Classes 和 Structures
用哪个?
记住, structs 是 值类型. 使用 structs 来定义那些没有特殊身份,只是用来构建数据。 一个[a, b, c] 的 array 和另外一个[a, b, c] 是完全相等并可替换的。
Classes 是 引用类型。用 Class 来定义有特殊身份和生命周期的类型。 例如需要使用 Class 来定义 persion,因为两个人代表不同的物体,不能因为他们有同样的名字和生日,就代表他们是同一个人。但是生日可以用 struct,因为两个生日的年月日相同,两个生日就相同。
Example definition
这是一个Class的定义
1 | class Circle: Shape { |
上面的例子遵守也一下规范
- 指定属性、变量、常量、参数声明和其他类型时冒号右侧加了空格,前面没有。例如:
x: Int
, 和Circle: Shape
。 - 两个参数具有相同的语义,定义在了同一行
- 定义了属性的 getter setter
- 不用加
internal
访问控制,因为默认就是。当重写方法时也不要重复定义访问控制。 - 将额外的方法函数组织到 extension。
- 隐藏实现细节,例子extension中的
centerString
是private
访问控制.
使用 Self
当编译器提示需要 self
时才使用 (在 @escaping
闭包, 或初始化方法中为了和参数区分时)。其他如果编译器不提醒,则删除 self
。
计算属性
为了跟简洁。计算类的 read-only
属性删除 getter
,只有当有 setter
时才用 get
闭包实现。
推荐:
1 | var diameter: Double { |
不推荐:
1 | var diameter: Double { |
Final
为类或其他元素添加 final
关键字会背离设计理念, 除非有明确的意图值得这样。 下面的例子, Box
明确了自定义子类类型时不允许的。 使用 final
来明确清晰的
1 | // 使用Box将泛型转化为引用类型。 |
函数声明
保持简洁的函数声明,并将开括号写在同一行:
1 | func reticulateSplines(spline: [Double]) -> Bool { |
如果函数需要很长的签名,在合适的位置换行,Xcode会自动为换行添加缩进。
1 | func reticulateSplines(spline: [Double], adjustmentFactor: Double, |
Closure Expressions
文档
只在单个闭包的时候才使用 Trailing closure syntax
。给闭包的参数定义好名字。
推荐:
1 | UIView.animate(withDuration: 1.0) { |
不推荐:
1 | UIView.animate(withDuration: 1.0, animations: { |
为内容简单的单表达式闭包(single-expression closures)使用隐式返回(implicit returns)
1 | attendeeList.sort { a, b in |
在trailing closures使用链式编程时需要保持内容简单易读。作者决定是否需要添加换行、空格和何时使用命名参数及匿名参数:
1 | let value = numbers.map { $0 * 2 }.filter { $0 % 3 == 0 }.index(of: 90) |
类型
如果可行,永远使用Swift原生类型,Swift提供了与Objective-C类型的桥接。
推荐:
1 | let width = 120.0 // Double |
不推荐:
1 | let width: NSNumber = 120.0 // NSNumber |
在 Sprite Kit 代码中, 如果可以,使用 CGFloat
来使代码更简洁和避免过多的转换。
常量
用let
定义常量, var
定义变量. 如果一个变量的值在生命周期内没有改变,使用 let
代替 var
。
Tip: 一个好的方法时永远使用let定义所有参数,仅在编译器提示需要使用 var
时才替换。
将常量定义在一个类型里,比定义在实例中跟好。 使用static let
定义一个类型属性作为常量。 这样定义类型属性比全局定义常量好,因为这样可以很好地与实例属性区分:
推荐:
1 | enum Math { |
Note: 使用枚举的好处是不会被实例化和控制命名空间。
不推荐:
1 | let e = 2.718281828459045235360287 // 影响了全局命名 |
静态方法和可变类型属性 Static Methods and Variable Type Properties
静态方法、可变类型的属性和全局函数、全局变量的作用一致,应谨慎使用。 当功能限定了类型或与Objective-C交互时才使用。
Optionals
当变量和函数返回参数可能为nil时,做 optional ?
处理。
使用!
标识隐式变量,除非你知道它在使用前一定会被初始化,像subviews 要在 viewDidLoad
里设置一样.
当访问一个可选型时,只访问一次,或访问多个可选型的话就使用链式。
1 | self.textContainer?.textLabel?.setNeedsDisplay() |
使用 if let
的 unwrap 来进行更多的操作
1 | if let textContainer = self.textContainer { |
当为可选型变量或属性命名时,无需在名字里说明, 像 optionalString
or maybeView
因为已经在类型里做了声明。
使用 if let
unwrap 时, 使用同样的命名,不要使用 unwrappedView
or actualLabel
。
推荐:
1 | var subview: UIView? |
不推荐:
1 | var optionalSubview: UIView? |
赖加载 Lazy Initialization
使用赖加载来更好地管理对象的生命周期,尤其是用在像赖性UIViewController
加载 subview 时。 可以使用 closure { }()
或者是有的工厂方法, 例如:
1 | lazy var locationManager: CLLocationManager = self.makeLocationManager() |
Notes:
- 这里不需要
[unowned self]
,没有产生循环应用。 - Location manager 会跳出隐私授权窗口,所以这样处理很好。
类型推理 Type Inference
类型判断的事情交给编译器,让代码更简洁。 类型推理同样适用于 array 和 dictionary。 当需要时才指明像 CGFloat
或 Int16
等类型的定义。
推荐:
1 | let message = "Click the button" |
不推荐:
1 | let message: String = "Click the button" |
空 Array 和空 Dictionary 的类型定义
对于空的 array 和空 dictionary 使用类型声明。(为长数组和字典也是用类型声明的方式)
推荐:
1 | var names: [String] = [] |
不推荐:
1 | var names = [String]() |
NOTE: 根据这样的规则,定义一个描述清晰的名字比以前更重要。
语法糖 Syntactic Sugar
使用更短版本的语法
推荐:
1 | var deviceModels: [String] |
不推荐:
1 | var deviceModels: Array<String> |
Functions vs Methods
谨慎定义一些没有被类或类型包含的函数。 推荐使用方法代替自由函数,那样会影响阅读性和查找性。
自由函数在当它们与任何类型或实例都不关联时才使用。
Preferred
1 | let sorted = items.mergeSorted() // easily discoverable |
Not Preferred
1 | let sorted = mergeSort(items) // hard to discover |
Free Function Exceptions
1 | let tuples = zip(a, b) // feels natural as a free function (symmetry) |
内存管理 Memory Management
避免循环引用。 分心对象图并用 weak
和 unowned
防止循环引用。 或者, 使用值类型 (struct
, enum
) 完全避免循环引用。详见
扩展对象的生命周期
使用 [weak self]
和 guard let strongSelf = self else { return }
扩展对象的生命周期。 如果不能立刻看出 self
的生命周期,推荐使用[weak self]
,因为 [unowned self]
除非知道self
的生命周期比闭包要长。尤其当扩展生命周期对象为可选型时。
推荐:
1 | resource.request().onComplete { [weak self] response in |
不推荐:
1 | // 如果 self 已经释放 ,就会引起崩溃 |
不推荐:
1 | // 释放可能发生在 updating model 和 updating UI 之间 |
访问控制 Access Control
合适地使用 private
和 fileprivate
使模块更清晰,达到更好的封装. private
和 fileprivate
可以的话推荐使用private
。 如使用 extnsion 可能需要使用 fileprivate
.
当你明确需要开放全部访问权限时才用 open
、 public
、 和 internal
。
将访问控制描述写在前面。 访问权限前面只允许出现static
或像 @IBAction
、 @IBOutlet
和 @discardableResult
。
推荐:
1 | private let message = "Great Scott!" |
不推荐:
1 | fileprivate let message = "Great Scott!" |
控制流 Control Flow
相对 while
,推荐使用 for-in
和 for
循环.
推荐:
1 | for _ in 0..<3 { |
不推荐:
1 | var i = 0 |
guard
避免使用嵌套 if
,使用 guard
来处理多种条件的判断。
推荐:
1 | func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies { |
不推荐:
1 | func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies { |
当遇到多个可选型时,可是使用 guard
或 if let
将条件组合起来,例子:
推荐:
1 | guard let number1 = number1, |
不推荐:
1 | if let number1 = number1 { |
异常处理
异常处理是很有必要的。 通常,通过一行像 return
、 throw
、break
、 continue
、 和 fatalError()
的处理, 避免过多的处理. 如果需要做一些退出处理, 使用 defer
块来做清理工作。
分号 Semicolons
Swift 不再需要分号,只有当一行执行多个操作时才需要,但不要这样做。
推荐:
1 | let swift = "not a scripting language" |
不推荐:
1 | let swift = "not a scripting language"; |
圆括号 Parentheses
圆括号不是必须的,应该删除。
推荐:
1 | if name == "Hello" { |
不推荐:
1 | if (name == "Hello") { |
当然,必要时使用圆括号来增加可读性。
推荐:
1 | let playerMark = (player == current ? "X" : "O") |