前言
這篇文章是我根據在 SwiftGraphics 工作時的一系列筆記整理出來的。文中大多數建議是經過深思熟慮的,但仍可以有其他類似的解決方法。因此,如果其他方案是有意義的,這些方案會被新增上去。
這個最佳實踐不是強加或者推薦 Swift 在程式、物件導向或者函式風格上的應用。更重要的是,這裡要講述的是務實的方法。如有需要的話,某些建議可能會集中在物件導向或者實用的解決方法。
這篇文章講述的範圍主要針對 Swift 語言以及 Swift 標準庫。即便如此,如果能提出一個獨特的 Swift 的視角和見解,我們依然會提供諸如 Swift 在 Mac OS、iOS、 WatchOS 以及 TV OS 上使用的特別建議。而如何在 Xcode 和 LLDB 上有效地使用 Swift,這樣的建議也會以 Hints & tips 的風格提供。
這個過程需要付出很多的努力,非常感謝為本文做出貢獻的那些人。
此外,可以在[Swift-Lang slack]裡面討論。
貢獻者須知
請先確保所有的示例是可以執行的(某些示例可能不是正確)。這個 markdown 能夠轉換成一個 Mac OS X playground。
黃金準則
1. 一般來說,Apple 都是正確的,遵循 Apple 喜歡的或者示範的處理方式。在任何情況下,你都應該遵循 Apple 的程式碼風格,正如他們”The Swift Programming Language” 這本書裡面的定義一樣。然而 Apple 是個大公司,我們將會看到很多在示例程式碼中的差異。
2. 永遠不要僅僅為了減少程式碼量而去寫程式碼。儘量依賴Xcode中的自動補全程式碼,自動建議 , 複製和貼上。詳盡的程式碼描述風格對其他程式碼維護者來說是非常有好處的。即便如此,過度的冗餘也會失去 Swift 的重要特性:型別推斷。
最佳實踐
1.命名
正如 Swift Programming Language 中的型別名稱都是以大駝峰命名法命名的(例如:VehicleController)。
變數和常量則以小駝峰命名法命名(例如:vehicleName)。
你應該使用 Swift 模板去命名你的程式碼而不是使用 Objective-C 類字首的風格(除非和 Objective-C 接連)。
不要使用任何匈牙利標識法( Hungarian notation )命名(例如:k為常量,m為方法),應使用簡短的命名並且使用 Xcode 的型別 Quick Help (01.png+ click) 去查明變數的型別。同樣地,不要使用小寫字母+下劃線( SNAKE_CASE )的命名方式。
唯一比較特別的是 enum 值的命名,這裡需要使用大駝峰命名法(這個也是遵循 Apple 的 Swift Programming Language 風格):
1 2 3 |
enum Planet { case Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune } |
在所有可能的情況裡,名稱的不必要減少和縮寫都應該避免,將來你應該能在沒有任何損害和依賴 Xcode 的自動補全功能的情況下,確切地指出型別特徵” ViewController “。非常普遍的縮寫如 URL 是允許的。縮寫應該用所有字母大寫( URL )或者所有字母小寫( url )表示。對型別和變數使用相同的規則。如果 url 是個型別,則應該為大寫,如果是個變數,則應該為小寫。
2.註釋
註釋不應該用來使程式碼無效,註釋程式碼會使程式碼無效且影響程式碼的整潔。如果你想要移除程式碼,但是仍想保留以防程式碼在以後會用到,你應該依賴 git 或者 bug tracker 。
3.型別推斷
在可能的地方,使用Swift的型別推斷以減少多餘的型別資訊。例如,正確的寫法:
1 |
var currentLocation = Location() |
而不是:
1 |
var currentLocation: Location = Location() |
4.Self 推斷
讓編譯器在所有允許的地方推斷 self 。在 init 中設定引數以及 non-escaping closures 中應該顯性地使用 self 。例如:
1 2 3 4 5 6 7 |
struct Example { let name: String init(name: String) { self.name = name } } |
5.引數列表型別推斷
在一個閉包表示式( closure expression )中指定引數型別可能導致程式碼更加冗長。只有當需要指定型別時。
1 2 3 4 5 6 7 8 9 10 |
let people = [ ("Mary", 42), ("Susan", 27), ("Charlie", 18), ] let strings = people.map() { (name: String, age: Int) -> String in return "\(name) is \(age) years old" } |
如果編譯器能夠推斷型別,則應該去掉型別定義。
1 2 3 4 |
let strings = people.map() { (name, age) in return "\(name) is \(age) years old" } |
使用排序好的引數編號命名(”$0″,”$1″,”$2″)能更好地減少冗餘,這經常能夠完整匹配引數列表。只有當closure的引數名稱中沒有過多的資訊時,使用編號命名。(例如特別簡單的 maps 和 filters )。
Apple 能夠並將會改變由 Objective-C frameworks 轉換過來的 Swift 的引數型別。例如,選項被移除或者變為自動展開等。我們應有意地指定你的選項並依賴 Swift 去推斷型別,減少在這種情況下程式中斷的風險。
你總是應該有節制地指定返回型別。例如,這個引數列表明顯過分冗餘:
1 2 3 4 |
dispatch_async(queue) { () -> Void in print("Fired.") } |
6.常量
在型別定義的時候,常量應該在型別裡宣告為 static 。例如:
1 2 3 4 5 6 7 8 9 10 11 12 |
struct PhysicsModel { static var speedOfLightInAVacuum = 299_792_458 } class Spaceship { static let topSpeed = PhysicsModel.speedOfLightInAVacuum var speed: Double func fullSpeedAhead() { speed = Spaceship.topSpeed } } |
使用 static 修飾常量可以允許他們在被引用的時候不需要例項化型別。
除了單例以外,應儘量避免生成全域性常量。
7.計算型型別屬性(Computed Properties)
當你只需要繼承 getter 方法時,返回簡單的 Computed 屬性即可。例如,應該這樣做:
1 2 3 4 5 |
class Example { var age: UInt32 { return arc4random() } } |
而不是:
1 2 3 4 5 6 7 |
class Example { var age: UInt32 { get { return arc4random() } } } |
如果你在屬性中新增了 set 或者 didSet ,那麼你應該顯示地提供 get 方法。
1 2 3 4 5 6 7 8 9 10 |
class Person { var age: Int { get { return Int(arc4random()) } set { print("That's not your age.") } } } |
8.例項轉換(Converting Instances)
當建立程式碼去從一個型別轉換到另外的 init() 方法:
1 2 3 4 5 |
extension NSColor { convenience init(_ mood: Mood) { super.init(color: NSColor.blueColor) } } |
在 Swift 標準庫中,對於把一個型別的例項轉換為另外一種,現在看來 init 方法是比較喜歡用的一種方式。
“to” 方法是另外一種比較合理的技術(儘管你應該遵循 Apple 的引導去使用 init 方法):
1 2 3 4 5 |
struct Mood { func toColor() -> NSColor { return NSColor.blueColor() } } |
而你可能試圖去使用一個getter,例如:
1 2 3 4 5 |
struct Mood { var color: NSColor { return NSColor.blueColor() } } |
getters 通常由於應該返回可接受型別的元件而受到限制。例如,返回了 Circle 的例項是非常適合使用 getter 的,但是轉換一個 Circle 為 CGPath 最好在 CGPath 上使用”to”函式或者 init() 擴充套件。
9.單例(Singletons)
在Swift中單例是很簡單的:
1 2 3 |
class ControversyManager { static let sharedInstance = ControversyManager() } |
Swift 的 runtime 會保證單例的建立並且採用執行緒安全的方式訪問。
單例通常只需要訪問”sharedInstance”的靜態屬性,除非你有不得已的原因去重新命名它。注意,不要用靜態函式或者全域性函式去訪問你的單例。
(因為在 Swift 中單例太簡單了,並且持續的命名已經耗費了你太多的時間,你應該有更多的時間去抱怨為什麼單例是一個反模式的設計,但是避免花費太多時間,你的同伴會感謝你的。)
10.使用擴充套件來組織程式碼
擴充套件應該被用於組織程式碼。
一個例項的次要方法和屬性應該移動到擴充套件中。注意,現在並不是所有的屬性型別都支援移動到擴充套件中,為了做到最好,你應該在這個限制中使用擴充套件。
你應該使用擴充套件去幫助組織你的例項定義。一個比較好的例子是,一個 view controller 繼承了 table view data source 和 delegate protocols 。為了使table view中的程式碼最小化,把 data source 和 delegate 方法整合到擴充套件中以適應相應的 protocol 。
在一個單一的原始檔中,在你覺得能夠最好地組織程式碼的時候,把一些定義加入到擴充套件中。不要擔心把 main class 的方法或者 struct 中指向方法和屬性定義的方法加入擴充套件。只要所有檔案都包涵在一個 Swift 檔案中,那就是沒問題的。
反之,main 的例項定義不應該指向定義在超出 main Swift 檔案範圍的擴充套件的元素。
11.鏈式 Setters
對於簡單的 setters 屬性,不要使用鏈式 setters 方法當做便利的替代方法。
正確的做法:
1 2 |
instance.foo = 42 instance.bar = "xyzzy" |
錯誤的做法:
1 |
instance.setFoo(42).setBar("xyzzy") |
相較於鏈式setters,傳統的setters更為簡單和不需要過多的公式化。
12.錯誤處理
Swift 2.0 的 do/try/catch 機制非常棒。
13.避免使用try!
一般來說,使用如下寫法:
1 2 3 4 5 6 |
do { try somethingThatMightThrow() } catch { fatalError("Something bad happened.") } |
而不是:
1 |
try! somethingThatMightThrow() |
即使這種形式特別冗長,但是它提供了context讓其他開發者可以檢查這個程式碼。
在更詳盡的錯誤處理策略出來之前,如果把 try! 當做一個臨時的錯誤處理是沒問題的。但是建議你最好週期性地檢查你程式碼,找出其中任何有可能逃出你程式碼檢查的非法try!。
14.避免使用try?
try?是用來“壓制”錯誤,而且只有當你確信對錯誤的生成不關心時,try?才是有用的。一般來說,你應該捕獲錯誤並至少列印出錯誤。
15.過早返回&Guards
可能的話,使用guard宣告去處理過早的返回或者其他退出的情況(例如,fatal errors 或者 thorwn errors)。
正確的寫法:
1 2 3 4 |
guard let safeValue = criticalValue else { fatalError("criticalValue cannot be nil here") } someNecessaryOperation(safeValue) |
錯誤的寫法:
1 2 3 4 5 |
if let safeValue = criticalValue { someNecessaryOperation(safeValue) } else { fatalError("criticalValue cannot be nil here") } |
或者:
1 2 3 4 |
if criticalValue == nil { fatalError("criticalValue cannot be nil here") } someNecessaryOperation(criticalValue!) |
這個flatten code以其他方式進入一個if let 程式碼塊,並且在靠近相關的環境中過早地退出了,而不是進入else程式碼塊。
甚至當你沒有捕獲一個值(guard let),這個模式在編譯期間也會強制過早退出。在第二個if的例子裡,儘管程式碼flattend得像guard一樣,但是一個毀滅性的錯誤或者其他返回一些無法退出的程式(或者基於確切例項的非法態)將會導致crash。一個過早的退出發生時,guard宣告將會及時發現錯誤,並將其從else block中移除。
16.”Early”訪問控制
即使你的程式碼沒有分離成獨立的模組,你也應該經常考慮訪問控制。把一個定義標記為 private 或者 internal 對於程式碼來說相當於一個輕量級的文件。每一個閱讀程式碼的人都會知道這個元素是不能“觸碰”的。反之,把一個定義為 public 就相當於邀請其他程式碼去訪問這個元素。我們最好顯示地指明而不是依賴 Swift 的預設訪問控制等級。( internal )
如果你的程式碼庫在將來不斷擴張,它可能會被分解成子模組.這樣做,會使一個已經裝飾著訪問控制資訊的程式碼庫更加方便、快捷。
17.限制性的訪問控制
一般來來說,當新增訪問控制到你的程式碼時,最好有詳盡的限制。這裡,使用 private 比 internal 更有意義,而使用 internal 顯然比 public 更好。(注意: internal 是預設的)。
如有需要,把程式碼的訪問控制變得更加開放是非常容易的(沿著這樣的途徑: “private” to “internal” to “public”) 。過於開放的訪問控制程式碼被其他程式碼使用可能不是很合適。有足夠限制的程式碼能夠發現不合適和錯誤的使用,並且能提供更好的介面。一個例子就是一個型別公開地暴露了一個internal cache。
而且,程式碼的限制訪問限制了“暴露的表面積”,並且允許程式碼在更小影響其他程式碼的情況下重構。其他的技術如:Protocol Driven Development 也能起到同樣的作用。
TODO Section
- This is a list of headings for possible future expansion.
- Protocols & Protocol Driven Development
- Implicitly Unwrapped Optionals
- Reference vs Value Types
- Async Closures
- unowned vs weak
- Cocoa Delegates
- Immutable Structs
- Instance Initialisation
- Logging & Printing
- Computed Properties vs Functions
- Value Types and Equality