Al03's blog

Swift Style Guide

翻译自raywenderlich Swift Style Guideraywenderlich团队专门制作高质量的编程指导文章和书籍,发现了这篇关于 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
2
3
import SomeModule

let myClass = MyModule.UsefulClass()

Delegates

当创建代理方法时,第一个参数要是 delegate 的 source。 (UIKit 里的例子)

推荐:

1
2
func namePickerView(_ namePickerView: NamePickerView, didSelectName name: String)
func namePickerViewShouldReload(_ namePickerView: NamePickerView) -> Bool

不推荐:

1
2
func didSelectName(namePicker: NamePickerViewController, name: String)
func namePickerShouldReload() -> Bool

Use Type Inferred Context

使用编译器的语境推理,让代码更短. (Also see Type Inference.)

推荐:

1
2
3
4
let selector = #selector(viewDidLoad)
view.backgroundColor = .red
let toView = context.view(forKey: .to)
let view = UIView(frame: .zero)

不推荐:

1
2
3
4
let selector = #selector(ViewController.viewDidLoad)
view.backgroundColor = UIColor.red
let toView = context.view(forKey: UITransitionContextViewKey.to)
let view = UIView(frame: CGRect.zero)

范型

范型参数需要首字母大写,若所定义的范型没有特殊意义,再使用TUV

推荐:

1
2
3
struct Stack<Element> { ... }
func write<Target: OutputStream>(to target: inout Target)
func swap<T>(_ a: inout T, _ b: inout T)

不推荐:

1
2
3
struct Stack<T> { ... }
func write<target: OutputStream>(to target: inout target)
func swap<Thing>(_ a: inout Thing, _ b: inout Thing)

代码组织

使用 extensions 将具有相同逻辑的代码封装起来。 每个 extension 需要使用 // MARK: - 注释来更好的管理。

协议实现

特别是在为一个模型增加协议实现时,推荐使用 extension。

推荐:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyViewController: UIViewController {
// class stuff here
}

// MARK: - UITableViewDataSource
extension MyViewController: UITableViewDataSource {
// table view data source methods
}

// MARK: - UIScrollViewDelegate
extension MyViewController: UIScrollViewDelegate {
// scroll view delegate methods
}

不推荐:

1
2
3
class MyViewController: UIViewController, UITableViewDataSource, UIScrollViewDelegate {
// all methods
}

未使用的代码

未使用的代码,包括Xcode的模版代码和占位注释,以及默认的协议实现,都应该删除。

推荐:

1
2
3
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Database.contacts.count
}

不推荐:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return Database.contacts.count
}

简化 import

简化引用框架,例如如果引用 Foundation 满足需求,就不要引入 UIKit

空格

  • 方法和(if/else/switch/while 等) 的{应在同一行,}在新的一行。
  • 提示: 你可以选中部分代码(或 ⌘A 选中全部)重新整理缩进 然后 Control-I (或菜单栏 Editor\Structure\Re-Indent)。

推荐:

1
2
3
4
5
if user.isHappy {
// Do something
} else {
// Do something else
}

不推荐:

1
2
3
4
5
6
7
if user.isHappy
{
// Do something
}
else {
// Do something else
}
  • 冒号左侧不应该有空格,但右侧应该有。 除了三元运算符 ? :, 空字典 [:]#selector 的无参数名语法 (_:).

推荐:

1
2
3
class TestDatabase: Database {
var data: [String: CGFloat] = ["A": 1.2, "B": 3.2]
}

不推荐:

1
2
3
class TestDatabase : Database {
var data :[String:CGFloat] = ["A" : 1.2, "B":3.2]
}
  • 每行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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Circle: Shape {
var x: Int, y: Int
var radius: Double
var diameter: Double {
get {
return radius * 2
}
set {
radius = newValue / 2
}
}

init(x: Int, y: Int, radius: Double) {
self.x = x
self.y = y
self.radius = radius
}

convenience init(x: Int, y: Int, diameter: Double) {
self.init(x: x, y: y, radius: diameter / 2)
}

override func area() -> Double {
return Double.pi * radius * radius
}
}

extension Circle: CustomStringConvertible {
var description: String {
return "center = \(centerString) area = \(area())"
}
private var centerString: String {
return "(\(x),\(y))"
}
}

