新聞中心
自定義操作符和操作符重載是一個(gè)非常強(qiáng)大的功能,可以讓我們構(gòu)建非常有趣的解決方案。它可以讓我們降低呈現(xiàn)型函數(shù)調(diào)用的冗長(zhǎng),這可能會(huì)給我們清潔代碼

前言
很少有Swift功能能和使用自定義操作符的一樣產(chǎn)生如此多的激烈辯論。雖然有些人發(fā)現(xiàn)它們真的有用,可以降低代碼冗余,或?qū)嵤┹p量級(jí)語(yǔ)法擴(kuò)展,但其他人認(rèn)為應(yīng)該完全避免它們。
愛它們或者恨它們 —— 無(wú)論哪種方式都有一些真正有趣的事情,我們可以與自定義操作一起做 ——無(wú)論我們是否重載現(xiàn)有的東西或定義自己的東西。本周,讓我們來看看可以使用自定義操作符的一些情況,以及使用它們的一些優(yōu)點(diǎn)。
數(shù)字容器
有時(shí)我們定義了實(shí)質(zhì)上只是容器的值類型其容納著更加原始的值。例如,在一個(gè)戰(zhàn)略游戲中,玩家可以收集兩種資源 ——木材和金幣。要在代碼中建模這些資源,我使用作為木材和金幣值的容器的 Resource 結(jié)構(gòu)體,如下所示:
struct Resources {
var gold: Int
var wood: Int
}
每當(dāng)我引用一組資源時(shí),我就會(huì)使用此結(jié)構(gòu) —— 例如,要跟蹤玩家當(dāng)前可用的資源:
struct Player {
var resources: Resources
}
您可以在游戲中花費(fèi)資源的一件事是為您的軍隊(duì)培訓(xùn)新單位。執(zhí)行此類動(dòng)作時(shí),我只需從當(dāng)前的玩家的資源中減去該單元的金幣和木材成本:
func trainUnit(ofKind kind: Unit.Kind) {
let unit = Unit(kind: kind)
board.add(unit)
currentPlayer.resources.gold -= kind.cost.gold
currentPlayer.resources.wood -= kind.cost.wood
}
做到上面的完全有效,但由于游戲中有許多影響玩家資源的動(dòng)作,代碼中有許多地方必須重復(fù)金幣和木頭的兩個(gè)減法。
這不僅使得很容易忘記減少其中一個(gè)值,同時(shí)它還使得引入一種新的資源類型更難(例如,銀幣),因?yàn)槲冶仨毻ㄟ^查看整個(gè)代碼并更新所有處理資源的地方。
操作符重載
讓我們嘗試使用操作符重載來解決上述問題。使用大多數(shù)語(yǔ)言(包括Swift)的操作符時(shí),您有都有兩個(gè)選項(xiàng),重載現(xiàn)有運(yùn)算符,或者創(chuàng)建一個(gè)新的運(yùn)算符。重載工作就像方法重載,您可以使用新的輸入或輸出創(chuàng)建新版本的操作符。
在這種情況下,我們將定義-=運(yùn)算符的過載,它們適用于兩個(gè) Resources 值,如下所示:
extension Resources {
static func -=(lhs: inout Resources, rhs: Resources) {
lhs.gold -= rhs.gold
lhs.wood -= rhs.wood
}
}
就像遵守 Equatable 協(xié)議的時(shí)候一樣,Swift 中的操作符重載只是可以在類型上聲明的一個(gè)正常靜態(tài)函數(shù)。在此處 -= 中,操作符的左側(cè)是一個(gè) inoiut 參數(shù),這是我們要修改的值。
通過我們的操作符重載,我們現(xiàn)在可以直接在當(dāng)前的玩家的資源上簡(jiǎn)單地調(diào)用 -= ,就像我們將其放在在任何原始數(shù)值上:
currentPlayer.resources -= kind.cost
這不僅很好閱讀,它還有助于我們消除代碼重復(fù)問題。由于我們總是希望所有外部邏輯修改完整的 Resource 實(shí)例,因此我們可以將金幣 gold 和木材 wood 屬性作為只讀屬性開放給外部其他類:
struct Resources {
private(set) var gold: Int
private(set) var wood: Int
init(gold: Int, wood: Int) {
self.gold = gold
self.wood = wood
}
}
另一種實(shí)現(xiàn)方法 — 可變函數(shù)
另一種我們可以解決上面的 Resources 問題的方法是使用可變函數(shù)而不是操作符重載。我們可以添加一個(gè)函數(shù),通過另一個(gè)實(shí)例減少 Resources 值的屬性,如下所示:
extension Resources {
mutating func reduce(by resources: Resources) {
gold -= resources.gold
wood -= resources.wood
}
}
這兩個(gè)解決方案都有它們的優(yōu)點(diǎn),您可以爭(zhēng)辯說可變函數(shù)方法更明確。但是,您也不希望數(shù)學(xué)的標(biāo)準(zhǔn)減法API變成:5.reduce(by: 3),所以也許這是一個(gè)運(yùn)算符重載表現(xiàn)完美的地方。
布局計(jì)算
讓我們來看看另一種方案,其中使用操作符重載可能非常好。盡管我們擁有自動(dòng)布局和強(qiáng)大的布局API,但有時(shí)我們發(fā)現(xiàn)自己在某些情況下需要進(jìn)行手動(dòng)布局計(jì)算。
在這樣的情況下,它非常常見,必須在二維值上進(jìn)行數(shù)學(xué)操作 —— 如 CGPoint,CGSize 和 CGVector。例如,我們可能需要通過使用圖像視圖的大小和一些額外的邊距來計(jì)算標(biāo)簽的原點(diǎn),如下所示:
label.frame.origin = CGPoint(
x: imageView.bounds.width + 10,
y: imageView.bounds.height + 20
)
如果我們可以簡(jiǎn)單地添加它們,而不是必須始終展開 point 和 size 來使用他們的底層組件,這會(huì)不會(huì)很好(就像上面對(duì) Resources 的操作一樣)?
為了能夠這樣做,我們可以通過重載+運(yùn)算符來接受兩個(gè) CGSize 實(shí)例作為輸入,并輸出 CGPoint 值:
extension CGSize {
static func +(lhs: CGSize, rhs: CGSize) -> CGPoint {
return CGPoint(
x: lhs.width + rhs.width,
y: lhs.height + rhs.height
)
}
}
通過上面的代碼,我們現(xiàn)在可以寫下我們的布局計(jì)算:
label.frame.origin = imageView.bounds.size + CGSize(width: 10, height: 20)
這很酷,但必須為我們的位置創(chuàng)造 CGSize 會(huì)感到有點(diǎn)奇怪。使這個(gè)有點(diǎn)更好的一種方法可以是定義另一個(gè) + 重載,該 + 重載接受包含兩個(gè) CGFloat 值的元組,如下所示:
extension CGSize {
static func +(lhs: CGSize, rhs: (x: CGFloat, y: CGFloat)) -> CGPoint {
return CGPoint(
x: lhs.width + rhs.x,
y: lhs.height + rhs.y
)
}
}
這讓我們?cè)谶@兩種方式中的任何一個(gè)寫下我們的布局計(jì)算:
// 使用元組標(biāo)簽:
label.frame.origin = imageView.bounds.size + (x: 10, y: 20)
// 或者不寫:
label.frame.origin = imageView.bounds.size + (10, 20)
那非常緊湊,很好!但現(xiàn)在我們正在接近導(dǎo)致操作符的爭(zhēng)論出現(xiàn)的核心問題 —— 平衡冗余程度和可讀性。由于我們?nèi)匀惶幚頂?shù)字,我認(rèn)為大多數(shù)人會(huì)發(fā)現(xiàn)上面的易于閱讀和理解,但隨著我們繼續(xù)自定義操作符的用途,它變得更加復(fù)雜,特別是當(dāng)我們開始引入全新的操作符時(shí)。
處理錯(cuò)誤的自定義運(yùn)算符
到目前為止,我們還只是簡(jiǎn)單的重載了系統(tǒng)已經(jīng)存在的操作符。但是,如果我們想開始使用無(wú)法真正映射到現(xiàn)有的功能的操作符,我們需要定義自己的。
讓我們來看看另一個(gè)例子。Swift 的 do,try,catch 錯(cuò)誤處理機(jī)制在處理無(wú)法使用的同步操作時(shí)超級(jí)漂亮。它可以讓我們?cè)诔霈F(xiàn)錯(cuò)誤后,輕松安全地退出函數(shù)。例如在加載磁盤上保存的數(shù)據(jù)模型時(shí):
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ǎn)是我們直接向我們功能的調(diào)用者拋出出任何潛在的錯(cuò)誤,需要減少 API 可以拋出的錯(cuò)誤量,否則做有意義的錯(cuò)誤處理和測(cè)試變得非常困難。
理想情況下,我們想要的是給定 API 可以拋出的有限錯(cuò)誤,這樣我們就可以輕松地單獨(dú)處理每種情況。讓我們說我們也想捕捉所有潛在的錯(cuò)誤,讓我們同時(shí)擁有所有好的事情。因此,我們使用顯式 cases 定義一個(gè)錯(cuò)誤枚舉,每個(gè)錯(cuò)誤的枚舉都使用底層錯(cuò)誤的關(guān)聯(lián)值,如下所示:
extension NoteManager {
enum LoadingError: Error {
case invalidFile(Error)
case invalidData(Error)
case decodingFailed(Error)
}
}
但是,捕獲潛在的錯(cuò)誤并將它們轉(zhuǎn)換為自己類型是棘手的。我們必須寫下類似的標(biāo)準(zhǔn)錯(cuò)誤處理機(jī)制:
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)
}
}
}
我不認(rèn)為有人想要閱讀像上面的代碼。一個(gè)選項(xiàng)是介紹一個(gè) perform 函數(shù),我們可以用來把一個(gè)錯(cuò)誤轉(zhuǎn)換為另一個(gè)錯(cuò)誤:
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
}
}
func perform(_ expression: @autoclosure () throws -> T,
errorTransform: (Error) -> Error) throws -> T {
do {
return try expression()
} catch {
throw errorTransform(error)
}
}
更好一點(diǎn)了,但我們?nèi)匀挥泻芏噱e(cuò)誤轉(zhuǎn)換代碼會(huì)對(duì)我們的實(shí)際邏輯造成混亂。讓我們看看引入新的操作符是否可以幫助我們清理此代碼。
添加新的操作符
我們首先定義我們的新運(yùn)營(yíng)商。在這種情況下,我們將選擇 ?> 作為符號(hào)(具有替代返回類型的動(dòng)機(jī),所以我們正在尋找類似于 ->)的東西。由于這是一個(gè)將在兩側(cè)工作操作符,因此我們將其定義為 infix,如下所示:
infix operator ~>
使操作符如此強(qiáng)大的是它們可以自動(dòng)捕捉它們兩側(cè)的上下文。將其與Swift 的 @autoclosure 功能相結(jié)合,我們可以創(chuàng)建一些非??岬臇|西。
讓我們實(shí)現(xiàn) ?> 作為傳遞表達(dá)式和轉(zhuǎn)換錯(cuò)誤的操作符,拋出或返回與原始表達(dá)式相同的類型:
func ~>(expression: @autoclosure () throws -> T,
errorTransform: (Error) -> Error) throws -> T {
do {
return try expression()
} catch {
throw errorTransform(error)
}
}
那么上述這個(gè)操作符能夠讓我們做什么呢?由于枚舉具有關(guān)聯(lián)值的靜態(tài)函數(shù)在Swift中也是靜態(tài)函數(shù),我們可以簡(jiǎn)單地在我們的拋出表達(dá)式和錯(cuò)誤情況之間添加?>操作符,我們希望將任何底層錯(cuò)誤轉(zhuǎn)換為如下形式:
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
}
}
這很酷!通過使用操作符,我們已從我們的邏輯中刪除了大量的繁瑣代碼和語(yǔ)法,使我們的代碼更為聚焦。然而,缺點(diǎn)是我們引入了一個(gè)新的錯(cuò)誤處理語(yǔ)法,這可能是任何可能在未來加入我們項(xiàng)目的新開發(fā)人員完全不熟悉的。
結(jié)論
自定義操作符和操作符重載是一個(gè)非常強(qiáng)大的功能,可以讓我們構(gòu)建非常有趣的解決方案。它可以讓我們降低呈現(xiàn)型函數(shù)調(diào)用的冗長(zhǎng),這可能會(huì)給我們清潔代碼。然而,它也可以是一個(gè)滑坡,可以引導(dǎo)我們編寫隱秘的和難以閱讀的代碼,這對(duì)其他開發(fā)人員來說變得非常令人恐懼和混淆。
就像以更高級(jí)的方式使用第一類函數(shù)時(shí),我認(rèn)為在引入新的運(yùn)算符或創(chuàng)建額外的重載前,需要三思而后行。從其他開發(fā)人員獲得反饋也可以超級(jí)有價(jià)值,作為一種新的操作符,對(duì)您的感覺和對(duì)別人的感覺完全不一樣。與如此多的事情一樣,理解權(quán)衡并試圖為每種情況挑選最合適的工具。
網(wǎng)頁(yè)標(biāo)題:Swift中自定義操作符
本文地址:http://m.fisionsoft.com.cn/article/dghhhhs.html


咨詢
建站咨詢
