自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

如何在 Swift 中自定義操作符

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

[[408634]]

前言

很少有Swift功能能和使用自定義操作符的一樣產(chǎn)生如此多的激烈辯論。雖然有些人發(fā)現(xiàn)它們真的有用,可以降低代碼冗余,或?qū)嵤┹p量級(jí)語(yǔ)法擴(kuò)展,但其他人認(rèn)為應(yīng)該完全避免它們。

愛(ài)它們或者恨它們 —— 無(wú)論哪種方式都有一些真正有趣的事情,我們可以與自定義操作一起做 ——無(wú)論我們是否重載現(xiàn)有的東西或定義自己的東西。本周,讓我們來(lái)看看可以使用自定義操作符的一些情況,以及使用它們的一些優(yōu)點(diǎn)。

數(shù)字容器

有時(shí)我們定義了實(shí)質(zhì)上只是容器的值類型其容納著更加原始的值。例如,在一個(gè)戰(zhàn)略游戲中,玩家可以收集兩種資源 ——木材和金幣。要在代碼中建模這些資源,我使用作為木材和金幣值的容器的 Resource 結(jié)構(gòu)體,如下所示:

  1. struct Resources { 
  2.     var gold: Int 
  3.     var wood: Int 

每當(dāng)我引用一組資源時(shí),我就會(huì)使用此結(jié)構(gòu) —— 例如,要跟蹤玩家當(dāng)前可用的資源:

  1. struct Player { 
  2.     var resources: Resources 

您可以在游戲中花費(fèi)資源的一件事是為您的軍隊(duì)培訓(xùn)新單位。執(zhí)行此類動(dòng)作時(shí),我只需從當(dāng)前的玩家的資源中減去該單元的金幣和木材成本:

  1. func trainUnit(ofKind kind: Unit.Kind) { 
  2.     let unit = Unit(kind: kind) 
  3.     board.add(unit) 
  4.  
  5.     currentPlayer.resources.gold -= kind.cost.gold 
  6.     currentPlayer.resources.wood -= kind.cost.wood 

做到上面的完全有效,但由于游戲中有許多影響玩家資源的動(dòng)作,代碼中有許多地方必須重復(fù)金幣和木頭的兩個(gè)減法。

這不僅使得很容易忘記減少其中一個(gè)值,同時(shí)它還使得引入一種新的資源類型更難(例如,銀幣),因?yàn)槲冶仨毻ㄟ^(guò)查看整個(gè)代碼并更新所有處理資源的地方。

操作符重載

讓我們嘗試使用操作符重載來(lái)解決上述問(wèn)題。使用大多數(shù)語(yǔ)言(包括Swift)的操作符時(shí),您有都有兩個(gè)選項(xiàng),重載現(xiàn)有運(yùn)算符,或者創(chuàng)建一個(gè)新的運(yùn)算符。重載工作就像方法重載,您可以使用新的輸入或輸出創(chuàng)建新版本的操作符。

在這種情況下,我們將定義-=運(yùn)算符的過(guò)載,它們適用于兩個(gè) Resources 值,如下所示:

  1. extension Resources { 
  2.     static func -=(lhs: inout Resources, rhs: Resources) { 
  3.         lhs.gold -= rhs.gold 
  4.         lhs.wood -= rhs.wood 
  5.     } 

就像遵守 Equatable 協(xié)議的時(shí)候一樣,Swift 中的操作符重載只是可以在類型上聲明的一個(gè)正常靜態(tài)函數(shù)。在此處 -= 中,操作符的左側(cè)是一個(gè) inoiut 參數(shù),這是我們要修改的值。

通過(guò)我們的操作符重載,我們現(xiàn)在可以直接在當(dāng)前的玩家的資源上簡(jiǎn)單地調(diào)用 -= ,就像我們將其放在在任何原始數(shù)值上:

  1. currentPlayer.resources -= kind.cost 

這不僅很好閱讀,它還有助于我們消除代碼重復(fù)問(wèn)題。由于我們總是希望所有外部邏輯修改完整的 Resource 實(shí)例,因此我們可以將金幣 gold 和木材 wood 屬性作為只讀屬性開(kāi)放給外部其他類:

  1. struct Resources { 
  2.     private(set) var gold: Int 
  3.     private(set) var wood: Int 
  4.  
  5.     init(gold: Int, wood: Int) { 
  6.         self.gold = gold 
  7.         self.wood = wood 
  8.     } 

另一種實(shí)現(xiàn)方法 — 可變函數(shù)

另一種我們可以解決上面的 Resources 問(wèn)題的方法是使用可變函數(shù)而不是操作符重載。我們可以添加一個(gè)函數(shù),通過(guò)另一個(gè)實(shí)例減少 Resources 值的屬性,如下所示:

  1. extension Resources { 
  2.     mutating func reduce(by resources: Resources) { 
  3.         gold -= resources.gold 
  4.         wood -= resources.wood 
  5.     } 

這兩個(gè)解決方案都有它們的優(yōu)點(diǎn),您可以爭(zhēng)辯說(shuō)可變函數(shù)方法更明確。但是,您也不希望數(shù)學(xué)的標(biāo)準(zhǔn)減法API變成:5.reduce(by: 3),所以也許這是一個(gè)運(yùn)算符重載表現(xiàn)完美的地方。

布局計(jì)算

讓我們來(lái)看看另一種方案,其中使用操作符重載可能非常好。盡管我們擁有自動(dòng)布局和強(qiáng)大的布局API,但有時(shí)我們發(fā)現(xiàn)自己在某些情況下需要進(jìn)行手動(dòng)布局計(jì)算。

在這樣的情況下,它非常常見(jiàn),必須在二維值上進(jìn)行數(shù)學(xué)操作 —— 如 CGPoint,CGSize 和 CGVector。例如,我們可能需要通過(guò)使用圖像視圖的大小和一些額外的邊距來(lái)計(jì)算標(biāo)簽的原點(diǎn),如下所示:

  1. label.frame.origin = CGPoint( 
  2.     x: imageView.bounds.width + 10, 
  3.     y: imageView.bounds.height + 20 

如果我們可以簡(jiǎn)單地添加它們,而不是必須始終展開(kāi) point 和 size 來(lái)使用他們的底層組件,這會(huì)不會(huì)很好(就像上面對(duì) Resources 的操作一樣)?

為了能夠這樣做,我們可以通過(guò)重載+運(yùn)算符來(lái)接受兩個(gè) CGSize 實(shí)例作為輸入,并輸出 CGPoint 值:

  1. extension CGSize { 
  2.     static func +(lhs: CGSize, rhs: CGSize) -> CGPoint { 
  3.         return CGPoint( 
  4.             x: lhs.width + rhs.width, 
  5.             y: lhs.height + rhs.height 
  6.         ) 
  7.     } 

通過(guò)上面的代碼,我們現(xiàn)在可以寫(xiě)下我們的布局計(jì)算:

  1. label.frame.origin = imageView.bounds.size + CGSize(width: 10, height: 20) 

這很酷,但必須為我們的位置創(chuàng)造 CGSize 會(huì)感到有點(diǎn)奇怪。使這個(gè)有點(diǎn)更好的一種方法可以是定義另一個(gè) + 重載,該 + 重載接受包含兩個(gè) CGFloat 值的元組,如下所示:

  1. extension CGSize { 
  2.     static func +(lhs: CGSize, rhs: (x: CGFloat, y: CGFloat)) -> CGPoint { 
  3.         return CGPoint( 
  4.             x: lhs.width + rhs.x, 
  5.             y: lhs.height + rhs.y 
  6.         ) 
  7.     } 

這讓我們?cè)谶@兩種方式中的任何一個(gè)寫(xiě)下我們的布局計(jì)算:

  1. // 使用元組標(biāo)簽: 
  2. label.frame.origin = imageView.bounds.size + (x: 10, y: 20) 
  3.  
  4. // 或者不寫(xiě): 
  5. label.frame.origin = imageView.bounds.size + (10, 20) 

那非常緊湊,很好!但現(xiàn)在我們正在接近導(dǎo)致操作符的爭(zhēng)論出現(xiàn)的核心問(wèn)題 —— 平衡冗余程度和可讀性。由于我們?nèi)匀惶幚頂?shù)字,我認(rèn)為大多數(shù)人會(huì)發(fā)現(xiàn)上面的易于閱讀和理解,但隨著我們繼續(xù)自定義操作符的用途,它變得更加復(fù)雜,特別是當(dāng)我們開(kāi)始引入全新的操作符時(shí)。

處理錯(cuò)誤的自定義運(yùn)算符

到目前為止,我們還只是簡(jiǎn)單的重載了系統(tǒng)已經(jīng)存在的操作符。但是,如果我們想開(kāi)始使用無(wú)法真正映射到現(xiàn)有的功能的操作符,我們需要定義自己的。

讓我們來(lái)看看另一個(gè)例子。Swift 的 do,try,catch 錯(cuò)誤處理機(jī)制在處理無(wú)法使用的同步操作時(shí)超級(jí)漂亮。它可以讓我們?cè)诔霈F(xiàn)錯(cuò)誤后,輕松安全地退出函數(shù)。例如在加載磁盤上保存的數(shù)據(jù)模型時(shí):

  1. class NoteManager { 
  2.     func loadNote(fromFileNamed fileName: String) throws -> Note { 
  3.         let file = try fileLoader.loadFile(named: fileName) 
  4.         let data = try file.read() 
  5.         let note = try Note(data: data) 
  6.         return note 
  7.     } 

做出像上面的唯一主要的缺點(diǎn)是我們直接向我們功能的調(diào)用者拋出出任何潛在的錯(cuò)誤,需要減少 API 可以拋出的錯(cuò)誤量,否則做有意義的錯(cuò)誤處理和測(cè)試變得非常困難。

理想情況下,我們想要的是給定 API 可以拋出的有限錯(cuò)誤,這樣我們就可以輕松地單獨(dú)處理每種情況。讓我們說(shuō)我們也想捕捉所有潛在的錯(cuò)誤,讓我們同時(shí)擁有所有好的事情。因此,我們使用顯式 cases 定義一個(gè)錯(cuò)誤枚舉,每個(gè)錯(cuò)誤的枚舉都使用底層錯(cuò)誤的關(guān)聯(lián)值,如下所示:

  1. extension NoteManager { 
  2.     enum LoadingError: Error { 
  3.         case invalidFile(Error) 
  4.         case invalidData(Error) 
  5.         case decodingFailed(Error) 
  6.     } 

但是,捕獲潛在的錯(cuò)誤并將它們轉(zhuǎn)換為自己類型是棘手的。我們必須寫(xiě)下類似的標(biāo)準(zhǔn)錯(cuò)誤處理機(jī)制:

  1. class NoteManager { 
  2.     func loadNote(fromFileNamed fileName: String) throws -> Note { 
  3.         do { 
  4.             let file = try fileLoader.loadFile(named: fileName) 
  5.  
  6.             do { 
  7.                 let data = try file.read() 
  8.  
  9.                 do { 
  10.                     return try Note(data: data) 
  11.                 } catch { 
  12.                     throw LoadingError.decodingFailed(error) 
  13.                 } 
  14.             } catch { 
  15.                 throw LoadingError.invalidData(error) 
  16.             } 
  17.         } catch { 
  18.             throw LoadingError.invalidFile(error) 
  19.         } 
  20.     } 

我不認(rèn)為有人想要閱讀像上面的代碼。一個(gè)選項(xiàng)是介紹一個(gè) perform 函數(shù),我們可以用來(lái)把一個(gè)錯(cuò)誤轉(zhuǎn)換為另一個(gè)錯(cuò)誤:

  1. class NoteManager { 
  2.     func loadNote(fromFileNamed fileName: String) throws -> Note { 
  3.         let file = try perform(fileLoader.loadFile(named: fileName), 
  4.                                orThrow: LoadingError.invalidFile) 
  5.  
  6.         let data = try perform(file.read(), 
  7.                                orThrow: LoadingError.invalidData) 
  8.  
  9.         let note = try perform(Note(data: data), 
  10.                                orThrow: LoadingError.decodingFailed) 
  11.  
  12.         return note 
  13.     } 
  14.  
  15. func perform<T>(_ expression: @autoclosure () throws -> T, 
  16.                 errorTransform: (Error) -> Error) throws -> T { 
  17.     do { 
  18.         return try expression() 
  19.     } catch { 
  20.         throw errorTransform(error) 
  21.     } 

更好一點(diǎn)了,但我們?nèi)匀挥泻芏噱e(cuò)誤轉(zhuǎn)換代碼會(huì)對(duì)我們的實(shí)際邏輯造成混亂。讓我們看看引入新的操作符是否可以幫助我們清理此代碼。

添加新的操作符

我們首先定義我們的新運(yùn)營(yíng)商。在這種情況下,我們將選擇 〜> 作為符號(hào)(具有替代返回類型的動(dòng)機(jī),所以我們正在尋找類似于 ->)的東西。由于這是一個(gè)將在兩側(cè)工作操作符,因此我們將其定義為 infix,如下所示:

  1. infix operator ~> 

使操作符如此強(qiáng)大的是它們可以自動(dòng)捕捉它們兩側(cè)的上下文。將其與Swift 的 @autoclosure 功能相結(jié)合,我們可以創(chuàng)建一些非??岬臇|西。

讓我們實(shí)現(xiàn) 〜> 作為傳遞表達(dá)式和轉(zhuǎn)換錯(cuò)誤的操作符,拋出或返回與原始表達(dá)式相同的類型:

  1. func ~><T>(expression: @autoclosure () throws -> T, 
  2.            errorTransform: (Error) -> Error) throws -> T { 
  3.     do { 
  4.         return try expression() 
  5.     } catch { 
  6.         throw errorTransform(error) 
  7.     } 

那么上述這個(gè)操作符能夠讓我們做什么呢?由于枚舉具有關(guān)聯(lián)值的靜態(tài)函數(shù)在Swift中也是靜態(tài)函數(shù),我們可以簡(jiǎn)單地在我們的拋出表達(dá)式和錯(cuò)誤情況之間添加〜>操作符,我們希望將任何底層錯(cuò)誤轉(zhuǎn)換為如下形式:

  1. class NoteManager { 
  2.     func loadNote(fromFileNamed fileName: String) throws -> Note { 
  3.         let file = try fileLoader.loadFile(named: fileName) ~> LoadingError.invalidFile 
  4.         let data = try file.read() ~> LoadingError.invalidData 
  5.         let note = try Note(data: data) ~> LoadingError.decodingFailed 
  6.         return note 
  7.     } 

這很酷!通過(guò)使用操作符,我們已從我們的邏輯中刪除了大量的繁瑣代碼和語(yǔ)法,使我們的代碼更為聚焦。然而,缺點(diǎn)是我們引入了一個(gè)新的錯(cuò)誤處理語(yǔ)法,這可能是任何可能在未來(lái)加入我們項(xiàng)目的新開(kāi)發(fā)人員完全不熟悉的。

結(jié)論

自定義操作符和操作符重載是一個(gè)非常強(qiáng)大的功能,可以讓我們構(gòu)建非常有趣的解決方案。它可以讓我們降低呈現(xiàn)型函數(shù)調(diào)用的冗長(zhǎng),這可能會(huì)給我們清潔代碼。然而,它也可以是一個(gè)滑坡,可以引導(dǎo)我們編寫(xiě)隱秘的和難以閱讀的代碼,這對(duì)其他開(kāi)發(fā)人員來(lái)說(shuō)變得非常令人恐懼和混淆。

就像以更高級(jí)的方式使用第一類函數(shù)時(shí),我認(rèn)為在引入新的運(yùn)算符或創(chuàng)建額外的重載前,需要三思而后行。從其他開(kāi)發(fā)人員獲得反饋也可以超級(jí)有價(jià)值,作為一種新的操作符,對(duì)您的感覺(jué)和對(duì)別人的感覺(jué)完全不一樣。與如此多的事情一樣,理解權(quán)衡并試圖為每種情況挑選最合適的工具。

本文轉(zhuǎn)載自微信公眾號(hào)「Swift社區(qū)」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系Swift社區(qū)公眾號(hào)。

 

責(zé)任編輯:姜華 來(lái)源: Swift社區(qū)
相關(guān)推薦

2010-07-14 14:55:07

Perl操作符

2021-07-15 16:41:21

Swift查詢函數(shù)

2019-12-02 21:29:45

Keras神經(jīng)網(wǎng)絡(luò)TensorFlow

2021-07-16 11:00:40

Django用戶模型Python

2009-07-21 12:47:04

Scala私有字段定義操作符

2009-07-03 18:20:45

VSTS 2010網(wǎng)絡(luò)

2021-10-31 18:59:55

Python操作符用法

2017-06-20 12:48:55

React Nativ自定義模塊Note.js

2016-11-17 15:35:51

RxJava操作Subscriber

2009-08-19 17:26:28

C# 操作符

2018-09-26 10:25:27

Window 10自定義命令

2020-07-20 15:15:06

Swift函數(shù)代碼

2011-04-08 16:26:14

JavaScript

2010-07-19 11:00:24

Perl操作符

2010-07-14 14:30:31

Perl操作符

2010-03-01 11:10:41

WCF綁定元素

2021-10-09 13:48:11

操作符Python運(yùn)算符

2009-07-21 09:31:00

Scala操作符

2009-09-15 17:16:58

LINQ查詢操作符

2009-09-16 09:09:23

Linq Contai
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)