上面的例子遵守也一下规范

  • 指定属性、变量、常量、参数声明和其他类型时冒号右侧加了空格,前面没有。例如: x: Int, 和 Circle: Shape
  • 两个参数具有相同的语义,定义在了同一行
  • 定义了属性的 getter setter
  • 不用加 internal 访问控制,因为默认就是。当重写方法时也不要重复定义访问控制。
  • 将额外的方法函数组织到 extension。
  • 隐藏实现细节,例子extension中的 centerStringprivate 访问控制.

使用 Self

当编译器提示需要 self 时才使用 (在 @escaping 闭包, 或初始化方法中为了和参数区分时)。其他如果编译器不提醒,则删除 self

计算属性

为了跟简洁。计算类的 read-only 属性删除 getter ,只有当有 setter 时才用 get 闭包实现。

推荐:

1
2
3
var diameter: Double {
return radius * 2
}

不推荐:

1
2
3
4
5
var diameter: Double {
get {
return radius * 2
}
}

Final

为类或其他元素添加 final 关键字会背离设计理念, 除非有明确的意图值得这样。 下面的例子, Box 明确了自定义子类类型时不允许的。 使用 final 来明确清晰的

1
2
3
4
5
6
7
// 使用Box将泛型转化为引用类型。
final class Box<T> {
let value: T
init(_ value: T) {
self.value = value
}
}

函数声明

保持简洁的函数声明,并将开括号写在同一行:

1
2
3
func reticulateSplines(spline: [Double]) -> Bool {
// reticulate code goes here
}

如果函数需要很长的签名,在合适的位置换行,Xcode会自动为换行添加缩进。

1
2
3
4
func reticulateSplines(spline: [Double], adjustmentFactor: Double,
translateConstant: Int, comment: String) -> Bool {
// reticulate code goes here
}

Closure Expressions

文档 只在单个闭包的时候才使用 Trailing closure syntax 。给闭包的参数定义好名字。

推荐:

1
2
3
4
5
6
7
8
9
UIView.animate(withDuration: 1.0) {
self.myView.alpha = 0
}

UIView.animate(withDuration: 1.0, animations: {
self.myView.alpha = 0
}, completion: { finished in
self.myView.removeFromSuperview()
})

不推荐:

1
2
3
4
5
6
7
8
9
UIView.animate(withDuration: 1.0, animations: {
self.myView.alpha = 0
})

UIView.animate(withDuration: 1.0, animations: {
self.myView.alpha = 0
}) { f in
self.myView.removeFromSuperview()
}

为内容简单的单表达式闭包(single-expression closures)使用隐式返回(implicit returns)

1
2
3
attendeeList.sort { a, b in
a > b
}

在trailing closures使用链式编程时需要保持内容简单易读。作者决定是否需要添加换行、空格和何时使用命名参数及匿名参数:

1
2
3
4
5
6
let value = numbers.map { $0 * 2 }.filter { $0 % 3 == 0 }.index(of: 90)

let value = numbers
.map {$0 * 2}
.filter {$0 > 50}
.map {$0 + 10}

类型

如果可行,永远使用Swift原生类型,Swift提供了与Objective-C类型的桥接。

推荐:

1
2
let width = 120.0                                    // Double
let widthString = (width as NSNumber).stringValue // String

不推荐:

1
2
let width: NSNumber = 120.0                          // NSNumber
let widthString: NSString = width.stringValue // NSString

在 Sprite Kit 代码中, 如果可以,使用 CGFloat 来使代码更简洁和避免过多的转换。

常量

let 定义常量, var 定义变量. 如果一个变量的值在生命周期内没有改变,使用 let 代替 var

Tip: 一个好的方法时永远使用let定义所有参数,仅在编译器提示需要使用 var时才替换。

将常量定义在一个类型里,比定义在实例中跟好。 使用static let定义一个类型属性作为常量。 这样定义类型属性比全局定义常量好,因为这样可以很好地与实例属性区分:

推荐:

1
2
3
4
5
6
enum Math {
static let e = 2.718281828459045235360287
static let root2 = 1.41421356237309504880168872
}

let hypotenuse = side * Math.root2

Note: 使用枚举的好处是不会被实例化和控制命名空间。

不推荐:

