
知道 ObjectMapper 的人大概都見過在使用 Mappable 定義的模型中 func mapping(map: Map) {} 中需要寫很多 name <- map["name"] 這樣的代碼。這里的 <- 將模型中的屬性跟數(shù)據(jù)中的 key 對應(yīng)了起來。
Swift 提供的這種特性能夠減少很多的代碼量,也能極大的簡化語法。在標(biāo)準(zhǔn)庫或者是我們自己定義的一些類型中,有一些只是簡單的一些基本的值類型的容器,比如說 CGRect、CGSize、CGPoint 這些東西。或者直接使用 John Sundell 的文章 Custom operators in Swift 中的例子。在某個策略類游戲中,玩家能夠手機(jī)兩種資源木材還有金幣。為了要將兩種資源模型化,定義了 Resources 這個結(jié)構(gòu)體。
struct Resources {
var gold: Int
var wood: Int
}
當(dāng)然這些資源都是一個具體的玩家來使用或者賺取的。
struct Player {
var resources: Resources
}
用戶可以通過訓(xùn)練軍隊來使用這些資源。當(dāng)用戶訓(xùn)練軍隊的時候,都需要從用戶的 resources 里面減去對應(yīng)數(shù)量的金幣還有木材。比如用戶花費10個金幣20個木材訓(xùn)練了一個弓箭手(Archer)。
我們先定義弓箭手這個容器:
protocol Armyable {
var cost: Resources { get }
}
struct Archer: Armyable {
var cost: Resources = Resources(gold: 10, wood: 20)
}
在這個例子中我們首先定義了Armyable 這個協(xié)議來描述所有的軍隊類型。當(dāng)然在這個例子里面只有訓(xùn)練花費的資源也就是 cost 這一個東西。Archer 這個結(jié)構(gòu)體直接定義了訓(xùn)練一個弓箭手需要耗費的資源量。
現(xiàn)在再在 Player 這個方法里面定義訓(xùn)練軍隊的方法。
var board: [String]
mutating func trainArmy(_ unit: Armyable) {
resources.gold -= unit.cost.gold // line 1
resources.wood -= unit.cost.wood // line 2
board.append("弓箭手")
}
首先模擬的定義了一個數(shù)組來存放當(dāng)前的軍隊。然后定義了 trainArmy 這個方法來訓(xùn)練軍隊。這樣就完成了訓(xùn)練軍隊這個邏輯的編碼工作。但是可能你也想到了,在這類游戲中,有很多的情況需要操作用戶的資源,也就是說上面 line1 line2 之類的代碼會在這個游戲里寫很多次。如果你覺得只是重復(fù)寫點代碼沒什么的話,那么以后需要新增另外的什么資源的時候呢?恐怕就只能在整個代碼庫中找到所有相關(guān)的地方了。
操作符重載
這時候要是能夠用到數(shù)學(xué)符號 +、- 就完美了。Swift 也替我們想到了這點。我們可以自己定義一個操作符也可以重載一個已經(jīng)有了的操作符。操作符重載跟方法重載一樣。我們先重載 -= 這個符號。
extension Resources {
static func -= (lhs: inout Resources, rhs: Resources) {
lhs.gold -= rhs.gold
lhs.wood -= rhs.wood
}
}
跟 Equatable 一樣,Swift 中的操作符重載只是一個簡單的靜態(tài)方法。在 -= 這個方法里面,左邊的參數(shù)被標(biāo)記成了inout, 這個參數(shù)就是我們需要改變的值。有了 -= 這個操作符,我們現(xiàn)在就可以像操作數(shù)字一樣操作 resource
resources -= unit.cost
這么些不僅僅看起來或者讀起來很友好,也能夠幫助我們減少類似的代碼到處 copy 的問題。既然現(xiàn)在我們可以使用外部邏輯改變 resource ,現(xiàn)在甚至可以把 Resource 中的屬性改成只讀的。
struct Resources {
private(set) var gold: Int
private(set) var wood: Int
init(gold: Int, wood: Int) {
self.gold = gold
self.wood = wood
}
}
當(dāng)然我們也可以使用 mutating 方法來做這件事情。
extension Resources {
mutating func reduce(by resources: Resources) {
gold -= resources.gold
wood -= resources.wood
}
}
上面兩種方法都各有優(yōu)勢,你可以說使用 mutating 方法可以讓讀者更加明確代碼的含義。但是你肯定也不想標(biāo)準(zhǔn)庫中的減法變成
5.reduce(by: 3)這樣的。
布局運算中的操作符重載
還有一個場景就是剛剛提到了做 UI 布局的時候,涉及到的 CGRect、 CGPoint 等等。在做布局的時候經(jīng)常會涉及到需要對這些值進(jìn)行運算,如果能夠使用像上面那樣的方法來做這件事情不是很好的嗎?
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)
這樣已經(jīng)很好的,但是必須要創(chuàng)建一個 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)
}
}
然后就可以把上面的代碼進(jìn)一步簡化了:
label.frame.origin = imageView.bounds.size + (x: 10, y: 20)
// or
label.frame.origin = imageView.bounds.size + (10,20)
知道現(xiàn)在我們都還在操作數(shù)字相關(guān)的東西,大多數(shù)的人都能夠很輕松的去理解和閱讀這些代碼,但是如果是在涉及到一些特別的點,特別是需要引入新的操作符的時候,就需要好好去思考這樣做的必要性的。這是一個關(guān)于冗余代碼和可讀性代碼的關(guān)鍵點。
作者 John Sundel 有一個庫 CGOperators 是很多關(guān)于 Core Graphics 中的類的。
異常處理中的自定義操作符
到現(xiàn)在,我們已經(jīng)知道了如何去重載已有的操作符。有些時候我們還想要使用操作符來做一些操作,而在已經(jīng)存在的操作符中找不到對應(yīng)的,這種時候就需要自己去定義一個操作符了。
我們來舉個例子。 Swift 中的 do、try、 catch 是非常好的異常處理機(jī)制。它讓我們能夠很安全的從發(fā)生了異常的方法里退出,比如說下面這個從本地讀取數(shù)據(jù)的例子:
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
}
}
這么些最大的缺陷就是在遇到異常的時候,我們給調(diào)用者直接拋出了比較隱晦的異常。*“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)
}
}
}
不得不說這簡直就是一場災(zāi)難。相信沒人愿意讀到這樣的代碼吧!引入一個新的操作 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
}
}
這就好很多了,但是依然有很多異常處理相關(guān)的代碼會干擾主邏輯。下面我們來看看引入新的操作符之后會是什么樣的情況。
自定義操作符
我們現(xiàn)在來自定義一個操作符。我選擇了 ~> 。
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
操作符能夠如此強(qiáng)大的原因在于它能夠捕獲到兩邊的上下文。結(jié)合 Swift 的 @autoclosure 特性我們就可以做一些很酷的事情了。
請我們來實現(xiàn)這個操作符吧!讓它接受一個能夠拋出一場的表達(dá)式,以及一個異常轉(zhuǎn)換的表達(dá)式。返回原來的值或者是原來的異常。
func ~><T>(expression: @autoclosure () throws -> T,
errorTransform: (Error) -> Error) throws -> T {
do {
return try expression()
} catch {
throw errorTransform(error)
}
}
這一段代碼能夠讓我們很夠簡單的通過在操作和異常之間添加 ~> 來表達(dá)具體執(zhí)行的任務(wù)以及可能遇到的異常。之前的代碼就可以改成這樣了:
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
}
}
怎么樣,通過引入一個操作符,我們可以移除掉很多干擾閱讀的代碼。但是缺點就是,由于引入了新的操作符,這對新人來說,這會是額外的學(xué)習(xí)成本。
總結(jié)
自定義操作符以及操作符重載是 Swift 中一個很強(qiáng)大的特性,它能夠幫助你很輕松的去構(gòu)建一些解決方案。它能夠幫助我們減少在相似邏輯中的代碼復(fù)制,讓代碼更干凈。但是它也可能會讓你一不小心就寫出了隱晦,閱讀不友好的代碼。
在引入自定義操作符或者是想要重載某個操作符的時候,還是需要好好想一想利弊。從其他同事或者同行那里尋求建議是一個非常有效的方法,新的操作符對你自己來說可能很好,但是別人看起來可能會覺得很奇怪。同其他很多的事情一樣,這其實就是一個關(guān)于權(quán)衡的話題,我們需要為每種情況選擇最合適的解決方案。