Google Swift Style Guide 濃縮版
參考文件: ofollow,noindex"> Google Swift Style Guide
原始檔組織
檔名
所有 swift 原始檔都使用 .swift 副檔名。
通常使用原始檔中主體(primary entity)命名。如果是擴充套件現有型別,在現有型別有使用 + 號,帶上擴充套件的內容。
例子:
-
如果是一個單一的型別 MyType,那麼檔名為 MyType.swift。
-
如果是擴充套件 MyType 實現 MyProtocol,那麼檔名為 MyType+MyProtocol.swift。
-
如果是給 MyType 增加了一下擴充套件方法,那麼檔案的字首是 MyType+ ,比如可以是 MyType+Additions.swift。
-
如果檔案裡的宣告不是在一個相關型別下,可以描述這些宣告的作用、場景。比如 Math.swift。
Import 宣告
預設直接 import 最高階的 module。顯式匯入使用的所有模組,比如雖然 UIKit 依賴 Foundation,但是如果使用 UIKit,還是會分別匯入這兩個 module。
只有在 import 最高階不能訪問到需要的物件,或者為了避免汙染到現有的名稱空間裡的物件才會 import 子模組。
Import 宣告在檔案的頂部。
不同的 import 型別用換行隔開。
例子:
import CoreLocation import MyThirdPartyModule import SpriteKit import UIKit import func Darwin.C.isatty @testable import MyModuleUnderTest
Type, Variable, Function 的宣告
通常一個原始檔裡只有一個頂級的型別宣告,特別是這個型別的內容很多的時候。不過有的場景下一個檔案裡包含幾個有關聯的型別也是合理的,比如:
-
一個類和它的代理協議定義在同一個檔案裡。
-
一個型別和它相關的 helper 方法有時會定義在同一個檔案裡。這個時候通常會使用 fileprivate 修飾 helper 方法。
型別、變數、函式在原始檔中的佈局順序對程式碼的可讀性有很大的影響。但是這些的順序並沒有一個固定的形式,不同的型別、不同的檔案可能會有不同的組織形式。
重要的是__每一個檔案、型別必須按照一定的邏輯來組織這些元素__,也就是說如果別人問起為什麼這樣分佈可以有一個說法。比如往一個型別裡增加了一些方法,肯定不能只是簡單追加在檔案的末尾。當然你也可以說是按照方法寫的時間順序來組織,然而時間順序並不是一種邏輯上的組織。
在組織這些元素時建議使用 // MARK: 來為分組寫上註釋。
class MovieRatingViewController: UITableViewController { // MARK: - View controller lifecycle methods override func viewDidLoad() { // ... } override func viewWillAppear(_ animated: Bool) { // ... } // MARK: - Movie rating manipulation methods @objc private func ratingStarWasTapped(_ sender: UIButton?) { // ... } @objc private func criticReviewWasTapped(_ sender: UIButton?) { // ... } }
過載的宣告放在一起
如果一個型別有多個初始化方法或者 subscript,或者一個檔案、型別有多個基本名字相同的方法(引數標籤不同),要把它們寫在一起。
Extension 裡的方法也要按照邏輯組織
Extension 的方法可能被幾個不同的場景使用到。按照邏輯組織 Extension 裡的成員也同樣重要。也要按照一定的邏輯關係組織裡面的元素。
通用格式
見文件: 通用格式
專有結構的格式
非文件類註釋
非文件類註釋使用 // ,不使用 C 風格的 /* ... */ 。
屬性
變數宣告的地方儘量靠近在使用的地方。
每一行只宣告一個變數。
× var a = 5, b = 10
Switch
Case 的宣告和 switch 保持同樣的縮排,case 裡的內容換行後有 2 個縮排。
√ switch order { case .ascending: print("Ascending") case .descending: print("Descending") case .same: print("Same") } × switch order { case .ascending: print("Ascending") case .descending: print("Descending") case .same: print("Same") } × switch order { case .ascending: print("Ascending") case .descending: print("Descending") case .same: print("Same") }
Enum
通常 enum 下的每個 case 都是單獨一行。只有在確認未來不會擴充套件,不需要給 case 單獨寫註釋,沒有使用關聯型別,沒有 rawValue 值的 enum 才有可能寫成一行的。
√ public enum Token { case comma case semicolon case identifier } public enum Token { case comma, semicolon, identifier } public enum Token { case comma case semicolon case identifier(String) }
× public enum Token { case comma, semicolon, identifier(String) }
如果每一個 case 都是 indirect,那麼直接在 enum 上宣告 indirect。
√ public indirect enum DependencyGraphNode { case userDefined(dependencies: [DependencyGraphNode]) case synthesized(dependencies: [DependencyGraphNode]) } × public enum DependencyGraphNode { indirect case userDefined(dependencies: [DependencyGraphNode]) indirect case synthesized(dependencies: [DependencyGraphNode]) }
列舉的選項順序必須有意義。如果沒有特別的邏輯可以參考,按照每項的首字母順序。
下面的例子用空行分組,整體上按照狀態碼從小到達排序看起來很清楚:
√ public enum HTTPStatus: Int { case ok = 200 case badRequest = 400 case notAuthorized = 401 case paymentRequired = 402 case forbidden = 403 case notFound = 404 case internalServerError = 500 }
如果直接按照首字母的詞典書序則會降低了可讀性。
× public enum HTTPStatus: Int { case badRequest = 400 case forbidden = 403 case internalServerError = 500 case notAuthorized = 401 case notFound = 404 case ok = 200 case paymentRequired = 402 }
尾閉包
如果兩個函式只有最後一個閉包引數不同,那麼就不應該過載。需要宣告不同的函式名。如果只是尾引數閉包不同,那麼如果使用了尾閉包的語法糖呼叫,會造成歧義,不知道呼叫的是哪個函式。
比如下面的例子,呼叫 greet 的時候就不知道呼叫的是哪個函式:
× func greet(enthusiastically nameProvider: () -> String) { print("Hello, \(nameProvider())! It's a pleasure to see you!") } func greet(apathetically nameProvider: () -> String) { print("Oh, look. It's \(nameProvider()).") } greet { "John" }// error: ambiguous use of 'greet'
如果函式呼叫的引數分成了很多行,這個時候不使用尾閉包的語法糖。
√ UIView.animate( withDuration: 0.5, animations: { // ... }, completion: { finished in // ... }) × UIView.animate( withDuration: 0.5, animations: { // ... }) { finished in // ... }
如果引數閉包符合尾閉包的定義,那麼應該總是使用尾閉包的語法糖。只有兩種例外情況:
-
前面提到的有過載的函式,使用尾閉包會導致歧義。
-
如果尾閉包用在條件控制語句中,尾閉包會引起語法的衝突。
√ Timer.scheduledTimer(timeInterval: 30, repeats: false) { timer in print("Timer done!") } if let firstActive = list.first(where: { $0.isActive }) { process(firstActive) }
× Timer.scheduledTimer(timeInterval: 30, repeats: false, block: { timer in print("Timer done!") }) // This example fails to compile. if let firstActive = list.first { $0.isActive } { process(firstActive) }
使用尾閉包時,刪除尾閉包外的括號:
√ let squares = [1, 2, 3].map { $0 * $0 } × let squares = [1, 2, 3].map({ $0 * $0 }) let squares = [1, 2, 3].map() { $0 * $0 }
尾部逗號
賦值陣列、字典時每個元素分別佔有一行時,最後一個選項後面也新增逗號。這樣未來如果有元素加入會更加方便。
√ let configurationKeys = [ "bufferSize", "compression", "encoding", ] × let configurationKeys = [ "bufferSize", "compression", "encoding" ]
數字字面量
如果是一個很長的數字時,建議使用下劃線按照語言習慣三位或者四位一組分割連線。
√ let number = 100_0000
Attribute
帶引數的特性(比如 @availability(...)、@objc(...))單獨宣告在一行裡。
√ @available(iOS 9.0, *) public func coolNewFeature() { // ... } × @available(iOS 9.0, *) public func coolNewFeature() { // ... }
不帶引數的特性(比如 @IBOutlet、@NSManaged)則宣告在同一行裡。如果寫到同一行後代碼長度超過長度限制,則將特性標籤單獨宣告一行。
√ public class MyViewController: UIViewController { @IBOutlet private var tableView: UITableView! }
命名
Apple’s API Style Guidelines
認真貫徹學習蘋果官方釋出的 API design guidelines。
不要使用約定命名樣式代替訪問控制
如果要控制訪問許可權應該使用訪問控制(internal、fileprivate、private),不用使用自定義的命名方式來區分,比如在方法前前下劃線表示私有。
只有在極端的情況下才會採用這種自定義命名錶示。比如有一個方法只是為了某個模板呼叫才公開的,這種情況下本意是私有的,但是又必須宣告成 public,可以使用自定義的命名慣例。
識別符號
通常,識別符號只包含 7 位 ASCII 字元。如果 Unicode 識別符號在程式碼庫的問題域中有明確含義(例如,表示數學概念的希臘字母),並且能被使用該程式碼的團隊很好地理解,則可以使用它們。
初始化方法
如果初始化引數和自身的屬性是一一對應的關係,引數名和屬性名保持一致。
√ public struct Person { public let name: String public let phoneNumber: String // GOOD. public init(name: String, phoneNumber: String) { self.name = name self.phoneNumber = phoneNumber } }
× public struct Person { public let name: String public let phoneNumber: String // AVOID. public init(name otherName: String, phoneNumber otherPhoneNumber: String) { name = otherName phoneNumber = otherPhoneNumber } }
靜態屬性
如果一個靜態屬性返回的是自身的型別,不用在屬性名稱字尾中重複宣告型別。
√ public class UIColor { public class var red: UIColor {// GOOD. // ... } } public class URLSession { public class var shared: URLSession {// GOOD. // ... } }
× public class UIColor { public class var redColor: UIColor {// AVOID. // ... } } public class URLSession { public class var sharedSession: URLSession {// AVOID. // ... } }
如果是表示單例的靜態屬性,一般命名為 shared 或者 default。
全域性常量
就像正常變數一樣使用匈牙利命名方式,不要在前面加上 g、k 或其他特別的格式。
√ let secondsPerMinute = 60 × let SecondsPerMinute = 60 let kSecondsPerMinute = 60 let gSecondsPerMinute = 60 let SECONDS_PER_MINUTE = 60
代理方法
代理方法的命名和正常函式命名會有明顯的區別,命名的方式參考了 Cocoa 的 protocol。
代理的主體作為方法第一個引數。
只有一個引數
如果返回值是 void,方法名以主體開頭,接一個動詞短語表示發生的事件。隱藏引數名稱標籤。
√ func scrollViewDidBeginScrolling(_ scrollView: UIScrollView)
如果返回值是 Bool,方法名以主體開頭,接一個表示判斷的短語。隱藏引數名稱標籤。
√ func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool
如果返回的是其他型別的值,方法名則是一個描述了返回值角色的名詞短語。引數標籤會盡量使用介詞或者片語來連線方法名和主體。
√ func numberOfSections(in scrollView: UIScrollView) -> Int
多個引數
代理方法多個引數的情況下,第一個引數總是隱藏標籤。
命名的分類與上面提到的相似,區別只是方法的基礎名字都是主體,對於方法的具體作用描述宣告在引數標籤裡。
√ func tableView( _ tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAt indexPath: IndexPath) func tableView( _ tableView: UITableView, shouldSpringLoadRowAt indexPath: IndexPath, with context: UISpringLoadedInteractionContext ) -> Bool func tableView( _ tableView: UITableView, heightForRowAt indexPath: IndexPath ) -> CGFloat