本文翻譯于The Official raywenderlich.com Swift Style Guide。目前更新到 Swfit4.2。
前言:作為一名開發人員,須知好的代碼規范,不僅能夠提升代碼的可讀性、提升開發效率同時也會對讓團隊間的開發溝通效果得到加強。針對Swift語言raywenderlich.com給出來一份較為完善的開發指南。本文就是基于原文翻譯而成。英文水平較好這可點擊上方鏈接,查看原文。
[TOC]
開發警告??
開發者應該努力做到代碼沒有警告的通過編譯,此規則會由許多的代碼編寫風格來決定,例如使用#selector
類型來代替字符串字母量、盡量避免使用標記了DEPRECATED
的API等。
命名規范
描述性和一致性的命名會使代碼更易于閱讀和理解。蘋果官方的API設計指南中也描述了Swift的命名約定,一些關鍵點包括:
- 命名應當清晰。
- 命名清晰度的重要性大于簡潔性,盡可能的表達清晰而不用害怕命名過長。
- 使用駝峰法命名。
- 命名針對類型和協議使用大寫字母開頭,其它都已小寫字母開頭。
- 盡量包含所需要的單詞,同時省略不必要的單詞。
- 命名的名稱應當基于當前語義,而不是類型。
- 對信息缺失的命名可以適當補充。
- 命名盡量使用流暢的語法
- 工廠方法以
make
開頭 - 命名方法考慮以下因素
- 不可變(non-mutating)的動詞方法后接 -ed、-ing。
- 不可變(non-mutating)的名詞方法遵守
formX
- boolean 類型的方法閱讀應當類似斷言。只有是、否兩種含義。
- 描述某些東西的協議應該被理解為名詞(例如Collection)。
- 描述一個協議能力應該使用后綴命名able,ible或ing (例如Equatable,ProgressReporting)。
- 使用通俗易懂的術語,無論是新手還是專家都能輕松接受。
- 一般來說避免使用縮寫。
- 使用既定成俗的命名。
- 首選方法和屬性來代替函數。
- 通常縮略語和首字母縮略詞應根據案例慣例統一大寫或小寫。
- 為具有相同含義的方法指定相同的基名。
- 避免方法的返回值過多。
- 將方法名稱命名部分放在第一個參數名中。
- 選擇好的方法參數名稱,見文知意,起到好的說明效果。
- 對閉包和元組類型參數說明
- 利用好默認參數。
類前綴
Swift類型由包含它們的模塊自動命名,并且不應添加類前綴,例如RW。如果來自不同模塊的兩個名稱發生沖突,則可以通過在類型名稱前加上模塊名稱來消除歧義。但是,只有在存在混淆的時才指定模塊名稱,這在平時開發中應該很少出現。
import SomeModule
let myClass = MyModule.UsefulClass()
Delegates
創建自定義委托方法時,未命名的第一個參數應該是代理源。
推薦:
func namePickerView(_ namePickerView:NamePickerView,didSelectName name:String)
func namePickerViewShouldReload(_ namePickerView:NamePickerView)- > Bool
不推薦:
func didSelectName(namePicker: NamePickerViewController, name: String)
func namePickerShouldReload() -> Bool
類型推導
使用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)
泛型
泛型類型的參數應該具有描述性的,按駝峰命名。當一個泛型類名沒有一個有意義的名稱,可以使用傳統的單大寫字母代替,例如T,U或V。
推薦:
struct Stack<Element> { ... }
func write<Target: OutputStream>(to target: inout Target)
func swap<T>(_ a: inout T, _ b: inout T)
不推薦:
struct Stack<Element> { ... }
func write<Target: OutputStream>(to target: inout Target)
func swap<T>(_ a: inout T, _ b: inout T)
語法
使用美式的語法,美式語法會更加匹配蘋果的API。
推薦:
let color = "red"
不推薦:
let colour = "red"
代碼結構
使用extensions來組織你的代碼結構,每個extension中應該是一整塊的相關代碼。每個extensions之前都應添加// MARK: -
注釋。
協議一致性
遵守協議時,針對每個協議下的方法添加單獨的擴展,使得協議方法組合在一起。
推薦:
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
}
在UIKit中的Viewcontroller
中可以使用Extension來分離自定義訪問器和IBAction。
未使用的代碼
應當刪除未使用的代碼包括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
}
最小import原則
只導入所需的模塊,例如當你只需要Foundation
時,無需導入UIKit
,當你需要UIKit
時,無需再重復導入Foundation
。
推薦:
import UIKit
var view: UIView
var deviceModels: [String]
推薦:
import Foundation
var deviceModels: [String]
不推薦:
import UIKit
import Foundation
var view: UIView
var deviceModels: [String]
不推薦:
import UIKit
var deviceModels: [String]
代碼間隔
使用2個空格而不是制表符縮進以節省空間,Xcode中的設置可以按如下配置:
- 方法括號和其他大括號(
if
/else
/switch
/while
等)始終保持左括號與語法在相同的行上,右括號換行。 - 您可以通過選擇一些代碼(或Command-A以選擇所有代碼)然后再按Control-I來重新縮進。
推薦:
if user.isHappy {
// Do something
} else {
// Do something else
}
不推薦:
if user.isHappy
{
// Do something
}
else {
// Do something else
}
- 方法之間應該只有一個空行,以幫助視覺清晰度和組織。
- 開閉的大括號和代碼之間不應該有空行
-
:
一般左邊沒有空格,右邊一個空格。除了三目運算符? :
、空字典[:]
、和#selector
的語法如addTarget(_:action:)
推薦:
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]
}
- 每行代碼應該限定在70個字符以內
- 避免在行尾隨意的添加空格
- 每個文件的末尾添加一個換行符
注釋
使用注釋來說明代碼塊的作用,注釋應當實時的更新或者刪除。
避免代碼內聯的塊注釋,代碼應該做到自我解釋,無需多的注釋說明就能看懂。
避免使用C語言的注釋風格(/* ... */
)。使用 //
或者 ///
來添加注釋。
類和結構體
類和結構體的選擇?
結構體是值語義,當我們定義一個沒有特性的事物的時候可以采用Struct
類型,一個包含[a,b,c]
的數組和另一個包含[a,b,c]
的數組是可以互相替換的。無論你使用第一個數組還是第二個數組都表達的是同一個東西,所以Array
就是一個Struct
類型。
類是引用語義,當定義一個具有特性或者有特殊聲明周期的事物時采用Class
類型。當你定義人的時候應該定義為類,兩個同姓名且同生日的人對象在含義上卻是兩個不同的東西。
Example 展示
規范的類定義
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
。 - 在同一行代碼中定義相同類型的變量。如:
var x: Int,y: Int
- 縮進getter和setter定義以及屬性觀察器。
- 不要添加修飾符,例如internal它們已經是默認值。同樣,重寫方法時也不要重復訪問修飾符。
- 在Extension中添加額外的功能(例如打印)。
-
centerString
使用private
訪問控制隱藏實現細節。
使用self
為了代碼簡潔,應當在Swift
中避免顯式使用self
來訪問屬性及調用方法。
僅在編譯器要求使用self
的時候使用(在@escaping閉包中、初始化器中用于消除參數中相同名稱的屬性歧義)
計算屬性
為簡明起見,如果計算屬性是只讀的,則省略get子句。只有在提供set子句時才需要get子句。
推薦:
var diameter : Double {
return radius * 2
}
不推薦:
var diameter : Double {
get {
return radius * 2
}
}
final標記
特定的情況下使用final
標記,當你明確知道你的類或者屬性成員是終極的時候可以使用final
標記。在下面的示例中,Box
具有特定目的并且不希望在派生類中進行自定義。標記它就final
清楚了。
// 將泛型類型轉換成引用類型
final class Box<T> {
let value: T
init(_ value: T) {
self.value = value
}
}
函數聲明
在左括號下面添加一行簡短函數聲明。
func reticulateSplines(spline: [Double]) -> Bool {
// reticulate code goes here
}
對于名稱和參數都較多的函數,將每個參數放在一個新行上,并在后續行中添加一個額外的縮進:
func reticulateSplines(
spline:[ Double ],
adjustmentFactor:Double,
translateConstant:Int,comment:String
)- > Bool {
// reticulate code goes here
}
使用()
不要使用(Void)
來表示參數為空,在閉包和函數返回時使用Void
而不是()
。
推薦:
func updateConstraints() -> Void {
// magic happens here
}
typealias CompletionHandler = (result) -> Void
不推薦:
func updateConstraints() -> () {
// magic happens here
}
typealias CompletionHandler = (result) -> ()
函數調用
和函數聲明一樣,在函數調用時候如果單行就能表示就是用單行:
let success = reticulateSplines(splines)
如果調用函數參數過多時,每個參數換行并在后續行中添加一個額外的縮進:
let success = reticulateSplines(
spline: splines,
adjustmentFactor: 1.3,
translateConstant: 2,
comment: "normalize the display")
閉包表達式
僅當參數中只有一個閉包表達式參數且是最后一個參數的時候才使用尾隨閉包。
推薦:
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()
}
對于上下文清晰的單表達式閉包,使用隱式返回:
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}
類型
盡可能使用Swift
的原生類型和表達式,Swift
提供了Swift提供了與Objective-C的橋接,因此您仍然可以根據需要調用所有OC方法。
推薦:
let width = 120.0 // Double
let widthString = "\(width)" // String
一般:
let width = 120.0 // Double
let widthString = (width as NSNumber).stringValue // String
不推薦:
let width: NSNumber = 120.0 // NSNumber
let widthString: NSString = width.stringValue // NSString
在繪圖的代碼中使用CGFloat
類型,避免過多轉換使代碼更簡潔。
常量
如果變量的值將不會改變時始終使用let
而不是var
。
Tip:你可以一開始就是用 let
來定義所有的內容,當編譯器編譯錯誤的時候在改為var
你可以在一個類型里面去定義常量而不是在類型的實例變量中去使用類型屬性。使用static let
聲明類型常量比直接聲明全局變量更推薦。這樣可以更好區分全局變量和實例屬性。
推薦:
enum Math {
static let e = 2.718281828459045235360287
static let root2 = 1.41421356237309504880168872
}
let hypotenuse = side * Math.root2
不推薦:
let e = 2.718281828459045235360287 // 污染全局命名空間
let root2 = 1.41421356237309504880168872
let hypotenuse = side * root2 // 什么 root2?
靜態方法和可變類型屬性
靜態方法和類型屬性跟全局函數和全局變量的工作原理類似,應當謹慎使用。當功能的作用域是一個特定類型或需要與 Objective-C 交互時,它們才非常有用。
可選類型
在可能出現 nil
值的情況下,使用 ?
聲明變量和函數返回類型為可選類型。
當訪問一個可選值時,如果值僅被訪問一次或在鏈中有許多可選項時,使用可選鏈:
self.textContainer?.textLabel?.setNeedsDisplay()
當一次性解包和執行多個操作更方便時,使用可選綁定:
if let textContainer = self.textContainer {
// do many things with textContainer
}
在命名可選變量和屬性時,需避免類似optionalString
或 maybeView
這樣的命名,因為他們的可選性已經體現在類型聲明中了。
對于可選綁定,適當時使用原始名稱,而不是使用像 unwrappedView 或 actualLabel 這樣的名稱。
不推薦:
var subview: UIView?
var volume: Double?
// later on...
if let subview = subview, let volume = volume {
// 使用展開的 subview 和 volume 做某件事
}
推薦:
var optionalSubview: UIView?
var volume: Double?
if let unwrappedSubview = optionalSubview {
if let realVolume = volume {
// 使用 unwrappedSubview 和 volume 做某件事
}
}
懶加載(延遲初始化)
在更細粒度地控制對象聲明周期時考慮使用延遲初始化。 對于 UIViewController
,延遲初始化視圖是非常正確的。你也可以直接調用 { }() 的閉包或調用私有工廠方法。例如:
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]。
- 位置管理器向用戶彈出申請權限是影響用戶體驗的,所以細顆粒地控制在這里是有意義的。
類型推斷
優先選擇簡潔緊湊的代碼,讓編譯器為單個實例的常量或變量推斷類型。類型推斷也適合于小(非空)的數組和字典。如需使用特定類型,需提前聲明。如 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]()
空數組和空字典的類型前置聲明
為空數組和空字典使用類型注釋。(對于分配給大型、多行文字的數組和字典,使用類型前置聲明。)
推薦:
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: [String]
var employees: [Int: String]
var faxNumber: Int?
函數 vs 方法
不在類或類型的中聲明的自由函數應該被謹慎使用。可能的話,首選方法而不是自由函數。這有助于可讀性和易領悟性。
自由函數最適用于它們與任何特定類或實例無關的情況。
推薦:
let sorted = items.mergeSorted() // easily discoverable
rocket.launch() // acts on the model
不推薦:
let sorted = mergeSort(items) // hard to discover
launch(&rocket)
使用自由函數的特列:
let tuples = zip(a, b) // feels natural as a free function (symmetry)
let value = max(x, y, z) // another free function that feels natural
內存管理
在你的代碼中絕對不能出現循環引用的情況,使用內存分析工具并用 weak
和 unowned
來防止強循環引用。或者,使用值類型( struct、enum )來徹底防止循環引用。
延長對象的生命周期
使用慣用語法 [weak self]
和 guard let strongSelf = self else { return }
來延長對象的生命周期。 在 self
超出閉包生命周期不明顯的地方,[weak self]
更優于[unowned self]
。顯式的延長生命周期會優于使用可選解包。
推薦:
resource.request().onComplete { [weak self] response in
guard let self = self else {
return
}
let model = self.updateModel(response)
self.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)
}
訪問控制
一般情況下private
和fileprivate
優先使用private
,只有編譯器提示使用fileprivate
才做修改。
將訪問控制用作前置屬性說明符。僅有 static 說明符或諸如 @IBAction 、 @IBOutlet 和 @discardableResult 的屬性可以放在訪問控制說明符前面。
推薦:
private let message = "Great Scott!"
class TimeMachine {
private dynamic lazy var fluxCapacitor = FluxCapacitor()
}
不推薦:
fileprivate let message = "Great Scott!"
class TimeMachine {
lazy dynamic private var fluxCapacitor = FluxCapacitor()
}
控制流
優先選擇 for 循環的 for-in 格式而不是 while-condition-increment 格式。
推薦:
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
}
黃金路徑
不要嵌套多個條件判斷,可以提前將結果返回。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 或 if let 解包多個可選值時,在可能的情況下使用最下化復合版本嵌套。舉例:
推薦:
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")
}
失敗防護
對于用某些方法退出,防護語句是必要的。一般地,它應該是一行簡潔的語句,比如: return 、 throw 、 break 、 continue 和 fatalError()。應該避免大的代碼塊。如果清理代碼被用在多個退出點,則可以考慮用 defer 塊來避免清理代碼的重復。
分號
在 Swift 中,每條代碼語句后面都不需要加分號。只有在你希望在一行中結合多條語句,才需要加分號。
不要在用分號分隔的單行中寫多條語句。
推薦:
let swift = "not a scripting language"
不推薦:
let swift = "not a scripting language";
括號
條件判斷周圍的括號是不必要的,應該省略。
推薦:
if name == "Hello" {
print("World")
}
不推薦:
if (name == "Hello") {
print("World")
}
在復雜的表達式中,添加括號可以讓代碼讀起來更清晰。
推薦:
let playerMark = (player == current ? "X" : "O")
多行字符串字面量
當你創建長字符串文字時,推薦使用多行字符串字面量,左右"""
和字符串間隔開。
推薦:
let message = """
You cannot charge the flux \
capacitor with a 9V battery.
You must use a super-charger \
which costs 10 credits. You currently \
have \(credits) credits available.
"""
不推薦:
let message = """You cannot charge the flux \
capacitor with a 9V battery.
You must use a super-charger \
which costs 10 credits. You currently \
have \(credits) credits available.
"""
不推薦:
let message = "You cannot charge the flux " +
"capacitor with a 9V battery.\n" +
"You must use a super-charger " +
"which costs 10 credits. You currently " +
"have \(credits) credits available."
不要使用Emoji
在你定義變量或者方法中不要使用Emoji。Emoji會增加開發人員對代碼的理解難度。
Xcode中的Organization和Bundle Identifier設置
Xcode中組織名稱應該是你單位或者你個人的名稱,如Ray Wenderlich
,Bundle Identifier
一般采用com/cn +公司名+項目名
形式。如當你項目名稱為TutorialName
時,Bundle Identifier
則應該命名為 ·com.raywenderlich.TutorialName`