知道 ObjectMapper
的人大概都見過在使用 Mappable
定義的模型中 func mapping(map: Map) {}
中須要寫不少 name <- map["name"]
這樣的代碼。這裏的 <-
將模型中的屬性跟數據中的 key
對應了起來。git
Swift 提供的這種特性可以減小不少的代碼量,也能極大的簡化語法。在標準庫或者是咱們本身定義的一些類型中,有一些只是簡單的一些基本的值類型的容器,好比說 CGRect
、CGSize
、CGPoint
這些東西。或者直接使用 John Sundell 的文章 Custom operators in Swift 中的例子。在某個策略類遊戲中,玩家可以收集兩種資源木材還有金幣。爲了要將兩種資源模型化,定義了 Resources
這個結構體。github
struct Resources {
var gold: Int
var wood: Int
}
複製代碼
固然這些資源都是一個具體的玩家來使用或者賺取的。express
struct Player {
var resources: Resources
}
複製代碼
用戶能夠經過訓練軍隊來使用這些資源。當用戶訓練軍隊的時候,都須要從用戶的 resources
裏面減去對應數量的金幣還有木材。好比用戶花費10個金幣20個木材訓練了一個弓箭手(Archer
)。swift
咱們先定義弓箭手這個容器:api
protocol Armyable {
var cost: Resources { get }
}
struct Archer: Armyable {
var cost: Resources = Resources(gold: 10, wood: 20)
}
複製代碼
在這個例子中咱們首先定義了Armyable
這個協議來描述全部的軍隊類型。固然在這個例子裏面只有訓練花費的資源也就是 cost
這一個東西。Archer
這個結構體直接定義了訓練一個弓箭手須要耗費的資源量。數組
如今再在 Player
這個方法裏面定義訓練軍隊的方法。安全
var board: [String]
mutating func trainArmy(_ unit: Armyable) {
resources.gold -= unit.cost.gold // line 1
resources.wood -= unit.cost.wood // line 2
board.append("弓箭手")
}
複製代碼
首先模擬的定義了一個數組來存放當前的軍隊。而後定義了 trainArmy
這個方法來訓練軍隊。這樣就完成了訓練軍隊這個邏輯的編碼工做。可是可能你也想到了,在這類遊戲中,有不少的狀況須要操做用戶的資源,也就是說上面 line1 line2 之類的代碼會在這個遊戲裏寫不少次。若是你以爲只是重複寫點代碼沒什麼的話,那麼之後須要新增另外的什麼資源的時候呢?恐怕就只能在整個代碼庫中找到全部相關的地方了。app
這時候要是可以用到數學符號 +
、-
就完美了。Swift 也替咱們想到了這點。咱們能夠本身定義一個操做符也能夠重載一個已經有了的操做符。操做符重載跟方法重載同樣。咱們先重載 -=
這個符號。佈局
extension Resources {
static func -= (lhs: inout Resources, rhs: Resources) {
lhs.gold -= rhs.gold
lhs.wood -= rhs.wood
}
}
複製代碼
跟 Equatable
同樣,Swift 中的操做符重載只是一個簡單的靜態方法。在 -=
這個方法裏面,左邊的參數被標記成了inout
, 這個參數就是咱們須要改變的值。有了 -=
這個操做符,咱們如今就能夠像操做數字同樣操做 resourcepost
resources -= unit.cost
複製代碼
這麼些不只僅看起來或者讀起來很友好,也可以幫助咱們減小相似的代碼處處 copy 的問題。既然如今咱們可使用外部邏輯改變 resource ,如今甚至能夠把 Resource 中的屬性改爲只讀的。
struct Resources {
private(set) var gold: Int
private(set) var wood: Int
init(gold: Int, wood: Int) {
self.gold = gold
self.wood = wood
}
}
複製代碼
固然咱們也可使用 mutating
方法來作這件事情。
extension Resources {
mutating func reduce(by resources: Resources) {
gold -= resources.gold
wood -= resources.wood
}
}
複製代碼
上面兩種方法都各有優點,你能夠說使用 mutating 方法可讓讀者更加明確代碼的含義。可是你確定也不想標準庫中的減法變成
5.reduce(by: 3)
這樣的。
還有一個場景就是剛剛提到了作 UI 佈局的時候,涉及到的 CGRect、 CGPoint 等等。在作佈局的時候常常會涉及到須要對這些值進行運算,若是可以使用像上面那樣的方法來作這件事情不是很好的嗎?
extension CGSize {
static func + (lhs: CGSize, rhs: CGSize) -> CGPoint {
return CGPoint(x: lhs.width + rhs.width,
y: lhs.height + rhs.height)
}
}
複製代碼
這段代碼,重載了 +
這個操做符,接受兩個 CGSize, 返回 CGPoint。而後就能夠這樣寫了
label.frame.origin = imageView.bounds.size + CGSize(width: 10, height: 20)
複製代碼
這樣已經很好的,可是必需要建立一個 CGSize 對象確實還不夠好。因此咱們再多定義一個 +
這個操做符接受一個元組:
extension CGSize {
static func + (lhs: CGSize, rhs: (x: CGFloat, y: CGFloat)) -> CGPoint {
return CGPoint(
x: lhs.width + rhs.x,
y: lhs.height + rhs.y)
}
}
複製代碼
而後就能夠把上面的代碼進一步簡化了:
label.frame.origin = imageView.bounds.size + (x: 10, y: 20)
// or
label.frame.origin = imageView.bounds.size + (10,20)
複製代碼
知道如今咱們都還在操做數字相關的東西,大多數的人都可以很輕鬆的去理解和閱讀這些代碼,可是若是是在涉及到一些特別的點,特別是須要引入新的操做符的時候,就須要好好去思考這樣作的必要性的。這是一個關於冗餘代碼和可讀性代碼的關鍵點。
做者 John Sundel 有一個庫 CGOperators 是不少關於 Core Graphics 中的類的。
到如今,咱們已經知道了如何去重載已有的操做符。有些時候咱們還想要使用操做符來作一些操做,而在已經存在的操做符中找不到對應的,這種時候就須要本身去定義一個操做符了。
咱們來舉個例子。 Swift 中的 do
、try
、 catch
是很是好的異常處理機制。它讓咱們可以很安全的從發生了異常的方法裏退出,好比說下面這個從本地讀取數據的例子:
class NoteManager {
func loadNote(fromFileNamed fileName: String) throws -> Note {
let file = try fileLoader.loadFile(named: fileName)
let data = try file.read()
let note = try Note(data: data)
return note
}
}
複製代碼
這麼些最大的缺陷就是在遇到異常的時候,咱們給調用者直接拋出了比較隱晦的異常。*「Providing a unified Swift error API」 這篇文章聊過減小一個 API 可以拋出異常的總量的好處。
這種狀況下,咱們想要的異常實際上是有限的,這樣咱們就可以很輕鬆的處理每一種異常狀況。可是,咱們仍是像捕獲到全部的異常,得到每一個異常的消息,咱們能夠定義一個枚舉:
extension NoteManager {
enum LoadingError: Error {
case invalidFile(Error)
case invalidData(Error)
case decodingFailed(Error)
}
}
複製代碼
這樣就能夠將各類異常消息歸類,而且不會影響到外界知道這個錯誤的具體信息。可是這樣寫代碼就會變成這樣了:
class NoteManager {
func loadNote(fromFileNamed fileName: String) throws -> Note {
do {
let file = try fileLoader.loadFile(named: fileName)
do {
let data = try file.read()
do {
return try Note(data: data)
} catch {
throw LoadingError.decodingFailed(error)
}
} catch {
throw LoadingError.invalidData(error)
}
} catch {
throw LoadingError.invalidFile(error)
}
}
}
複製代碼
不得不說這簡直就是一場災難。相信沒人願意讀到這樣的代碼吧!引入一個新的操做 perform
可讓代碼看起來更友好一些:
class NoteManager {
func loadNote(fromFileNamed fileName: String) throws -> Note {
let file = try perform(fileLoader.loadFile(named: fileName),
orThrow: LoadingError.invalidFile)
let data = try perform(file.read(),
orThrow: LoadingError.invalidData)
let note = try perform(Note(data: data),
orThrow: LoadingError.decodingFailed)
return note
}
}
複製代碼
這就好不少了,可是依然有不少異常處理相關的代碼會干擾主邏輯。下面咱們來看看引入新的操做符以後會是什麼樣的狀況。
咱們如今來自定義一個操做符。我選擇了 ~>
。
infix operator ~>
複製代碼
prefix operator &*& {} //定義左操做符
infix operator ** {} //定義中操做符
postfix operator && {} //定義右操做符
prefix func &*&(a: Int) -> Int { ... }
postfix func &&(a: Int) -> Int { ... }
// let c = 1&&
// let b = &*&1
// let a = 1 ** 2
複製代碼
操做符可以如此強大的緣由在於它可以捕獲到兩邊的上下文。結合 Swift 的 @autoclosure
特性咱們就能夠作一些很酷的事情了。
請咱們來實現這個操做符吧!讓它接受一個可以拋出一場的表達式,以及一個異常轉換的表達式。返回原來的值或者是原來的異常。
func ~><T>(expression: @autoclosure () throws -> T,
errorTransform: (Error) -> Error) throws -> T {
do {
return try expression()
} catch {
throw errorTransform(error)
}
}
複製代碼
這一段代碼可以讓咱們很夠簡單的經過在操做和異常之間添加 ~>
來表達具體執行的任務以及可能遇到的異常。以前的代碼就能夠改爲這樣了:
class NoteManager {
func loadNote(fromFileNamed fileName: String) throws -> Note {
let file = try fileLoader.loadFile(named: fileName) ~> LoadingError.invalidFile
let data = try file.read() ~> LoadingError.invalidData
let note = try Note(data: data) ~> LoadingError.decodingFailed
return note
}
}
複製代碼
怎麼樣,經過引入一個操做符,咱們能夠移除掉不少干擾閱讀的代碼。可是缺點就是,因爲引入了新的操做符,這對新人來講,這會是額外的學習成本。
自定義操做符以及操做符重載是 Swift 中一個很強大的特性,它可以幫助你很輕鬆的去構建一些解決方案。它可以幫助咱們減小在類似邏輯中的代碼複製,讓代碼更乾淨。可是它也可能會讓你一不當心就寫出了隱晦,閱讀不友好的代碼。
在引入自定義操做符或者是想要重載某個操做符的時候,仍是須要好好想想利弊。從其餘同事或者同行那裏尋求建議是一個很是有效的方法,新的操做符對你本身來講可能很好,可是別人看起來可能會以爲很奇怪。同其餘不少的事情同樣,這其實就是一個關於權衡的話題,咱們須要爲每種狀況選擇最合適的解決方案。