Swift 程式碼質量指標
以上是一些常見的程式碼質量指標。我們的目標是如何更好的使用Swift
編寫出符合程式碼質量指標要求的程式碼。
提示:本文不涉及設計模式/架構,更多關注如何通過合理使用
Swift
特性做部分程式碼段的重構。
一些不錯的實踐
1. 利用編譯檢查
減少使用Any/AnyObject
因為Any/AnyObject
缺少明確的型別資訊,編譯器無法進行型別檢查,會帶來一些問題:
- 編譯器無法檢查型別是否正確保證型別安全
- 程式碼中大量的
as?
轉換 - 型別的缺失導致編譯器無法做一些潛在的
編譯優化
使用as?
帶來的問題
當使用Any/AnyObject
時會頻繁使用as?
進行型別轉換。這好像沒什麼問題因為使用as?
並不會導致程式Crash
。不過程式碼錯誤至少應該分為兩類,一類是程式本身的錯誤通常會引發Crash,另外一種是業務邏輯錯誤。使用as?
只是避免了程式錯誤Crash
,但是並不能防止業務邏輯錯誤。
``` func do(data: Any?) { guard let string = data as? String else { return } // }
do(1) do("") ```
以上面的例子為例,我們進行了as?
轉換,當data
為String
時才會進行處理。但是當do
方法內String
型別發生了改變函式,使用方並不知道已變更沒有做相應的適配,這時候就會造成業務邏輯的錯誤。
提示:這類錯誤通常更難發現,這也是我們在一次真實
bug
場景遇到的。
使用自定義型別
代替Dictionary
程式碼中大量Dictionary
資料結構會降低程式碼可維護性,同時帶來潛在的bug
:
key
需要字串硬編碼,編譯時無法檢查value
沒有型別限制。修改
時型別無法限制,讀取時需要重複型別轉換和解包操作- 無法利用
空安全
特性,指定某個屬性必須有值
提示:
自定義型別
還有個好處,例如JSON
轉自定義型別
時會進行型別/nil/屬性名
檢查,可以避免將錯誤資料丟到下一層。
不推薦
let dic: [String: Any]
let num = dic["value"] as? Int
dic["name"] = "name"
推薦
struct Data {
let num: Int
var name: String?
}
let num = data.num
data.name = "name"
適合使用Dictionary
的場景
資料不使用
- 資料並不讀取
只是用來傳遞。解耦
- 1.
元件間通訊
解耦使用HashMap
傳遞引數進行通訊。 - 2.跨技術棧邊界的場景,
混合棧間通訊/前後端通訊
使用HashMap
/JSON
進行通訊。
- 1.
使用列舉關聯值
代替Any
例如使用列舉改造NSAttributedString
API,原有APIvalue
為Any
型別無法限制特定的型別。
優化前
let string = NSMutableAttributedString()
string.addAttribute(.foregroundColor, value: UIColor.red, range: range)
改造後
enum NSAttributedStringKey {
case foregroundColor(UIColor)
}
let string = NSMutableAttributedString()
string.addAttribute(.foregroundColor(UIColor.red), range: range) // 不傳遞Color會報錯
使用泛型
/協議關聯型別
代替Any
使用泛型
或協議關聯型別
代替Any
,通過泛型型別約束
來使編譯器進行更多的型別檢查。
_
使用列舉
/常量
代替硬編碼
程式碼中存在重複的硬編碼
字串/數字,在修改時可能會因為不同步引發bug
。儘可能減少硬編碼
字串/數字,使用列舉
或常量
代替。
使用KeyPath
代替字串
硬編碼
KeyPath
包含屬性名和型別資訊,可以避免硬編碼
字串,同時當屬性名或型別改變時編譯器會進行檢查。
不推薦
class SomeClass: NSObject {
@objc dynamic var someProperty: Int
init(someProperty: Int) {
self.someProperty = someProperty
}
}
let object = SomeClass(someProperty: 10)
object.observeValue(forKeyPath: "", of: nil, change: nil, context: nil)
推薦
let object = SomeClass(someProperty: 10)
object.observe(.someProperty) { object, change in
}
2. 記憶體安全
!
屬性會在讀取時隱式強解包
,當值不存在時產生執行時異常導致Crash。
class ViewController: UIViewController {
@IBOutlet private var label: UILabel! // @IBOutlet需要使用!
}
減少使用!
進行強解包
使用!
強解包會在值不存在時產生執行時異常導致Crash。
var num: Int?
let num2 = num! // 錯誤
提示:建議只在小範圍的區域性程式碼段使用
!
強解包。
避免使用try!
進行錯誤處理
使用try!
會在方法丟擲異常時產生執行時異常導致Crash。
try! method()
使用weak
/unowned
避免迴圈引用 √
``` resource.request().onComplete { [weak self] response in guard let self = self else { return } let model = self.updateModel(response) self.updateUI(model) }
resource.request().onComplete { [unowned self] response in let model = self.updateModel(response) self.updateUI(model) } ```
減少使用unowned
unowned
在值不存在時會產生執行時異常導致Crash,只有在確定self
一定會存在時才使用unowned
。
class Class {
@objc unowned var object: Object
@objc weak var object: Object?
}
unowned
/weak
區別:
weak
- 必須設定為可選值,會進行弱引用處理效能更差。會自動設定為nil
unowned
- 可以不設定為可選值,不會進行弱引用處理效能更好。但是不會自動設定為nil
, 如果self
已釋放會觸發錯誤.
錯誤處理方式
可選值
- 呼叫方並不關注內部可能會發生錯誤,當發生錯誤時返回nil
try/catch
- 明確提示呼叫方需要處理異常,需要實現Error
協議定義明確的錯誤型別assert
- 斷言。只能在Debug
模式下生效precondition
- 和assert
類似,可以再Debug
/Release
模式下生效fatalError
- 產生執行時崩潰會導致Crash,應避免使用Result
- 通常用於閉包
非同步回撥返回值
減少使用可選值
可選值
的價值在於通過明確標識值可能會為nil
並且編譯器強制對值進行nil
判斷。但是不應該隨意的定義可選值,可選值不能用let
定義,並且使用時必須進行解包
操作相對比較繁瑣。在程式碼設計時應考慮這個值是否有可能為nil
,只在合適的場景使用可選值。
使用init
注入代替可選值
屬性
不推薦
class Object {
var num: Int?
}
let object = Object()
object.num = 1
推薦
``` class Object { let num: Int
init(num: Int) { self.num = num } } let object = Object(num: 1) ```
避免隨意給予可選值預設值
在使用可選值時,通常我們需要在可選值為nil
時進行異常處理。有時候我們會通過給予可選值預設值
的方式來處理。但是這裡應考慮在什麼場景下可以給予預設值。在不能給予預設值的場景應當及時使用return
或丟擲異常
,避免錯誤的值被傳遞到更多的業務流程。
不推薦
func confirmOrder(id: String) {}
// 給予錯誤的值會導致錯誤的值被傳遞到更多的業務流程
confirmOrder(id: orderId ?? "")
推薦
``` func confirmOrder(id: String) {}
guard let orderId = orderId else { // 異常處理 return } confirmOrder(id: orderId) ```
提示:通常強業務相關的值不能給予預設值:例如
商品/訂單id
或是價格
。在可以使用兜底邏輯的場景使用預設值,例如預設文字/文字顏色
。
使用列舉優化可選值
前提Object
結構同時只會有一個值存在:
優化前
class Object {
var name: Int?
var num: Int?
}
優化後
降低記憶體佔用
-列舉關聯型別
的大小取決於最大的關聯型別大小邏輯更清晰
- 使用enum
相比大量使用if/else
邏輯更清晰
enum CustomType {
case name(String)
case num(Int)
}
減少var
屬性
使用計算屬性
使用計算屬性
可以減少多個變數同步帶來的潛在bug。
不推薦
class model {
var data: Object?
var loaded: Bool
}
model.data = Object()
loaded = false
複製程式碼
推薦
``` class model { var data: Object? var loaded: Bool { return data != nil } } model.data = Object()
```
提示:計算屬性因為每次都會重複計算,所以計算過程需要輕量避免帶來效能問題。
使用filter/reduce/map
代替for
迴圈
使用filter/reduce/map
可以帶來很多好處,包括更少的區域性變數,減少模板程式碼,程式碼更加清晰,可讀性更高。
不推薦
let nums = [1, 2, 3]
var result = []
for num in nums {
if num < 3 {
result.append(String(num))
}
}
// result = ["1", "2"]
推薦
let nums = [1, 2, 3]
let result = nums.filter { $0 < 3 }.map { String($0) }
// result = ["1", "2"]
使用guard
進行提前返回
推薦
guard !a else {
return
}
guard !b else {
return
}
// do
不推薦
if a {
if b {
// do
}
}
使用三元運算子?:
推薦
``` let b = true let a = b ? 1 : 2
let c: Int? let b = c ?? 1 複製程式碼 ```
不推薦
var a: Int?
if b {
a = 1
} else {
a = 2
}
使用for where
優化迴圈
for
迴圈新增where
語句,只有當where
條件滿足時才會進入迴圈
不推薦
for item in collection {
if item.hasProperty {
// ...
}
}
推薦
for item in collection where item.hasProperty {
// item.hasProperty == true,才會進入迴圈
}
使用defer
defer
可以保證在函式退出前一定會執行。可以使用defer
中實現退出時一定會執行的操作例如資源釋放
等避免遺漏。
func method() {
lock.lock()
defer {
lock.unlock()
// 會在method作用域結束的時候呼叫
}
// do
}
字串
使用"""
在定義複雜
字串時,使用多行字串字面量
可以保持原有字串的換行符號/引號等特殊字元,不需要使用``進行轉義。
``` let quotation = """ The White Rabbit put on his spectacles. "Where shall I begin, please your Majesty?" he asked.
"Begin at the beginning," the King said gravely, "and go on till you come to the end; then stop." """ 複製程式碼 ```
提示:上面字串中的
""
和換行可以自動保留。
使用字串插值
使用字串插值可以提高程式碼可讀性。
不推薦
let multiplier = 3
let message = String(multiplier) + "times 2.5 is" +
String((Double(multiplier) * 2.5))
推薦
let multiplier = 3
let message = "(multiplier) times 2.5 is (Double(multiplier) * 2.5)"
集合
使用標準庫提供的高階函式
不推薦
var nums = []
nums.count == 0
nums[0]
複製程式碼
推薦
var nums = []
nums.isEmpty
nums.first
訪問控制
Swift
中預設訪問控制級別為internal
。編碼中應當儘可能減小屬性
/方法
/型別
的訪問控制級別隱藏內部實現。
提示:同時也有利於編譯器進行優化。
使用private
/fileprivate
修飾私有屬性
和方法
private let num = 1
class MyClass {
private var num: Int
}
使用private(set)
修飾外部只讀/內部可讀寫屬性
class MyClass {
private(set) var num = 1
}
let num = MyClass().num
MyClass().num = 2 // 會編譯報錯
函式
使用引數預設值
使用引數預設值
,可以使呼叫方傳遞更少
的引數。
不推薦
func test(a: Int, b: String?, c: Int?) {
}
test(1, nil, nil)
推薦
func test(a: Int, b: String? = nil, c: Int? = nil) {
}
test(1)
提示:相比
ObjC
,引數預設值
也可以讓我們定義更少的方法。
限制引數數量
當方法引數過多時考慮使用自定義型別
代替。
不推薦
func f(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) {
}
複製程式碼
推薦
struct Params {
let a, b, c, d, e, f: Int
}
func f(params: Params) {
}
使用@discardableResult
某些方法使用方並不一定會處理返回值,可以考慮新增@discardableResult
標識提示Xcode
允許不處理返回值不進行warning
提示。
``` // 上報方法使用方不關心是否成功 func report(id: String) -> Bool {}
@discardableResult func report2(id: String) -> Bool {}
report("1") // 編譯器會警告 report2("1") // 不處理返回值編譯器不會警告 ```
元組
避免過長的元組
元組雖然具有型別資訊,但是並不包含變數名
資訊,使用方並不清晰知道變數的含義。所以當元組數量過多時考慮使用自定義型別
代替。
``` func test() -> (Int, Int, Int) {
} let (a, b, c) = test() // a,b,c型別一致,沒有命名資訊不清楚每個變數的含義 print("a (a), b: (b), c: (c) ") ```
系統庫
KVO
/Notification
使用 block
API
block
API的優勢:
KVO
可以支援KeyPath
- 不需要主動移除監聽,
observer
釋放時自動移除監聽
不推薦
``` class Object: NSObject { init() { super.init() addObserver(self, forKeyPath: "value", options: .new, context: nil) NotificationCenter.default.addObserver(self, selector: #selector(test), name: NSNotification.Name(rawValue: ""), object: nil) }
override class func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { }
@objc private func test() { }
deinit { removeObserver(self, forKeyPath: "value") NotificationCenter.default.removeObserver(self) } } ```
推薦
``` class Object: NSObject {
private var observer: AnyObserver? private var kvoObserver: NSKeyValueObservation?
init() { super.init() observer = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: ""), object: nil, queue: nil) { (_) in } kvoObserver = foo.observe(.value, options: [.new]) { (foo, change) in } } } ```
Protocol
使用protocol
代替繼承
Swift
中針對protocol
提供了很多新特性,例如預設實現
,關聯型別
,支援值型別。在程式碼設計時可以優先考慮使用protocol
來避免臃腫的父類同時更多使用值型別。
提示:一些無法用
protocol
替代繼承
的場景: - 1.需要繼承NSObject子類。 - 2.需要呼叫super
方法。 - 3.實現抽象類
的能力。
Extension
使用extension
組織程式碼
使用extension
將私有方法
/父類方法
/協議方法
等不同功能程式碼進行分離更加清晰/易維護。
class MyViewController: UIViewController {
// class stuff here
}
// MARK: - Private
extension: MyViewController {
private func method() {}
}
// MARK: - UITableViewDataSource
extension MyViewController: UITableViewDataSource {
// table view data source methods
}
// MARK: - UIScrollViewDelegate
extension MyViewController: UIScrollViewDelegate {
// scroll view delegate methods
}
程式碼風格
良好的程式碼風格可以提高程式碼的可讀性
,統一的程式碼風格可以降低團隊內相互理解成本
。對於Swift
的程式碼格式化
建議使用自動格式化工具實現,將自動格式化新增到程式碼提交流程,通過定義Lint規則
統一團隊內程式碼風格。考慮使用SwiftFormat
和SwiftLint
。
提示:
SwiftFormat
主要關注程式碼樣式的格式化,SwiftLint
可以使用autocorrect
自動修復部分不規範的程式碼。
常見的自動格式化修正
- 移除多餘的
;
- 最多隻保留一行換行
- 自動對齊
空格
- 限制每行的寬度
自動換行
作者:何樂樂 連結:http://juejin.cn/post/6984768684250120222\ 來源:稀土掘金 著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
————————————————————————————————————————————