温德里奇规范
本规范原文链接.
规范只是一些最佳实践, 请酌情使用.
1 正确性
第一条原则, 也是最根本的原则: 应努力使代码在编译时没有任何的警告. 这就涉及到诸多的设计决定和实现途径的选择.
2 命名规则
命名时应做到见名知意, 且必须保证意义一致性.
下面列出的命名规则摘录自苹果的 API design guideline, 是其中一些主要的规则:
- 函数或方法名称必须能够清晰描述该函数或者方法的内容或功能
- 为了表达更加清楚, 可以适当放宽简洁性要求.
- 使用驼峰命名法.
- 使用驼峰命名法的时候就两种规则: 除了类型的名称开头是大写字母外, 其他的都是以小写字母开头.
- 在满足上述规则的前提下, 尽量做到表达简洁.
- 命名的时候是基于实体的抽象含义, 而非基于其类型.
- 保证抽象含义表达清楚的基础上, 适当增加类型信息.
- 保证命名后, 实体在使用上的流畅性. (这个需要技巧, 比如命名了一个东西, 找好久找不到, 想好久想不起来叫什么, 这样就会造成在使用上的不流畅)
- 工厂方法以单词
make
开头. - 方法名称中需要包含描述其功能的附加效应的词:(这个不易理解, 需参考 API 设计准则再加以详细描述)
- 使用专业术语或名词的时候, 尽量做到不要 "让知道的人笑掉大牙, 让不知道的人不知所云".
- 命名时尽量不要使用缩写, 特别是那些比较生僻的缩写或者是自己发明的缩写.
- 命名时做到有据可依, 要有先例最好?. (这个需参考 API 设计准则再加以详细描述)
- 尽量使用属性和方法, 不要使用没有处于特定命名空间中的变量或函数.
- 缩略语的大小写方针要统一, 要么统一大写, 要么统一小写.
- 具有相似含义的若干方法, 当应用场景不同的时候, 它们的名称需要附加描述含义的相同的前缀或后缀.
- swift中支持基于不同返回值时候的重载, 但是需要极力避免这样的重载发生.
- 使用文档化的参数命名.
- 为闭包或元组的参数提供标签.
- 尽量利用参数的默认值, 能省很多事.(比如可选参数的默认值是nil, 就不用再在外界传参时候再传入一个nil了.)
2.1 Prose
略
2.2 不要添加类型名前缀
由于 swift 中使用 module 作为一个名字空间, 而 module 名字唯一, 所以类名冲突是不存在的. 即使两个 module 中有相同的类名, 也可以通过 module名.类名
加以区分, 并且只有在有相同类名的时候才需要使用这样方式.
import SomeModule
let myClass = MyModule.UsefulClass()
基于上述事实, swift 中不要再使用类名前缀了.
2.3 代理方法命名
当自定义代理时, 代理方法的第一个参数应该是代理源, 且没有参数标签, 保证和原生代码中的代理方法命名方式的一致性:
应该:
func namePickerView(_ namePickerView: NamePickerView, didSelectName name: String)
func namePickerViewShouldReload(_ namePickerView: NamePickerView) -> Bool
不应:
func didSelectName(namePicker: NamePickerViewController, name: String)
func namePickerShouldReload() -> Bool
2.4 利用swift编译器的类型推导机制(写代码的时候的东西了这个, 需要移动位置)
充分利用编译器提供的类型上下文推导机制, 写更加短小且清晰易懂的代码.
应该:
let selector = #selector(viewDidLoad)
view.backgroundColor = .red
let toView = context.view(forKey: .to)
let view = UIView(frame: .zero)
不应:
let selector = #selector(ViewController.viewDidLoad)
view.backgroundColor = UIColor.red
let toView = context.view(forKey: UITransitionContextViewKey.to)
let view = UIView(frame: CGRect.zero)
2.5 泛型类型参数命名规则
泛型类型参数名称应该是有意义的, 且是大写字母开头的驼峰命名. 只有当泛型类型参数没有确切意义或角色时, 才使用传统的单个大写字母命名方式.
应该:
struct Stack<Element> { ... }
func write<Target: OutputStream>(to target: inout Target)
func swap<T>(_ a: inout T, _ b: inout T)
不应:
struct Stack<T> { ... }
func write<target: OutputStream>(to target: inout target)
func swap<Thing>(_ a: inout Thing, _ b: inout Thing)
2.6 关于美式还是英式英语单词
苹果公司的API都全部是美式的, 你说你整些英式在里面也不合适吧.
: ]
应该:
let color = "red"
不应:
let colour = "red"
3 代码组织(还没有表达清楚, 需要修改)
3.1 extension 的使用
利用 extension
将代码按照逻辑功能进行组织. 每一个 extension
应该有对应的 //MARK -
注释来分隔.
比如 UIViewController
的子类代码中, 可以将 view 的生命期回调, 自定义访问方法(setter或getter), IBAction等分别放到不同的 extension 中去实现.
3.2 协议的实现
当既有继承关系, 又有协议(或通俗地说: 接口)实现的情况下, 应将接口的实现放到 extension
中去. 这样可以保证相关功能的代码都位于同一片代码区域中.
应该:
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
}
不应:
class MyViewController: UIViewController, UITableViewDataSource, UIScrollViewDelegate {
// 所有代码都被放这里...
}
例外情况:
父类通过某种途径声明遵守某协议后(比如在类定义时声明遵守协议, 或是使用 extension 实现协议), 子类中都无法再次声明遵守某协议.
且如果父类中使用
extension
实现了协议, 子类中是无法重写任何协议方法的.(目前不支持重写 extension 中的方法)这样就引出了几个选择:
- 父类类定义时实现协议, 子类可以直接重写协议方法. 这样没有利用任何 extension 的功能.
- 父类通过
extension
实现协议, 则子类中无法对方法进行重写.- 将协议放在子类中去实现.
总结上述三点, 目前要利用
extension
来组织协议的实现代码的话, 则这个类要么是终端类, 要么其子类没有重写协议方法的需求.故在继承链的什么位置来实现协议, 使用何种方式实现协议, 这些都需要开发者根据实际情况来决定了.
3.3 未使用的代码的处理
对于未使用的(或者是废弃的)代码, 包括Xcode自动生成的代码或者是占位用的注释等, 都应该移除掉. 当然如果是你需要保留的一些注释除外, 比如未实现的代码的注释等.
另外就是一些自动生成的代码块, 实际并没有做任何工作, 只是调用父类的实现, 这类代码也应该移除.
应该:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Database.contacts.count
}
不应:
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
}
3.4 最小化引用原则
保证代码中最小限度地引入外部库. 比如, 当可以只引入 Foundation
就能完成工作的时候, 就不要引入更大的 UIKit
了.
4 空格和缩进
- 使用两个空格来代替
tab
.(非强制, 讨论2个空格还是4个空格没有任何意义, 唯一的要求是一个公司中必须使用同一种缩进策略, 比如统一2个空格.) - 代码块的起始大括号, 应和块开头的代码位于同一行. 不要另起一行写.(抛弃C语言的那一套习惯先)
- 利用 Xcode 自带的格式化快捷键
control + I
来格式化代码.
应该:
if user.isHappy {
// Do something
} else {
// Do something else
}
不应:
if user.isHappy
{
// Do something
}
else {
// Do something else
}
-
方法与方法之间应该用一个空行隔开.
-
方法内部代码应该使用空行来对不同逻辑功能的代码块进行分隔, 但不可过多, 过多的分隔意味着这个方法可以被再次重构分解为多个方法.
-
冒号的使用原则: 冒号始终靠近左边, 并且和左边的词之间没有空格, 而右边则需要一个空格:
应该:
class TestDatabase: Database { var data: [String: CGFloat] = ["A": 1.2, "B": 3.2] }
不应:
class TestDatabase : Database { var data :[String:CGFloat] = ["A" : 1.2, "B":3.2] }
-
每行的最大长度应该控制在一个规定值范围内. 而这个值根据实际需要确定. 暂定120. 可以在Xcode中设置.
-
避免在行尾出现空白符. (为什么?)
-
每个文件的末尾添加一个空行. (为什么?)
5 注释
写注释的目的是说明: "为什么这段特定代码会做这样的事情".
必须时刻保持注释与相关代码同步更新. 所以不能滥用注释, 但也别不写注释. 最好的代码是自解释的.
这里所说的注释指的是代码注释, 如果是用于生成文档的文档注释, 则需要详尽.
6 类和结构
Struct 是值类型的, 故 struct 适用于不需要保持其状态的对象. 例如: 一个包含 [啊, 波, 坡]
的数组和另外一个包含 [啊, 波, 坡]
的数组, 除了所处存储空间不同外, 实际上表达的逻辑功能是一样的, 二者可以随意互换. 正因为如此, swift 中的 array, string 等等实际都是 struct, 即是值类型的.
Class 是引用类型的. 如果要使用的对象具有一定的特殊性, 并且需要保持其状态(即具有一定的生命期), 故需要使用 Class作为该对象的类型. 比如需要建立两个人的模型, 就需要使用 class 来表达这两个人, 因为他们两个是不一样的, 但是两个人的生日可以用 struct 来表达, 因为一个2000年1月1日和另外一个2000年1月1日表达的是同一个值.
需要根据实体的抽象意义, 合理选择使用值类型还是引用类型.
应该:
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))"
}
}
6.1 self的使用
不是必须的情况下, 尽量省略self.
6.2 计算属性
当写只读属性的 getter 时, 应省略 get块:
应该:
var diameter: Double {
return radius * 2
}
不应:
var diameter: Double {
get {
return radius * 2
}
}
6.3 合理使用 final 关键字
7 闭包表达式
只有当闭包是方法的最后一个参数时, 才考虑写尾随闭包.
尽量给闭包中的参数赋予一个具体名称来表达其含义.
应该:
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()
})
不应:
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()
}
当上下文意思清晰的情况下, 可以省略单行闭包中的return:
attendeeList.sort { a, b in
a > b
}
另外就是闭包的一个格式问题, 由开发者按实际情况决定:
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}
8 类型
应尽可能使用 swift 中的原生类型.
应该:
let width = 120.0 // Double
let widthString = (width as NSNumber).stringValue // String
不应:
let width: NSNumber = 120.0 // NSNumber
let widthString: NSString = width.stringValue // NSString
只有当对性能改善更加明显的情况下, 才考虑使用 OC 中的类型.(比如在 Sprite Kit 中使用 CGFloat)
8.1 常量
如果一个量的值不会发生变化的情况下, 应使用 let 声明这个量, 而不是 var.
小贴士: 将所有的量都先用 let 声明, 只有当编译器发出警告时, 才替换为 var.
另外就是应尽量将所有使用到的常量按照功能不同放置在不同名字空间中(如enum).
应该:
enum Math {
static let e = 2.718281828459045235360287
static let root2 = 1.41421356237309504880168872
}
let hypotenuse = side * Math.root2
不应:
let e = 2.718281828459045235360287 // pollutes global namespace
let root2 = 1.41421356237309504880168872
let hypotenuse = side * root2 // what is root2?
上面的 enum 中没有 case, 这样的好处是让其仅仅作为一个名字空间使用.
8.2 类方法和类属性
类方法和类属性的作用类似于全局变量, 不过由于它们处于某些名字空间下, 更加合理安全.
8.3 可选类型
当方法或函数的返回值可能不存在时, 则将返回值类型声明为可选类型.
只有当确定可选值在当前时刻之前的确会存在的情况下, 才考虑用隐式可选类型 !
.
当值只会被访问一次的情况下, 使用可选链来获取这个值:
self.textContainer?.textLabel?.setNeedsDisplay()
当值会被多次用到时, 则使用可选绑定:
if let textContainer = self.textContainer {
// do many things with textContainer
}
由于在类型中已经隐式说明了变量是可选类型的, 故在命名时尽量不要使用 optionalString
, maybeView
这样形式的名字.
在使用可选绑定的时候, 尽量使用名字覆盖的办法, 不要再次将解包后的变量命名为诸如 unwrappedXXX
的形式.
应该:
var subview: UIView?
var volume: Double?
// later on...
if let subview = subview, let volume = volume {
// do something with unwrapped subview and volume
}
不应:
var optionalSubview: UIView?
var volume: Double?
if let unwrappedSubview = optionalSubview {
if let realVolume = volume {
// do something with unwrappedSubview and realVolume
}
}
8.4 懒加载
利用懒加载, 可以精确控制一个变量的生命期.
实现的时候可以使用一个直接执行的闭包 { }()
或一个私有工厂方法进行对象创建, swift 会在使用到这个对象的时候才执行那个闭包或者调用私有工厂方法.
lazy var locationManager: CLLocationManager = self.makeLocationManager()
private func makeLocationManager() -> CLLocationManager {
let manager = CLLocationManager()
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.delegate = self
manager.requestAlwaysAuthorization()
return manager
}
注意:
- 利用闭包实现懒加载时, 不需要
[unowned self]
, 因为这里不会出现引用循环. - 上述代码中的 locationManager 首次创建的时候会请求用户权限, 所以这里使用懒加载来加载它是非常合适的.
8.5 类型推导
尽量利用类型推导来写更加短小简洁的代码. 只有当类型推导的默认类型不满足要求时, 才考虑显式指定类型. 另外就是对于短小的数组, 尽量直接使用数组的字面量来表达数组.
应该:
let message = "Click the button"
let currentBounds = computeViewBounds()
var names = ["Mic", "Sam", "Christine"]
let maximumWidth: CGFloat = 106.5
不应:
let message: String = "Click the button"
let currentBounds: CGRect = computeViewBounds()
let names = [String]()
8.5.1 空数组或字典的类型标识
如果新建空数组或字典供之后使用, 则应该使用类型标识.(另外如果字典或数组字面量表达式过长时, 也可以考虑添加类型标识)
应该:
var names: [String] = []
var lookup: [String: Int] = [:]
不应:
var names = [String]()
var lookup = [String: Int]()
8.5.2 类型标识的写法
尽量使用 swift 的语法糖来写类型标识, 而不要去用原始的冗长方式.
应该:
var deviceModels: [String]
var employees: [Int: String]
var faxNumber: Int?
不应:
var deviceModels: Array<String>
var employees: Dictionary<Int, String>
var faxNumber: Optional<Int>
9 函数和方法之间的选择
只有当无法将这个功能归入某个类型或子名字空间内时, 才考虑使用函数.
let tuples = zip(a, b) // feels natural as a free function (symmetry)
let value = max(x, y, z) // another free function that feels natural
其余的情况都应该将这些功能作为某个类型的类方法或对象方法:
应该:
let sorted = items.mergeSorted() // easily discoverable
rocket.launch() // acts on the model
不应:
let sorted = mergeSort(items) // hard to discover
launch(&rocket)
10 内存管理
内存管理进入半自动时代, 所以唯一要记住的原则是: 不要制造引用循环!
在可能出现循环引用的情况下, 将变量类型标记为weak
或 unowned
. 另外, 也可以使用值类型 struct
或 enum
来避免引用循环.(为什么? 这些对象的存储空间还是在堆内存中, 只是指针变量是放在栈中的)
10.1 闭包中的self
当在闭包中需要使用 self, 且会形成引用循环时, 考虑使用 weak
标记 self, 尽量不要使用 unowned
, 除非 self 在执行这个闭包时很明显是还存在的情况.
应结合 guard
来安全地使用 weak self
.
应该:
resource.request().onComplete { [weak self] response in
guard let strongSelf = self else {
return
}
let model = strongSelf.updateModel(response)
strongSelf.updateUI(model)
}
不应:
// might crash if self is released before response returns
resource.request().onComplete { [unowned self] response in
let model = self.updateModel(response)
self.updateUI(model)
}
也不应:
// deallocate could happen between updating the model and updating UI
resource.request().onComplete { [weak self] response in
let model = self?.updateModel(response)
self?.updateUI(model)
}
11 访问控制
合理使用 private
和 fileprivate
, 这样代码会更加清晰, 且提高了封装的质量. 一般来说, 能用 private
的地方尽量用它. 但如果某些成分需要要 extension 中使用的时候, 则需要使用 fileprivate
.
另外的 open
, public
和 internal
, 就需要按情况来使用了.
访问控制修饰符应该放在最开始的位置. 除了有 static
以及注解的时候.
应该:
private let message = "Great Scott!"
class TimeMachine {
fileprivate dynamic lazy var fluxCapacitor = FluxCapacitor()
}
不应:
fileprivate let message = "Great Scott!"
class TimeMachine {
lazy dynamic fileprivate var fluxCapacitor = FluxCapacitor()
}
12 流程控制
能够使用 for-in
的时候就不要用 while
方式的循环.
应该:
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)
}
不应:
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
}
13 最佳侧原则
当编写条件判断代码的时候, 整个代码对齐的左侧称为"最佳侧", 意思是如果代码越远离左侧, 表示嵌套的层次越深, 代码的质量也就越低. 当有多层嵌套的时候, 尽量使用 guard
来简化嵌套.
应该:
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
}
不应:
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
}
}
另外就是 guard
后面可以写多条可选绑定语句, 这样写也可以简化很多嵌套代码.
应该:
guard let number1 = number1,
let number2 = number2,
let number3 = number3 else {
fatalError("impossible")
}
// do something with numbers
不应:
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")
}
13.1 guard 中失败的处理
在 guard 后的 else 语句中写的一般是某种形式的退出代码, 且一般都是单行代码如 return
, throw
, break
, continue
或者 fatalError()
等.
在实际编码的时候, 尽量避免在里面写大段的代码. 如果真的需要写一些清理功能的代码, 则可以放到 defer
中去写, 而不是在 guard
的 else
块中.
14 分号的使用
swift 不强制使用分号, 且一般应该不去写表达式末尾的分号. 只有当想在一行写多条语句的时候, 语句间才用分号分隔.
但请记住: 不应将多条语句写在一行上, 不应在表达式末尾添加分号.
不要把 swift 当做一种简单的脚本语言看待.
应该:
let swift = "not a scripting language"
不应:
let swift = "not a scripting language";
15 括号的使用
尽量避免在条件语句中使用括号. 只有当语句比较复杂的时候, 才考虑用括号来组织.
应该:
if name == "Hello" {
print("World")
}
let playerMark = (player == current ? "X" : "O")
不应:
if (name == "Hello") {
print("World")
}
16 Xcode中的组织名称以及 Bundle ID 的命名(可选)
Xcode 中的组织名称(organization)需要符合英文书写习惯, 即名字首字母大写, 各单词用空格分开, 比如 Ray Wenderlich
.
Bundle ID 需要设置为如 com.ray.TutorialName
的形式, 其中 TutorialName
即为工程名称.
17 版权声明的写法
版权声明需要在每个源文件的开头都写上. 这个就各有不同了.
下面是 MIT 开源协议的版权声明文本模板, 仅作为参考.
/**
* Copyright (c) 2017 XXXXX公司 LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* Notwithstanding the foregoing, you may not use, copy, modify, merge, publish,
* distribute, sublicense, create a derivative work, and/or sell copies of the
* Software in any work that is designed, intended, or marketed for pedagogical or
* instructional purposes related to programming, coding, application development,
* or information technology. Permission for such use, copying, modification,
* merger, publication, distribution, sublicensing, creation of derivative works,
* or sale is expressly withheld.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
18 对待生活的态度
保持乐观积极的态度, 笑也要开心地笑:
应该:
:] //这个是开心笑
不应:
:) //这个是勉强笑
网友评论