1
2
3
4
let e = 2.718281828459045235360287  // 影响了全局命名
let root2 = 1.41421356237309504880168872

let hypotenuse = side * root2 // what is root2?

静态方法和可变类型属性 Static Methods and Variable Type Properties

静态方法、可变类型的属性和全局函数、全局变量的作用一致,应谨慎使用。 当功能限定了类型或与Objective-C交互时才使用。

Optionals

当变量和函数返回参数可能为nil时,做 optional ?处理。

使用!标识隐式变量,除非你知道它在使用前一定会被初始化,像subviews 要在 viewDidLoad里设置一样.

当访问一个可选型时,只访问一次,或访问多个可选型的话就使用链式。

1
self.textContainer?.textLabel?.setNeedsDisplay()

使用 if let 的 unwrap 来进行更多的操作

1
2
3
if let textContainer = self.textContainer {
// do many things with textContainer
}

当为可选型变量或属性命名时,无需在名字里说明, 像 optionalString or maybeView 因为已经在类型里做了声明。

使用 if let unwrap 时, 使用同样的命名,不要使用 unwrappedView or actualLabel

推荐:

1
2
3
4
5
6
7
var subview: UIView?
var volume: Double?

// later on...
if let subview = subview, let volume = volume {
// do something with unwrapped subview and volume
}

不推荐:

1
2
3
4
5
6
7
8
var optionalSubview: UIView?
var volume: Double?

if let unwrappedSubview = optionalSubview {
if let realVolume = volume {
// do something with unwrappedSubview and realVolume
}
}

赖加载 Lazy Initialization

使用赖加载来更好地管理对象的生命周期,尤其是用在像赖性UIViewController 加载 subview 时。 可以使用 closure { }() 或者是有的工厂方法, 例如:

1
2
3
4
5
6
7
8
9
lazy var locationManager: CLLocationManager = self.makeLocationManager()

private func makeLocationManager() -> CLLocationManager {
let manager = CLLocationManager()
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.delegate = self
manager.requestAlwaysAuthorization()
return manager
}

Notes:

  • 这里不需要[unowned self] ,没有产生循环应用。
  • Location manager 会跳出隐私授权窗口,所以这样处理很好。

类型推理 Type Inference

类型判断的事情交给编译器,让代码更简洁。 类型推理同样适用于 array 和 dictionary。 当需要时才指明像 CGFloatInt16等类型的定义。

推荐:

1
2
3
4
let message = "Click the button"
let currentBounds = computeViewBounds()
var names = ["Mic", "Sam", "Christine"]
let maximumWidth: CGFloat = 106.5

不推荐:

1
2
3
let message: String = "Click the button"
let currentBounds: CGRect = computeViewBounds()
let names = [String]()

空 Array 和空 Dictionary 的类型定义

对于空的 array 和空 dictionary 使用类型声明。(为长数组和字典也是用类型声明的方式)

推荐:

1
2
var names: [String] = []
var lookup: [String: Int] = [:]

不推荐:

1
2
var names = [String]()
var lookup = [String: Int]()

NOTE: 根据这样的规则,定义一个描述清晰的名字比以前更重要。

语法糖 Syntactic Sugar

使用更短版本的语法

推荐:

1
2
3
var deviceModels: [String]
var employees: [Int: String]
var faxNumber: Int?

不推荐:

1
2
3
var deviceModels: Array<String>
var employees: Dictionary<Int, String>
var faxNumber: Optional<Int>

Functions vs Methods

谨慎定义一些没有被类或类型包含的函数。 推荐使用方法代替自由函数,那样会影响阅读性和查找性。

自由函数在当它们与任何类型或实例都不关联时才使用。

Preferred

1
2
let sorted = items.mergeSorted()  // easily discoverable
rocket.launch() // acts on the model

Not Preferred

1
2
let sorted = mergeSort(items)  // hard to discover
launch(&rocket)

Free Function Exceptions

1
2
let tuples = zip(a, b)  // feels natural as a free function (symmetry)
let value = max(x, y, z) // another free function that feels natural

内存管理 Memory Management

避免循环引用。 分心对象图并用 weakunowned 防止循环引用。 或者, 使用值类型 (struct, enum) 完全避免循环引用。详见

扩展对象的生命周期

