Swift 編碼風格指南

本文轉(zhuǎn)自:Swift 編碼風格指南 | www.samirchen.com

背景

本文主要是對以下幾個編碼規(guī)范的整理:

對其中少數(shù)規(guī)范,我根據(jù)自己的習慣做了修改。

這里有些關(guān)于編碼風格 Apple 官方文檔,如果有些東西沒有提及,可以在以下文檔來查找更多細節(jié):

命名

使用駝峰命名法為 、方法、變量 等取一個描述性強的命名。、結(jié)構(gòu)體、枚舉、協(xié)議 這些類型名應(yīng)該首字母大寫,而 方法、變量 則應(yīng)該首字母小寫。

推薦:

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
}

一般情況下,應(yīng)該避免使用縮略詞。遵循 API Design Guidelines 的規(guī)范,當你使用常見的縮略詞時,應(yīng)該保持它們的大小寫一致性,要么所有字母都大寫,要么所有字母都小寫。比如:

推薦:

let urlString: URLString
let userID: UserID

不推薦:

let uRLString: UrlString
let userId: UserId

對于函數(shù)和構(gòu)造器,除非上下文已經(jīng)很清晰,最好為所有參數(shù)添加局部參數(shù)名。如果可以的話,最好也添加外部參數(shù)名來讓函數(shù)調(diào)用語句更易讀。

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 設(shè)計規(guī)范,當協(xié)議是用來「描述一個東西是什么」時,協(xié)議名應(yīng)該是一個名詞,比如:CollectionWidgetFactory。當協(xié)議是用來「描述一種能力」時,協(xié)議名應(yīng)該以 -ing-able-ible 結(jié)尾,比如:EquatableResizing。

枚舉

遵循蘋果的 API 設(shè)計規(guī)范對 Swift 3 的要求,使用首字母小寫的駝峰命名法來給枚舉值命名。

enum Shape {
    case rectangle
    case square
    case rightTriangle
    case equilateralTriangle
}

文字描述

在所有提及到函數(shù)的文字中(包括教程、書、評論),請從調(diào)用者的視角進行考慮,將所有的必要參數(shù)名都包含進來,比如:

Call convertPointAt(column:row:) from your own init implementation.

If you call dateFromString(_:) make sure that you provide a string with the format "yyyy-MM-dd".

If you call timedAction(afterDelay:perform:) from viewDidLoad() 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 這樣的前綴了。如果兩個不同模塊的存在相同的名字,你可以通過在它們前面添加模塊名來避免沖突。當然,你應(yīng)該只在必要的時候才添加模塊名前綴。

import SomeModule

let myClass = MyModule.UsefulClass()

選擇器

不要再用字符串來指定選擇器,而應(yīng)該使用新的語法方式,更安全。通常,你應(yīng)該使用上下文來縮短選擇器表達式。

推薦:

let sel = #selector(viewDidLoad)

不推薦:

let sel = #selector(ViewController.viewDidLoad)

泛型

泛型名應(yīng)該有較好的閱讀性,用首字母大寫的駝峰式命名。當一個類型沒有有意義的關(guān)系和角色,使用傳統(tǒng)的 TUV 來替代。

推薦:

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"

代碼結(jié)構(gòu)

使用 // MARK: - 根據(jù)「代碼功能類別」、「protocol/delegate 方法實現(xiàn)」等依據(jù)對代碼進行分塊組織。代碼的組織順序從整體上盡量遵循我們的認知順序。

協(xié)議實現(xiàn)

其中,當你為一個類實現(xiàn)某些協(xié)議時,推薦添加一個獨立的 extension 來實現(xiàn)具體的協(xié)議方法,這樣可以讓協(xié)議相關(guān)的代碼聚合在一起,從而保持代碼結(jié)構(gòu)的清晰性,比如:

推薦:

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
}

由于編譯器不允許在派生類重復(fù)聲明對協(xié)議的實現(xiàn),所以并不要求總是復(fù)制基類的 extension 組。尤其當這個派生類是一個終端類,只有少量的方法需要重載時。何時保留 extension 組,這個應(yīng)該由作者自己決定。

對于 UIKit 的 ViewControllers,可以考慮將 Lifecycle、Custom Accessors、IBAction 放在獨立的 extension 中實現(xiàn)。

無用代碼

無用的代碼,包括 Xcode 代碼模板提供的默認代碼,以及占位的評論,都應(yīng)該被刪掉。除非你是在寫教程需要讀者來閱讀你注釋的代碼。

不推薦:

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 等)總是與關(guān)聯(lián)的程序語句在同一行打開,而在新起的一行結(jié)束。

推薦:

if user.isHappy {
    // Do something
} else {
    // Do something else
}

不推薦:

if user.isHappy
{
    // Do something
}
else {
    // Do something else
}
  • 方法之間應(yīng)該保留一行空格來使得代碼結(jié)構(gòu)組織更清晰。在方法中,可以用空行來隔開功能塊,但是當一個方法中存在太多功能塊時,那就意味著你可能需要重構(gòu)這個大方法為多個小方法了。

  • 冒號的左邊總是不空格,右邊空 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]
}

注釋

只在需要的時候添加注釋來解釋一段代碼的意義,注釋要么保持與對應(yīng)的代碼一起更新,要不然就刪掉。

避免使用在代碼中使用塊注釋,代碼應(yīng)該是自解釋的。除非你的注釋是用來生成文檔的。

類和結(jié)構(gòu)體

使用哪個

結(jié)構(gòu)體是值類型,使用結(jié)構(gòu)體來表示那些沒有區(qū)別性的事物。一個包含 [a, b, c] 元素的數(shù)組和另一個包含 [a, b, c] 元素的數(shù)組是完全可替換的,你用第一個數(shù)組和用第二個數(shù)組沒有任何區(qū)別,因為它們代表著同樣的東西。所以數(shù)組是結(jié)構(gòu)體。

類是引用類型,使用類來表示那些有區(qū)別性的事物。你用類來表示「人」這個概念,是因為兩個「人」的實例是兩個不一樣的事情。兩個「人」的實例就算擁有相同的姓名、生日,也不代表他們是一樣的。但是「人」的生日數(shù)據(jù)應(yīng)該用結(jié)構(gòu)體表示,因為一個 1950-03-03 和另一個 1950-03-03 是一回事,日期這個概念沒有區(qū)別性。

有時候,一些概念本應(yīng)是用結(jié)構(gòu)體,但是由于歷史原因被實現(xiàn)為類了,比如 NSDate、NSSet。

類定義示例

以下是一個設(shè)計較好的類定義示例:

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 不需要用它來訪問屬性或調(diào)用方法。

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

  • 在構(gòu)造器中,為了區(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

如果類不會被繼承,那么將它設(shè)為 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
}

在鏈式方法調(diào)用中使用尾閉包語法時,需要確保上下文清晰可讀。對于是否空行以及是否使用匿名參數(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 來避免過多的轉(zhuǎn)換從而使代碼更簡練。

常量

let 關(guān)鍵字來定義常量,使用 var 關(guān)鍵字來定義變量。如果變量不需要被修改,則應(yīng)該總是選擇使用 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)類型屬性與全局方法和全局變量類似,應(yīng)該盡量少用。

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 的變量或?qū)傩?,因為這層意思以及明顯的體現(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
  }
}

結(jié)構(gòu)體構(gòu)造器

使用 Swift 原生的結(jié)構(gòu)體構(gòu)造器。

推薦:

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.infiniteCGRect.null 這樣使用帶命名空間約束的結(jié)構(gòu)體常量,不推薦像 CGRectInfinite、CGRectNull 這樣使用全局的結(jié)構(gòu)體常量。對于已經(jīng)存在的結(jié)構(gòu)體類型變量,你可以使用類似 .zero 這樣的縮寫。

懶加載

使用懶加載機制來在對象的生命周期中實現(xiàn)更細粒度的內(nèi)存和邏輯控制。尤其是 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 有一個副作用,會喚起向用戶申請權(quán)限的 UI 界面,所以在這里使用懶加載機制可以達到更細粒度的內(nèi)存和邏輯控制。

類型推導(dǎo)

為了代碼緊湊,推薦盡量使用 Swift 的類型推導(dǎo)。不過,對于 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ù)不屬于任何一個類或類型,應(yīng)該盡量少用??梢缘脑?,盡量使用方法而非自由函數(shù)。這樣可讀性更好,也更易查找。

最適合使用自由函數(shù)的場景是這個函數(shù)功能與任何特定的類或類型都沒有關(guān)聯(lián)關(guā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

內(nèi)存管理

在編碼中應(yīng)該避免循環(huán)引用。對于會產(chǎn)生循環(huán)應(yīng)用的地方,使用 weakunowned 來解決。此外,還可以使用類型(struct、enum)來避免循環(huán)引用。

延伸對象生命周期

可以通過 [weak self]guard let strongSelf = self else { return } 來延伸對象的生命周期。

相對于 [unowned self],這里更推薦使用 [weak self]。[unowned self] 在其作用的對象被釋放后,會造成野指針,而 [weak self] 則會將對應(yīng)的引用置為 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。當使用條件語句編寫邏輯時,左手的代碼應(yīng)該是 「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 使用 guardif 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、breakcontinue、fatalError() 即可。應(yīng)該避免在退出時寫大段的代碼,如果確實需要在不同的退出點上編寫退出清理邏輯,可以考慮使用 defer 來避免重復(fù)。

分號

Swift 不需要在一行代碼結(jié)束時使用分號。只有當你想把多行代碼放在一行寫時,才需要用分號隔開它們,但是一般不推薦這樣做。只有在使用 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")
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容