本文轉自:Swift 編碼風格指南 | www.samirchen.com
背景
本文主要是對以下幾個編碼規(guī)范的整理:
對其中少數(shù)規(guī)范,我根據(jù)自己的習慣做了修改迹淌。
這里有些關于編碼風格 Apple 官方文檔锰扶,如果有些東西沒有提及田绑,可以在以下文檔來查找更多細節(jié):
- The Swift API Design Guidelines
- The Swift Programming Language
- Using Swift with Cocoa and Objective-C
- Swift Standard Library Reference
命名
使用駝峰命名法為 類
促绵、方法
、變量
等取一個描述性強的命名迂曲。類
禀忆、結構體
、枚舉
狸页、協(xié)議
這些類型名應該首字母大寫锨能,而 方法
、變量
則應該首字母小寫芍耘。
推薦:
private let maximumWidgetCount = 100
class WidgetContainer {
var widgetButton: UIButton
let widgetHeightPercentage = 0.85
}
不推薦:
let MAX_WIDGET_COUNT = 100
class app_widgetContainer {
var wBut: UIButton
let wHeightPct = 0.85
}
一般情況下址遇,應該避免使用縮略詞。遵循 API Design Guidelines 的規(guī)范斋竞,當你使用常見的縮略詞時倔约,應該保持它們的大小寫一致性,要么所有字母都大寫坝初,要么所有字母都小寫浸剩。比如:
推薦:
let urlString: URLString
let userID: UserID
不推薦:
let uRLString: UrlString
let userId: UserId
對于函數(shù)和構造器,除非上下文已經很清晰鳄袍,最好為所有參數(shù)添加局部參數(shù)名绢要。如果可以的話,最好也添加外部參數(shù)名來讓函數(shù)調用語句更易讀拗小。
func dateFromString(dateString: String) -> NSDate
func convertPointAt(column column: Int, row: Int) -> CGPoint
func timedAction(afterDelay delay: NSTimeInterval, perform action: SKAction) -> SKAction!
// would be called like this:
dateFromString("2014-03-14")
convertPointAt(column: 42, row: 13)
timedAction(afterDelay: 1.0, perform: someOtherAction)
對于類中的方法重罪,請遵循蘋果慣例,將方法名作為第一個參數(shù)的外部名:
class Counter {
func combineWith(otherCounter: Counter, options: Dictionary?) { ... }
func incrementBy(amount: Int) { ... }
}
協(xié)議
遵循蘋果的 API 設計規(guī)范,當協(xié)議是用來「描述一個東西是什么」時剿配,協(xié)議名應該是一個名詞搅幅,比如:Collection
、WidgetFactory
呼胚。當協(xié)議是用來「描述一種能力」時盏筐,協(xié)議名應該以 -ing
、-able
或 -ible
結尾砸讳,比如:Equatable
琢融、Resizing
。
枚舉
遵循蘋果的 API 設計規(guī)范對 Swift 3 的要求簿寂,使用首字母小寫的駝峰命名法來給枚舉值命名漾抬。
enum Shape {
case rectangle
case square
case rightTriangle
case equilateralTriangle
}
文字描述
在所有提及到函數(shù)的文字中(包括教程、書常遂、評論)纳令,請從調用者的視角進行考慮,將所有的必要參數(shù)名都包含進來克胳,比如:
Call
convertPointAt(column:row:)
from your owninit
implementation.If you call
dateFromString(_:)
make sure that you provide a string with the format "yyyy-MM-dd".If you call
timedAction(afterDelay:perform:)
fromviewDidLoad()
remember to provide an adjusted delay value and an action to perform.You shouldn't call the data source method
tableView(_:cellForRowAtIndexPath:)
directly.
類名前綴
Swift 的類型會被自動包含到它所在模塊的命名空間中平绩,所以沒有必要再給 Swift 的類型添加類似 RW
這樣的前綴了。如果兩個不同模塊的存在相同的名字漠另,你可以通過在它們前面添加模塊名來避免沖突捏雌。當然,你應該只在必要的時候才添加模塊名前綴笆搓。
import SomeModule
let myClass = MyModule.UsefulClass()
選擇器
不要再用字符串來指定選擇器性湿,而應該使用新的語法方式,更安全满败。通常肤频,你應該使用上下文來縮短選擇器表達式。
推薦:
let sel = #selector(viewDidLoad)
不推薦:
let sel = #selector(ViewController.viewDidLoad)
泛型
泛型名應該有較好的閱讀性算墨,用首字母大寫的駝峰式命名宵荒。當一個類型沒有有意義的關系和角色,使用傳統(tǒng)的 T
净嘀、U
报咳、V
來替代。
推薦:
struct Stack<Element> { ... }
func writeTo<Target: OutputStream>(inout target: Target)
func max<T: Comparable>(x: T, _ y: T) -> T
不推薦:
struct Stack<T> { ... }
func writeTo<target: OutputStream>(inout t: target)
func max<Thing: Comparable>(x: Thing, _ y: Thing) -> Thing
語言
使用美式英語面粮,這樣更契合蘋果的 API少孝。
推薦:
let color = "red"
不推薦:
let colour = "red"
代碼結構
使用 // MARK: -
根據(jù)「代碼功能類別」继低、「protocol/delegate 方法實現(xiàn)」等依據(jù)對代碼進行分塊組織熬苍。代碼的組織順序從整體上盡量遵循我們的認知順序。
協(xié)議實現(xiàn)
其中,當你為一個類實現(xiàn)某些協(xié)議時柴底,推薦添加一個獨立的 extension
來實現(xiàn)具體的協(xié)議方法婿脸,這樣可以讓協(xié)議相關的代碼聚合在一起,從而保持代碼結構的清晰性柄驻,比如:
推薦:
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 {
// all methods
}
由于編譯器不允許在派生類重復聲明對協(xié)議的實現(xiàn)狐树,所以并不要求總是復制基類的 extension 組。尤其當這個派生類是一個終端類鸿脓,只有少量的方法需要重載時抑钟。何時保留 extension 組,這個應該由作者自己決定野哭。
對于 UIKit 的 ViewControllers在塔,可以考慮將 Lifecycle、Custom Accessors拨黔、IBAction 放在獨立的 extension 中實現(xiàn)蛔溃。
無用代碼
無用的代碼,包括 Xcode 代碼模板提供的默認代碼篱蝇,以及占位的評論贺待,都應該被刪掉。除非你是在寫教程需要讀者來閱讀你注釋的代碼零截。
不推薦:
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func numberOfSectionsInTableView(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
}
推薦:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Database.contacts.count
}
最小引用
只 import 你需要的模塊麸塞。比如,如果引用 Foundation
以及足夠涧衙,就不要再引用 UIKit
了喘垂。
空白
使用 Tab 而非空格。
方法的大括號以及其他的大括號(
if
/else
/switch
/while
等)總是與關聯(lián)的程序語句在同一行打開绍撞,而在新起的一行結束正勒。
推薦:
if user.isHappy {
// Do something
} else {
// Do something else
}
不推薦:
if user.isHappy
{
// Do something
}
else {
// Do something else
}
方法之間應該保留一行空格來使得代碼結構組織更清晰。在方法中傻铣,可以用空行來隔開功能塊章贞,但是當一個方法中存在太多功能塊時,那就意味著你可能需要重構這個大方法為多個小方法了非洲。
冒號的左邊總是不空格鸭限,右邊空 1 格。除了在三元運算符
? :
和空字典[:]
中两踏。
推薦:
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]
}
注釋
只在需要的時候添加注釋來解釋一段代碼的意義败京,注釋要么保持與對應的代碼一起更新,要不然就刪掉梦染。
避免使用在代碼中使用塊注釋赡麦,代碼應該是自解釋的朴皆。除非你的注釋是用來生成文檔的。
類和結構體
使用哪個
結構體是值類型
泛粹,使用結構體來表示那些沒有區(qū)別性的事物遂铡。一個包含 [a, b, c] 元素的數(shù)組和另一個包含 [a, b, c] 元素的數(shù)組是完全可替換的,你用第一個數(shù)組和用第二個數(shù)組沒有任何區(qū)別晶姊,因為它們代表著同樣的東西扒接。所以數(shù)組是結構體。
類是引用類型
们衙,使用類來表示那些有區(qū)別性的事物钾怔。你用類來表示「人」這個概念,是因為兩個「人」的實例是兩個不一樣的事情蒙挑。兩個「人」的實例就算擁有相同的姓名蒂教、生日,也不代表他們是一樣的脆荷。但是「人」的生日數(shù)據(jù)應該用結構體表示凝垛,因為一個 1950-03-03 和另一個 1950-03-03 是一回事,日期這個概念沒有區(qū)別性蜓谋。
有時候梦皮,一些概念本應是用結構體,但是由于歷史原因被實現(xiàn)為類了桃焕,比如 NSDate剑肯、NSSet。
類定義示例
以下是一個設計較好的類定義示例:
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)
}
func describe() -> String {
return "I am a circle at \(centerString()) with an area of \(computeArea())"
}
override func computeArea() -> Double {
return M_PI * radius * radius
}
private func centerString() -> String {
return "(\(x),\(y))"
}
}
使用 Self
為了簡潔观堂,能不用 self
的地方就不用让网,因為 Swift 不需要用它來訪問屬性或調用方法。
Use self
when required to differentiate between property names and arguments in initializers, and when referencing properties in closure expressions (as required by the compiler):
在下面情況中师痕,你需要使用 self
:
- 在構造器中溃睹,為了區(qū)別傳入的參數(shù)和屬性。
- 在閉包中訪問屬性胰坟,編譯器要求用
self
因篇。
class BoardLocation {
let row: Int, column: Int
init(row: Int, column: Int) {
self.row = row
self.column = column
let closure = {
print(self.row)
}
}
}
計算屬性
為了簡潔,如果計算屬性是只讀的笔横,那么就省略 get
竞滓。只有當同時寫了 set
語句時,才寫 get
語句吹缔。
推薦:
var diameter: Double {
return radius * 2
}
不推薦:
var diameter: Double {
get {
return radius * 2
}
}
Final
如果類不會被繼承商佑,那么將它設為 final
的。比如:
// Turn any generic type into a reference type using this Box class.
final class Box<T> {
let value: T
init(_ value: T) {
self.value = value
}
}
函數(shù)聲明
對于較短的函數(shù)聲明厢塘,包括括號茶没,在一行完成肌幽。
func reticulateSplines(spline: [Double]) -> Bool {
// reticulate code goes here
}
對于較長的函數(shù)聲明,在合適的地方換行礁叔,并在新起的一行加縮進牍颈。
func reticulateSplines(spline: [Double], adjustmentFactor: Double,
translateConstant: Int, comment: String) -> Bool {
// reticulate code goes here
}
閉包表達式
如果參數(shù)列表只有最后一個參數(shù)是閉包類型迄薄,則盡可能使用尾閉包語法琅关。在所有情況下給閉包參數(shù)一個描述性強的命名。
推薦:
UIView.animateWithDuration(1.0) {
self.myView.alpha = 0
}
UIView.animateWithDuration(1.0,
animations: {
self.myView.alpha = 0
},
completion: { finished in
self.myView.removeFromSuperview()
}
)
不推薦:
UIView.animateWithDuration(1.0, animations: {
self.myView.alpha = 0
})
UIView.animateWithDuration(1.0,
animations: {
self.myView.alpha = 0
}) { f in
self.myView.removeFromSuperview()
}
對于上下文清晰的單表達式閉包讥蔽,使用隱式的返回值:
attendeeList.sort { a, b in
a > b
}
在鏈式方法調用中使用尾閉包語法時涣易,需要確保上下文清晰可讀。對于是否空行以及是否使用匿名參數(shù)等冶伞,則留給作者自行決定新症。例如:
let value = numbers.map { $0 * 2 }.filter { $0 % 3 == 0 }.indexOf(90)
let value = numbers
.map {$0 * 2}
.filter {$0 > 50}
.map {$0 + 10}
類型
如果可以的話,總是優(yōu)先使用 Swift 提供的原生類型响禽。Swift 提供了對 Objective-C 的橋接徒爹,所以你可以使用所有需要的方法。
推薦:
let width = 120.0 // Double
let widthString = (width as NSNumber).stringValue // String
不推薦:
let width: NSNumber = 120.0 // NSNumber
let widthString: NSString = width.stringValue // NSString
在 Sprite Kit 代碼中芋类,使用 CGFloat
來避免過多的轉換從而使代碼更簡練隆嗅。
常量
用 let
關鍵字來定義常量,使用 var
關鍵字來定義變量侯繁。如果變量不需要被修改胖喳,則應該總是選擇使用 let
。
一個建議是:總是使用 let
除非編譯器報警告訴你需要使用 var
贮竟。
使用類型而非實例屬性來定義常量丽焊。最好也別用全局常量,這樣能更好區(qū)分常量和實例屬性咕别。如下:
推薦:
enum Math {
static let e = 2.718281828459045235360287
static let pi = 3.141592653589793238462643
}
radius * Math.pi * 2 // circumference
使用 case-less 枚舉的優(yōu)勢在于它不會被意外初始化技健,而僅僅作為一個 namespace 來用。
不推薦:
let e = 2.718281828459045235360287 // pollutes global namespace
let pi = 3.141592653589793238462643
radius * pi * 2 // is pi instance data or a global constant?
靜態(tài)方法和靜態(tài)類型屬性
靜態(tài)方法和靜態(tài)類型屬性與全局方法和全局變量類似惰拱,應該盡量少用凫乖。
Optional
當一個變量或函數(shù)返回值可以為 nil 時,用 ?
將其聲明為 Optional 的弓颈。
對于在使用前一定會被初始化的實例變量帽芽,用 !
將其聲明為隱式解包類型(Implicitly Unwrapped Types)。
在訪問一個 Optional 值時翔冀,如果該值只被訪問一次导街,或者之后需要連續(xù)訪問多個 Optional 值,請使用鏈式 Optional 語法:
self.textContainer?.textLabel?.setNeedsDisplay()
對于需要將 Optional 值解開一次纤子,多處使用的情況搬瑰,使用 Optional 綁定更為方便:
if let textContainer = self.textContainer {
// do many things with textContainer
}
不要使用類似 optionalString
款票、maybeView
這種名字來命名 Optional 的變量或屬性,因為這層意思以及明顯的體現(xiàn)在他們的類型聲明上了泽论。
對于 Optional 綁定艾少,推薦直接用同樣的名字,不要用 unwrappedView
翼悴、actualLabel
這種命名缚够。
推薦:
var subview: UIView?
var volume: Double?
// later on...
if let subview = subview, 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
}
}
結構體構造器
使用 Swift 原生的結構體構造器。
推薦:
let bounds = CGRect(x: 40, y: 20, width: 120, height: 80)
let centerPoint = CGPoint(x: 96, y: 42)
不推薦:
let bounds = CGRectMake(40, 20, 120, 80)
let centerPoint = CGPointMake(96, 42)
推薦像 CGRect.infinite
鹦赎、CGRect.null
這樣使用帶命名空間約束的結構體常量谍椅,不推薦像 CGRectInfinite
、CGRectNull
這樣使用全局的結構體常量古话。對于已經存在的結構體類型變量雏吭,你可以使用類似 .zero
這樣的縮寫。
懶加載
使用懶加載機制來在對象的生命周期中實現(xiàn)更細粒度的內存和邏輯控制陪踩。尤其是 UIViewController
杖们,在加載其 views 時,盡量采用懶加載方式肩狂≌辏可以使用 { }()
這種閉包的方式或者私有工廠的方式來實現(xiàn)懶加載。比如:
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]
婚温,因為這里沒有引起循環(huán)引用描焰。 - CLLocationManager 有一個副作用,會喚起向用戶申請權限的 UI 界面栅螟,所以在這里使用懶加載機制可以達到更細粒度的內存和邏輯控制荆秦。
類型推導
為了代碼緊湊,推薦盡量使用 Swift 的類型推導力图。不過步绸,對于 CGFloat
、Int16
這種吃媒,推薦盡量指定明確的類型瓤介。
推薦:
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]()
注意: 遵循這條規(guī)范意味著選用一個描述性強的命名,比之前更重要了赘那。
類型標注
對于空的數(shù)組和字典刑桑,使用類型標注。
推薦:
var names: [String] = []
var lookup: [String: Int] = [:]
不推薦:
var names = [String]()
var lookup = [String: Int]()
語法糖
推薦使用簡短的聲明募舟。
推薦:
var deviceModels: [String]
var employees: [Int: String]
var faxNumber: Int?
不推薦:
var deviceModels: Array<String>
var employees: Dictionary<Int, String>
var faxNumber: Optional<Int>
函數(shù)和方法
自由函數(shù)不屬于任何一個類或類型祠斧,應該盡量少用」敖福可以的話琢锋,盡量使用方法而非自由函數(shù)辕漂。這樣可讀性更好,也更易查找吴超。
最適合使用自由函數(shù)的場景是這個函數(shù)功能與任何特定的類或類型都沒有關聯(lián)關系钉嘹。
推薦:
let sorted = items.mergeSort() // easily discoverable
rocket.launch() // clearly acts on the model
不推薦:
let sorted = mergeSort(items) // hard to discover
launch(&rocket)
自由函數(shù)示例
let tuples = zip(a, b) // feels natural as a free function (symmetry)
let value = max(x,y,z) // another free function that feels natural
內存管理
在編碼中應該避免循環(huán)引用。對于會產生循環(huán)應用的地方鲸阻,使用 weak
和 unowned
來解決跋涣。此外,還可以使用類型(struct
赘娄、enum
)來避免循環(huán)引用仆潮。
延伸對象生命周期
可以通過 [weak self]
宏蛉、guard let strongSelf = self else { return }
來延伸對象的生命周期遣臼。
相對于 [unowned self]
,這里更推薦使用 [weak self]
拾并。[unowned self]
在其作用的對象被釋放后揍堰,會造成野指針,而 [weak self]
則會將對應的引用置為 nil嗅义。
相對于 Optional 拆包屏歹,更推薦明確的延長生命周期。
推薦:
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)
}
訪問控制
開發(fā)的訪問級別不需要把 public
寫出來之碗,但對于 private
蝙眶,則最好寫出。
除了 static
褪那、@IBAction
幽纷、@IBOutlet
,一般情況下博敬,總是把訪問控制修飾符 private
放在屬性修飾符的第一位友浸。
推薦:
class TimeMachine {
private dynamic lazy var fluxCapacitor = FluxCapacitor()
}
不推薦:
class TimeMachine {
lazy dynamic private var fluxCapacitor = FluxCapacitor()
}
控制流
相對于 while-condition-increment
,更推薦使用 for-in
偏窝。
推薦:
for _ in 0..<3 {
print("Hello three times")
}
for (index, person) in attendeeList.enumerate() {
print("\(person) is at position #\(index)")
}
for index in 0.stride(to: items.count, by: 2) {
print(index)
}
for index in (0...3).reverse() {
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
}
黃金路徑
盡早 return 或 break收恢。當使用條件語句編寫邏輯時,左手的代碼應該是 「golden」 或 「happy」 路徑祭往。也就是說伦意,不要嵌套多個 if 語句,即使寫多個 return 語句也是 OK 的硼补。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
}
}
當多個 Optional 使用 guard
或 if let
拆包,推薦最小化嵌套括勺。比如:
推薦:
guard let number1 = number1, number2 = number2, 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")
}
失敗的 Guard
guard
語句一般都需要以某種方式退出執(zhí)行缆八。一般來說使用 return
曲掰、throw
、break
奈辰、continue
栏妖、fatalError()
即可。應該避免在退出時寫大段的代碼奖恰,如果確實需要在不同的退出點上編寫退出清理邏輯吊趾,可以考慮使用 defer
來避免重復。
分號
Swift 不需要在一行代碼結束時使用分號瑟啃。只有當你想把多行代碼放在一行寫時论泛,才需要用分號隔開它們,但是一般不推薦這樣做蛹屿。只有在使用 for-conditional-increment
時用到分號是例外屁奏,當然我們更推薦使用 for-in
。
推薦:
let swift = "not a scripting language"
不推薦:
let swift = "not a scripting language";
圓括號
條件判斷語句外的圓括號不是必須的错负,推薦省略它們坟瓢。
推薦:
if name == "Hello" {
print("World")
}
不推薦:
if (name == "Hello") {
print("World")
}