使用 [weak self]guard let strongSelf = self else { return } 扩展对象的生命周期。 如果不能立刻看出 self 的生命周期,推荐使用[weak self] ,因为 [unowned self] 除非知道self的生命周期比闭包要长。尤其当扩展生命周期对象为可选型时。

推荐:

1
2
3
4
5
6
7
resource.request().onComplete { [weak self] response in
guard let strongSelf = self else {
return
}
let model = strongSelf.updateModel(response)
strongSelf.updateUI(model)
}

不推荐:

1
2
3
4
5
// 如果 self 已经释放 ,就会引起崩溃
resource.request().onComplete { [unowned self] response in
let model = self.updateModel(response)
self.updateUI(model)
}

不推荐:

1
2
3
4
5
// 释放可能发生在 updating model 和 updating UI 之间
resource.request().onComplete { [weak self] response in
let model = self?.updateModel(response)
self?.updateUI(model)
}

访问控制 Access Control

合适地使用 privatefileprivate使模块更清晰,达到更好的封装. privatefileprivate 可以的话推荐使用private。 如使用 extnsion 可能需要使用 fileprivate.

当你明确需要开放全部访问权限时才用 openpublic、 和 internal

将访问控制描述写在前面。 访问权限前面只允许出现static 或像 @IBAction@IBOutlet@discardableResult

推荐:

1
2
3
4
5
private let message = "Great Scott!"

class TimeMachine {
fileprivate dynamic lazy var fluxCapacitor = FluxCapacitor()
}

不推荐:

1
2
3
4
5
fileprivate let message = "Great Scott!"

class TimeMachine {
lazy dynamic fileprivate var fluxCapacitor = FluxCapacitor()
}

控制流 Control Flow

相对 while ,推荐使用 for-infor 循环.

推荐:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for _ in 0..<3 {
print("Hello three times")
}

for (index, person) in attendeeList.enumerated() {
print("\(person) is at position #\(index)")
}

for index in stride(from: 0, to: items.count, by: 2) {
print(index)
}

for index in (0...3).reversed() {
print(index)
}

不推荐:

1
2
3
4
5
6
7
8
9
10
11
12
13
var i = 0
while i < 3 {
print("Hello three times")
i += 1
}


var i = 0
while i < attendeeList.count {
let person = attendeeList[i]
print("\(person) is at position #\(i)")
i += 1
}

guard

避免使用嵌套 if ,使用 guard 来处理多种条件的判断。

推荐:

1
2
3
4
5
6
7
8
9
10
11
12
13
func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {

guard let context = context else {
throw FFTError.noContext
}
guard let inputData = inputData else {
throw FFTError.noInputData
}

// use context and input to compute the frequencies

return frequencies
}

不推荐:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {

if let context = context {
if let inputData = inputData {
// use context and input to compute the frequencies

return frequencies
} else {
throw FFTError.noInputData
}
} else {
throw FFTError.noContext
}
}

当遇到多个可选型时,可是使用 guardif let 将条件组合起来,例子:

推荐:

1
2
3
4
5
6
guard let number1 = number1, 
let number2 = number2,
let number3 = number3 else {
fatalError("impossible")
}
// do something with numbers

不推荐:

1
2
3
4
5
6
7
8
9
10
11
12
13
if let number1 = number1 {
if let number2 = number2 {
if let number3 = number3 {
// do something with numbers
} else {
fatalError("impossible")
}
} else {
fatalError("impossible")
}
} else {
fatalError("impossible")
}

异常处理

异常处理是很有必要的。 通常,通过一行像 returnthrowbreakcontinue、 和 fatalError()的处理, 避免过多的处理. 如果需要做一些退出处理, 使用 defer 块来做清理工作。

分号 Semicolons

Swift 不再需要分号,只有当一行执行多个操作时才需要,但不要这样做。

推荐:

1
let swift = "not a scripting language"

不推荐:

1
let swift = "not a scripting language";

圆括号 Parentheses

圆括号不是必须的,应该删除。

推荐:

1
2
3
if name == "Hello" {
print("World")
}

不推荐:

1
2
3
if (name == "Hello") {
print("World")
}

当然,必要时使用圆括号来增加可读性。

推荐:

1
let playerMark = (player == current ? "X" : "O")

引用