Swift 中關(guān)于操作符的那些事兒

image

知道 ObjectMapper 的人大概都見過在使用 Mappable 定義的模型中 func mapping(map: Map) {} 中需要寫很多 name <- map["name"] 這樣的代碼。這里的 <- 將模型中的屬性跟數(shù)據(jù)中的 key 對應(yīng)了起來。

Swift 提供的這種特性能夠減少很多的代碼量,也能極大的簡化語法。在標(biāo)準(zhǔn)庫或者是我們自己定義的一些類型中,有一些只是簡單的一些基本的值類型的容器,比如說 CGRect、CGSizeCGPoint 這些東西。或者直接使用 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 中的 dotry、 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)衡的話題,我們需要為每種情況選擇最合適的解決方案。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容