Swift - 高級運(yùn)算符

高級運(yùn)算符

除了之前介紹過的 基本運(yùn)算符兢孝,Swift 還提供了數(shù)種可以對數(shù)值進(jìn)行復(fù)雜運(yùn)算的高級運(yùn)算符袖瞻。它們包含了在 C 和 Objective-C 中已經(jīng)被大家所熟知的位運(yùn)算符和移位運(yùn)算符阔馋。

與 C 語言中的算術(shù)運(yùn)算符不同扶欣,Swift 中的算術(shù)運(yùn)算符默認(rèn)是不會(huì)溢出的械媒。所有溢出行為都會(huì)被捕獲并報(bào)告為錯(cuò)誤掖桦。如果想讓系統(tǒng)允許溢出行為冕香,可以選擇使用 Swift 中另一套默認(rèn)支持溢出的運(yùn)算符,比如溢出加法運(yùn)算符(&+)镣奋。所有的這些溢出運(yùn)算符都是以 & 開頭的币呵。

自定義結(jié)構(gòu)體、類和枚舉時(shí)侨颈,如果也為它們提供標(biāo)準(zhǔn) Swift 運(yùn)算符的實(shí)現(xiàn)富雅,將會(huì)非常有用。在 Swift 中為這些運(yùn)算符提供自定義的實(shí)現(xiàn)非常簡單肛搬,運(yùn)算符也會(huì)針對不同類型使用對應(yīng)實(shí)現(xiàn)。

我們不用被預(yù)定義的運(yùn)算符所限制毕贼。在 Swift 中可以自由地定義中綴温赔、前綴、后綴和賦值運(yùn)算符鬼癣,它們具有自定義的優(yōu)先級與關(guān)聯(lián)值陶贼。這些運(yùn)算符在代碼中可以像預(yù)定義的運(yùn)算符一樣使用,你甚至可以擴(kuò)展已有的類型以支持自定義運(yùn)算符待秃。

位運(yùn)算符

位運(yùn)算符可以操作數(shù)據(jù)結(jié)構(gòu)中每個(gè)獨(dú)立的比特位拜秧。它們通常被用在底層開發(fā)中,比如圖形編程和創(chuàng)建設(shè)備驅(qū)動(dòng)章郁。位運(yùn)算符在處理外部資源的原始數(shù)據(jù)時(shí)也十分有用枉氮,比如對自定義通信協(xié)議傳輸?shù)臄?shù)據(jù)進(jìn)行編碼和解碼。

Swift 支持 C 語言中的全部位運(yùn)算符暖庄,接下來會(huì)一一介紹聊替。

Bitwise NOT Operator(按位取反運(yùn)算符)

按位取反運(yùn)算符(~對一個(gè)數(shù)值的全部比特位進(jìn)行取反:

image.png

按位取反運(yùn)算符是一個(gè)前綴運(yùn)算符,直接放在運(yùn)算數(shù)之前培廓,并且它們之間不能添加任何空格:

let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits 
// 等于 0b11110000

UInt8 類型的整數(shù)有 8 個(gè)比特位惹悄,可以存儲(chǔ) 0 ~ 255 之間的任意整數(shù)。這個(gè)例子初始化了一個(gè) UInt8 類型的整數(shù)肩钠,并賦值為二進(jìn)制的 00001111泣港,它的前 4 位為 0,后 4 位為 1价匠。這個(gè)值等價(jià)于十進(jìn)制的 15当纱。

接著使用按位取反運(yùn)算符創(chuàng)建了一個(gè)名為 invertedBits 的常量,這個(gè)常量的值與全部位取反后的 initialBits 相等踩窖。即所有的 0 都變成了 1惫东,同時(shí)所有的 1 都變成 0invertedBits 的二進(jìn)制值為 11110000,等價(jià)于無符號(hào)十進(jìn)制數(shù)的 240廉沮。

Bitwise AND Operator(按位與運(yùn)算符)

按位與運(yùn)算符(& 對兩個(gè)數(shù)的比特位進(jìn)行合并颓遏。它返回一個(gè)新的數(shù),只有當(dāng)兩個(gè)數(shù)的對應(yīng)位1 的時(shí)候滞时,新數(shù)的對應(yīng)位才為 1

image.png

在下面的示例當(dāng)中叁幢,firstSixBitslastSixBits 中間 4 個(gè)位的值都為 1。使用按位與運(yùn)算符之后坪稽,得到二進(jìn)制數(shù)值 00111100曼玩,等價(jià)于無符號(hào)十進(jìn)制數(shù)的 60

let firstSixBits: UInt8 = 0b11111100
let lastSixBits: UInt8  = 0b00111111
let middleFourBits = firstSixBits & lastSixBits 
// 等于 00111100

Bitwise OR Operator(按位或運(yùn)算符)

按位或運(yùn)算符(|可以對兩個(gè)數(shù)的比特位進(jìn)行比較。它返回一個(gè)新的數(shù)窒百,只要兩個(gè)數(shù)的對應(yīng)位中有任意一個(gè)1 時(shí)黍判,新數(shù)的對應(yīng)位就為 1

image.png

在下面的示例中,someBitsmoreBits 存在不同的位被設(shè)置為 1篙梢。使用按位或運(yùn)算符之后顷帖,得到二進(jìn)制數(shù)值 11111110,等價(jià)于無符號(hào)十進(jìn)制數(shù)的 254

let someBits: UInt8 = 0b10110010
let moreBits: UInt8 = 0b01011110
let combinedbits = someBits | moreBits 
// 等于 11111110

Bitwise XOR Operator(按位異或運(yùn)算符)

按位異或運(yùn)算符渤滞,或稱“排外的或運(yùn)算符”(^)贬墩,可以對兩個(gè)數(shù)的比特位進(jìn)行比較。它返回一個(gè)新的數(shù)妄呕,當(dāng)兩個(gè)數(shù)的對應(yīng)位不相同時(shí)陶舞,新數(shù)的對應(yīng)位就為 1,并且對應(yīng)位相同時(shí)則為 0

image.png

在下面的示例當(dāng)中绪励,firstBitsotherBits 都有一個(gè)自己為 1肿孵,而對方為 0 的位。按位異或運(yùn)算符將新數(shù)的這兩個(gè)位都設(shè)置為 1疏魏。在其余的位上 firstBitsotherBits 是相同的颁井,所以設(shè)置為 0

let firstBits: UInt8 = 0b00010100
let otherBits: UInt8 = 0b00000101
let outputBits = firstBits ^ otherBits 
// 等于 00010001

Bitwise Left and Right Shift Operators(按位左移、右移運(yùn)算符)

按位左移運(yùn)算符(<<按位右移運(yùn)算符(>>可以對一個(gè)數(shù)的所有位進(jìn)行指定位數(shù)的左移和右移蠢护,但是需要遵守下面定義的規(guī)則雅宾。

對一個(gè)數(shù)進(jìn)行按位左移或按位右移,相當(dāng)于對這個(gè)數(shù)進(jìn)行乘以 2 或除以 2 的運(yùn)算葵硕。將一個(gè)整數(shù)左移一位眉抬,等價(jià)于將這個(gè)數(shù)乘以 2,同樣地懈凹,將一個(gè)整數(shù)右移一位蜀变,等價(jià)于將這個(gè)數(shù)除以 2。

無符號(hào)整數(shù)的移位運(yùn)算

對無符號(hào)整數(shù)進(jìn)行移位的規(guī)則如下:

  1. 已存在的位按指定的位數(shù)進(jìn)行左移和右移介评。

  2. 任何因移動(dòng)而超出整型存儲(chǔ)范圍的位都會(huì)被丟棄库北。

  3. 0 來填充移位后產(chǎn)生的空白位爬舰。

這種方法稱為邏輯移位

以下這張圖展示了 11111111 << 1(即把 11111111 向左移動(dòng) 1 位)寒瓦,和 11111111 >> 1(即把 11111111 向右移動(dòng) 1 位)的結(jié)果情屹。藍(lán)色的數(shù)字是被移位的,灰色的數(shù)字是被拋棄的杂腰,橙色的 0 則是被填充進(jìn)來的:

image.png

下面的代碼演示了 Swift 中的移位運(yùn)算:

let shiftBits: UInt8 = 4 // 即二進(jìn)制的 00000100
shiftBits << 1           // 00001000
shiftBits << 2           // 00010000
shiftBits << 5           // 10000000
shiftBits << 6           // 00000000
shiftBits >> 2           // 00000001

可以使用移位運(yùn)算對其他的數(shù)據(jù)類型進(jìn)行編碼和解碼:

let pink: UInt32 = 0xCC6699
let redComponent = (pink & 0xFF0000) >> 16  // redComponent 是 0xCC垃你,即 204
let greenComponent = (pink & 0x00FF00) >> 8 // greenComponent 是 0x66, 即 102
let blueComponent = pink & 0x0000FF         // blueComponent 是 0x99喂很,即 153

這個(gè)示例使用了一個(gè)命名為 pinkUInt32 型常量來存儲(chǔ) Cascading Style Sheets(CSS)中粉色的顏色值惜颇。該 CSS 的顏色值 #CC6699,在 Swift 中表示為十六進(jìn)制的 0xCC6699少辣。然后利用按位與運(yùn)算符(&)和按位右移運(yùn)算符(>>)從這個(gè)顏色值中分解出紅(CC)凌摄、綠(66)以及藍(lán)(99)三個(gè)部分。

紅色部分是通過對 0xCC66990xFF0000 進(jìn)行按位與運(yùn)算后得到的漓帅。0xFF0000 中的 0 部分“掩蓋”了 OxCC6699 中的第二锨亏、第三個(gè)字節(jié),使得數(shù)值中的 6699 被忽略煎殷,只留下 0xCC0000

然后腿箩,將這個(gè)數(shù)向右移動(dòng) 16 位(>> 16)豪直。十六進(jìn)制中每兩個(gè)字符占用 8 個(gè)比特位,所以移動(dòng) 16 位后 0xCC0000 就變?yōu)?0x0000CC珠移。這個(gè)數(shù)和 0xCC 是等同的弓乙,也就是十進(jìn)制數(shù)值的 204

同樣的钧惧,綠色部分通過對 0xCC66990x00FF00 進(jìn)行按位與運(yùn)算得到 0x006600暇韧。然后將這個(gè)數(shù)向右移動(dòng) 8 位,得到 0x66浓瞪,也就是十進(jìn)制數(shù)值的 102懈玻。

最后,藍(lán)色部分通過對 0xCC66990x0000FF 進(jìn)行按位與運(yùn)算得到 0x000099乾颁。因?yàn)?0x000099 已經(jīng)等于 0x99 涂乌,也就是十進(jìn)制數(shù)值的 153,所以這個(gè)值就不需要再向右移位了英岭。

有符號(hào)整數(shù)的移位運(yùn)算

對比無符號(hào)整數(shù)湾盒,有符號(hào)整數(shù)的移位運(yùn)算相對復(fù)雜得多,這種復(fù)雜性源于有符號(hào)整數(shù)的二進(jìn)制表現(xiàn)形式诅妹。(為了簡單起見罚勾,以下的示例都是基于 8 比特的有符號(hào)整數(shù)毅人,但是其中的原理對任何位數(shù)的有符號(hào)整數(shù)都是通用的。)

有符號(hào)整數(shù)使用第 1 個(gè)比特位(通常被稱為符號(hào)位)來表示這個(gè)數(shù)的正負(fù)尖殃。符號(hào)位為 0 代表正數(shù)丈莺,為 1 代表負(fù)數(shù)。

其余的比特位(通常被稱為數(shù)值位)存儲(chǔ)了實(shí)際的值分衫。有符號(hào)正整數(shù)和無符號(hào)數(shù)的存儲(chǔ)方式是一樣的场刑,都是從 0 開始算起。這是值為 4Int8 型整數(shù)的二進(jìn)制位表現(xiàn)形式:

image.png

符號(hào)位為 0(代表這是一個(gè)“正數(shù)”)蚪战,另外 7 位則代表了十進(jìn)制數(shù)值 4 的二進(jìn)制表示牵现。

負(fù)數(shù)的存儲(chǔ)方式略有不同。它存儲(chǔ) 2n 次方減去其實(shí)際值的絕對值邀桑,這里的 n 是數(shù)值位的位數(shù)瞎疼。一個(gè) 8 比特位的數(shù)有 7 個(gè)比特位是數(shù)值位,所以是 27 次方壁畸,即 128贼急。

這是值為 -4Int8 型整數(shù)的二進(jìn)制表現(xiàn)形式:

image.png

這次的符號(hào)位為 1,說明這是一個(gè)負(fù)數(shù)捏萍,另外 7 個(gè)位則代表了數(shù)值 124(即 128 - 4)的二進(jìn)制表示:

image.png

負(fù)數(shù)的表示通常被稱為二進(jìn)制補(bǔ)碼太抓。用這種方法來表示負(fù)數(shù)乍看起來有點(diǎn)奇怪,但它有幾個(gè)優(yōu)點(diǎn)令杈。

首先走敌,如果想對 -1-4 進(jìn)行加法運(yùn)算,我們只需要對這兩個(gè)數(shù)的全部 8 個(gè)比特位執(zhí)行標(biāo)準(zhǔn)的二進(jìn)制相加(包括符號(hào)位)逗噩,并且將計(jì)算結(jié)果中超出 8 位的數(shù)值丟棄:

image.png

其次掉丽,使用二進(jìn)制補(bǔ)碼可以使負(fù)數(shù)的按位左移和右移運(yùn)算得到跟正數(shù)同樣的效果,即每向左移一位就將自身的數(shù)值乘以 2异雁,每向右一位就將自身的數(shù)值除以 2捶障。要達(dá)到此目的,對有符號(hào)整數(shù)的右移有一個(gè)額外的規(guī)則:當(dāng)對有符號(hào)整數(shù)進(jìn)行按位右移運(yùn)算時(shí)纲刀,遵循與無符號(hào)整數(shù)相同的規(guī)則项炼,但是對于移位產(chǎn)生的空白位使用符號(hào)位進(jìn)行填充,而不是用 0示绊。

image.png

這個(gè)行為可以確保有符號(hào)整數(shù)的符號(hào)位不會(huì)因?yàn)橛乙七\(yùn)算而改變芥挣,這通常被稱為算術(shù)移位

由于正數(shù)和負(fù)數(shù)的特殊存儲(chǔ)方式耻台,在對它們進(jìn)行右移的時(shí)候空免,會(huì)使它們越來越接近 0。在移位的過程中保持符號(hào)位不變盆耽,意味著負(fù)整數(shù)在接近 0 的過程中會(huì)一直保持為負(fù)蹋砚。

溢出運(yùn)算符

當(dāng)向一個(gè)整數(shù)類型的常量或者變量賦予超過它容量的值時(shí)扼菠,Swift 默認(rèn)會(huì)報(bào)錯(cuò),而不是允許生成一個(gè)無效的數(shù)坝咐。這個(gè)行為為我們在運(yùn)算過大或者過小的數(shù)時(shí)提供了額外的安全性循榆。

例如,Int16 型整數(shù)能容納的有符號(hào)整數(shù)范圍是 -3276832767墨坚。當(dāng)為一個(gè) Int16 類型的變量或常量賦予的值超過這個(gè)范圍時(shí)秧饮,系統(tǒng)就會(huì)報(bào)錯(cuò):

var potentialOverflow = Int16.max
// potentialOverflow 的值是 32767,這是 Int16 能容納的最大整數(shù)
potentialOverflow += 1
// 這里會(huì)報(bào)錯(cuò)

在賦值時(shí)為過大或者過小的情況提供錯(cuò)誤處理泽篮,能讓我們在處理邊界值時(shí)更加靈活盗尸。

然而,當(dāng)你希望的時(shí)候也可以選擇讓系統(tǒng)在數(shù)值溢出的時(shí)候采取截?cái)嗵幚砻背牛菆?bào)錯(cuò)泼各。Swift 提供的三個(gè)溢出運(yùn)算符來讓系統(tǒng)支持整數(shù)溢出運(yùn)算。這些運(yùn)算符都是以 & 開頭的:

  • 溢出加法 &+

  • 溢出減法 &-

  • 溢出乘法 &*

數(shù)值溢出

數(shù)值有可能出現(xiàn)上溢或者下溢亏拉。

這個(gè)示例演示了當(dāng)我們對一個(gè)無符號(hào)整數(shù)使用溢出加法(&+)進(jìn)行上溢運(yùn)算時(shí)會(huì)發(fā)生什么:

var unsignedOverflow = UInt8.max
// unsignedOverflow 等于 UInt8 所能容納的最大整數(shù) 255
unsignedOverflow = unsignedOverflow &+ 1
// 此時(shí) unsignedOverflow 等于 0

unsignedOverflow 被初始化為 UInt8 所能容納的最大整數(shù)(255扣蜻,以二進(jìn)制表示即 11111111)。然后使用溢出加法運(yùn)算符(&+)對其進(jìn)行加 1 運(yùn)算及塘。這使得它的二進(jìn)制表示正好超出 UInt8 所能容納的位數(shù)莽使,也就導(dǎo)致了數(shù)值的溢出,如下圖所示笙僚。數(shù)值溢出后芳肌,仍然留在 UInt8 邊界內(nèi)的值是 00000000,也就是十進(jìn)制數(shù)值的 0味咳。

image.png

當(dāng)允許對一個(gè)無符號(hào)整數(shù)進(jìn)行下溢運(yùn)算時(shí)也會(huì)產(chǎn)生類似的情況庇勃。這里有一個(gè)使用溢出減法運(yùn)算符(&-)的例子:

var unsignedOverflow = UInt8.min
// unsignedOverflow 等于 UInt8 所能容納的最小整數(shù) 0
unsignedOverflow = unsignedOverflow &- 1
// 此時(shí) unsignedOverflow 等于 255

UInt8 型整數(shù)能容納的最小值是 0檬嘀,以二進(jìn)制表示即 00000000槽驶。當(dāng)使用溢出減法運(yùn)算符對其進(jìn)行減 1 運(yùn)算時(shí),數(shù)值會(huì)產(chǎn)生下溢并被截?cái)酁?11111111鸳兽, 也就是十進(jìn)制數(shù)值的 255掂铐。

image.png

溢出也會(huì)發(fā)生在有符號(hào)整型上。針對有符號(hào)整型的所有溢出加法或者減法運(yùn)算都是按位運(yùn)算的方式執(zhí)行的揍异,符號(hào)位也需要參與計(jì)算全陨,正如 按位左移、右移運(yùn)算符 所描述的衷掷。

var signedOverflow = Int8.min
// signedOverflow 等于 Int8 所能容納的最小整數(shù) -128
signedOverflow = signedOverflow &- 1
// 此時(shí) signedOverflow 等于 127

Int8 型整數(shù)能容納的最小值是 -128辱姨,以二進(jìn)制表示即 10000000。當(dāng)使用溢出減法運(yùn)算符對其進(jìn)行減 1 運(yùn)算時(shí)戚嗅,符號(hào)位被翻轉(zhuǎn)雨涛,得到二進(jìn)制數(shù)值 01111111枢舶,也就是十進(jìn)制數(shù)值的 127,這個(gè)值也是 Int8 型整數(shù)所能容納的最大值替久。

image.png

對于無符號(hào)與有符號(hào)整型數(shù)值來說凉泄,當(dāng)出現(xiàn)上溢時(shí),它們會(huì)從數(shù)值所能容納的最大數(shù)變成最小數(shù)蚯根。同樣地后众,當(dāng)發(fā)生下溢時(shí),它們會(huì)從所能容納的最小數(shù)變成最大數(shù)颅拦。

優(yōu)先級和結(jié)合性

運(yùn)算符的優(yōu)先級使得一些運(yùn)算符優(yōu)先于其他運(yùn)算符蒂誉;它們會(huì)先被執(zhí)行。

結(jié)合性定義了相同優(yōu)先級的運(yùn)算符是如何結(jié)合的矩距,也就是說拗盒,是與左邊結(jié)合為一組,還是與右邊結(jié)合為一組锥债《赣可以將其理解為“它們是與左邊的表達(dá)式結(jié)合的”,或者“它們是與右邊的表達(dá)式結(jié)合的”哮肚。

當(dāng)考慮一個(gè)復(fù)合表達(dá)式的計(jì)算順序時(shí)登夫,運(yùn)算符的優(yōu)先級和結(jié)合性是非常重要的。舉例來說允趟,運(yùn)算符優(yōu)先級解釋了為什么下面這個(gè)表達(dá)式的運(yùn)算結(jié)果會(huì)是 17恼策。

2 + 3 % 4 * 5
// 結(jié)果是 17

如果你直接從左到右進(jìn)行運(yùn)算,你可能認(rèn)為運(yùn)算的過程是這樣的:

  • 2 + 3 = 5

  • 5 % 4 = 1

  • 1 * 5 = 5

但是正確答案是 17 而不是 5潮剪。優(yōu)先級高的運(yùn)算符要先于優(yōu)先級低的運(yùn)算符進(jìn)行計(jì)算涣楷。與 C 語言類似,在 Swift 中抗碰,乘法運(yùn)算符(*)與取余運(yùn)算符(%)的優(yōu)先級高于加法運(yùn)算符(+)狮斗。因此,它們的計(jì)算順序要先于加法運(yùn)算弧蝇。

而乘法運(yùn)算與取余運(yùn)算的優(yōu)先級相同碳褒。這時(shí)為了得到正確的運(yùn)算順序,還需要考慮結(jié)合性看疗。乘法運(yùn)算與取余運(yùn)算都是左結(jié)合的沙峻。可以將這考慮成两芳,從它們的左邊開始為這兩部分表達(dá)式都隱式地加上括號(hào):

2 + ((3 % 4) * 5)

(3 % 4) 等于 3摔寨,所以表達(dá)式相當(dāng)于:

2 + (3 * 5)

3 * 5 等于 15,所以表達(dá)式相當(dāng)于:

2 + 15

因此計(jì)算結(jié)果為 17怖辆。

有關(guān) Swift 標(biāo)準(zhǔn)庫提供的操作符信息是复,包括操作符優(yōu)先級組和結(jié)合性設(shè)置的完整列表沉填,請參見 操作符聲明

注意

相對 C 語言和 Objective-C 來說佑笋,Swift 的運(yùn)算符優(yōu)先級和結(jié)合性規(guī)則更加簡潔和可預(yù)測翼闹。但是,這也意味著它們相較于 C 語言及其衍生語言并不是完全一致蒋纬。在對現(xiàn)有的代碼進(jìn)行移植的時(shí)候猎荠,要注意確保運(yùn)算符的行為仍然符合你的預(yù)期。

運(yùn)算符函數(shù)

類和結(jié)構(gòu)體可以為現(xiàn)有的運(yùn)算符提供自定義的實(shí)現(xiàn)蜀备。這通常被稱為運(yùn)算符重載关摇。

下面的例子展示了如何讓自定義的結(jié)構(gòu)體支持加法運(yùn)算符(+)。算術(shù)加法運(yùn)算符是一個(gè)二元運(yùn)算符碾阁,因?yàn)樗菍蓚€(gè)值進(jìn)行運(yùn)算输虱,同時(shí)它還可以稱為中綴運(yùn)算符,因?yàn)樗霈F(xiàn)在兩個(gè)值中間脂凶。

例子中定義了一個(gè)名為 Vector2D 的結(jié)構(gòu)體用來表示二維坐標(biāo)向量 (x, y)宪睹,緊接著定義了一個(gè)可以將兩個(gè) Vector2D 結(jié)構(gòu)體實(shí)例進(jìn)行相加的運(yùn)算符函數(shù)

struct Vector2D {
    var x = 0.0, y = 0.0
}

extension Vector2D {
    static func + (left: Vector2D, right: Vector2D) -> Vector2D {
        return Vector2D(x: left.x + right.x, y: left.y + right.y)
    }
}

該運(yùn)算符函數(shù)被定義為 Vector2D 上的一個(gè)類方法,并且函數(shù)的名字與它要進(jìn)行重載的 + 名字一致蚕钦。因?yàn)榧臃ㄟ\(yùn)算并不是一個(gè)向量必需的功能亭病,所以這個(gè)類方法被定義在 Vector2D 的一個(gè)擴(kuò)展中,而不是 Vector2D 結(jié)構(gòu)體聲明內(nèi)嘶居。而算術(shù)加法運(yùn)算符是二元運(yùn)算符罪帖,所以這個(gè)運(yùn)算符函數(shù)接收兩個(gè)類型為 Vector2D 的參數(shù),同時(shí)有一個(gè) Vector2D 類型的返回值邮屁。

在這個(gè)實(shí)現(xiàn)中整袁,輸入?yún)?shù)分別被命名為 leftright,代表在 + 運(yùn)算符左邊和右邊的兩個(gè) Vector2D 實(shí)例佑吝。函數(shù)返回了一個(gè)新的 Vector2D 實(shí)例坐昙,這個(gè)實(shí)例的 xy 分別等于作為參數(shù)的兩個(gè)實(shí)例的 xy 的值之和。

這個(gè)類方法可以在任意兩個(gè) Vector2D 實(shí)例中間作為中綴運(yùn)算符來使用:

let vector = Vector2D(x: 3.0, y: 1.0)
let anotherVector = Vector2D(x: 2.0, y: 4.0)
let combinedVector = vector + anotherVector
// combinedVector 是一個(gè)新的 Vector2D 實(shí)例迹蛤,值為 (5.0, 5.0)

這個(gè)例子實(shí)現(xiàn)兩個(gè)向量 (3.0民珍,1.0)(2.0襟士,4.0) 的相加盗飒,并得到新的向量 (5.0,5.0)陋桂。這個(gè)過程如下圖示:

image.png

前綴和后綴運(yùn)算符

上個(gè)例子演示了一個(gè)二元中綴運(yùn)算符的自定義實(shí)現(xiàn)逆趣。類與結(jié)構(gòu)體也能提供標(biāo)準(zhǔn)一元運(yùn)算符的實(shí)現(xiàn)。一元運(yùn)算符只運(yùn)算一個(gè)值嗜历。當(dāng)運(yùn)算符出現(xiàn)在值之前時(shí)宣渗,它就是前綴的(例如 -a)抖所,而當(dāng)它出現(xiàn)在值之后時(shí),它就是后綴的(例如 b!)痕囱。

要實(shí)現(xiàn)前綴或者后綴運(yùn)算符田轧,需要在聲明運(yùn)算符函數(shù)的時(shí)候在 func 關(guān)鍵字之前指定 prefix 或者 postfix 修飾符:

extension Vector2D {
    static prefix func - (vector: Vector2D) -> Vector2D {
        return Vector2D(x: -vector.x, y: -vector.y)
    }
}

這段代碼為 Vector2D 類型實(shí)現(xiàn)了一元運(yùn)算符(-a)。由于該運(yùn)算符是前綴運(yùn)算符鞍恢,所以這個(gè)函數(shù)需要加上 prefix 修飾符傻粘。

對于簡單數(shù)值,一元負(fù)號(hào)運(yùn)算符可以對它們的正負(fù)性進(jìn)行改變帮掉。對于 Vector2D 來說弦悉,該運(yùn)算將其 xy 屬性的正負(fù)性都進(jìn)行了改變:

let positive = Vector2D(x: 3.0, y: 4.0)
let negative = -positive
// negative 是一個(gè)值為 (-3.0, -4.0) 的 Vector2D 實(shí)例
let alsoPositive = -negative
// alsoPositive 是一個(gè)值為 (3.0, 4.0) 的 Vector2D 實(shí)例

復(fù)合賦值運(yùn)算符

復(fù)合賦值運(yùn)算符將賦值運(yùn)算符(=)與其它運(yùn)算符進(jìn)行結(jié)合。例如蟆炊,將加法與賦值結(jié)合成加法賦值運(yùn)算符(+=)稽莉。在實(shí)現(xiàn)的時(shí)候,需要把運(yùn)算符的左參數(shù)設(shè)置成 inout 類型涩搓,因?yàn)檫@個(gè)參數(shù)的值會(huì)在運(yùn)算符函數(shù)內(nèi)直接被修改污秆。

在下面的例子中,對 Vector2D 實(shí)例實(shí)現(xiàn)了一個(gè)加法賦值運(yùn)算符函數(shù):

extension Vector2D {
    static func += (left: inout Vector2D, right: Vector2D) {
        left = left + right
    }
}

因?yàn)榧臃ㄟ\(yùn)算在之前已經(jīng)定義過了昧甘,所以在這里無需重新定義混狠。在這里可以直接利用現(xiàn)有的加法運(yùn)算符函數(shù),用它來對左值和右值進(jìn)行相加疾层,并再次賦值給左值:

var original = Vector2D(x: 1.0, y: 2.0)
let vectorToAdd = Vector2D(x: 3.0, y: 4.0)
original += vectorToAdd
// original 的值現(xiàn)在為 (4.0, 6.0)

注意

不能對默認(rèn)的賦值運(yùn)算符(=)進(jìn)行重載将饺。只有復(fù)合賦值運(yùn)算符可以被重載。同樣地痛黎,也無法對三元條件運(yùn)算符 (a ? b : c) 進(jìn)行重載予弧。

等價(jià)運(yùn)算符

通常情況下,自定義的類和結(jié)構(gòu)體沒有對等價(jià)運(yùn)算符進(jìn)行默認(rèn)實(shí)現(xiàn)湖饱,等價(jià)運(yùn)算符通常被稱為相等運(yùn)算符(==)與不等運(yùn)算符(!=)掖蛤。

為了使用等價(jià)運(yùn)算符對自定義的類型進(jìn)行判等運(yùn)算,需要為“相等”運(yùn)算符提供自定義實(shí)現(xiàn)井厌,實(shí)現(xiàn)的方法與其它中綴運(yùn)算符一樣, 并且增加對標(biāo)準(zhǔn)庫 Equatable 協(xié)議的遵循:

extension Vector2D: Equatable {
    static func == (left: Vector2D, right: Vector2D) -> Bool {
        return (left.x == right.x) && (left.y == right.y)
    }
}

上述代碼實(shí)現(xiàn)了“相等”運(yùn)算符(==)來判斷兩個(gè) Vector2D 實(shí)例是否相等蚓庭。對于 Vector2D 來說,“相等”意味著“兩個(gè)實(shí)例的 xy 都相等”仅仆,這也是代碼中用來進(jìn)行判等的邏輯器赞。如果你已經(jīng)實(shí)現(xiàn)了“相等”運(yùn)算符,通常情況下你并不需要自己再去實(shí)現(xiàn)“不等”運(yùn)算符(!=)墓拜。標(biāo)準(zhǔn)庫對于“不等”運(yùn)算符提供了默認(rèn)的實(shí)現(xiàn)港柜,它簡單地將“相等”運(yùn)算符的結(jié)果進(jìn)行取反后返回。

現(xiàn)在我們可以使用這兩個(gè)運(yùn)算符來判斷兩個(gè) Vector2D 實(shí)例是否相等:

let twoThree = Vector2D(x: 2.0, y: 3.0)
let anotherTwoThree = Vector2D(x: 2.0, y: 3.0)
if twoThree == anotherTwoThree {
    print("These two vectors are equivalent.")
}
// 打印“These two vectors are equivalent.”

多數(shù)簡單情況下,你可以讓 Swift 合成等價(jià)運(yùn)算符的實(shí)現(xiàn)夏醉,詳見 使用合成實(shí)現(xiàn)來采納協(xié)議爽锥。

自定義運(yùn)算符

除了實(shí)現(xiàn)標(biāo)準(zhǔn)運(yùn)算符,在 Swift 中還可以聲明和實(shí)現(xiàn)自定義運(yùn)算符畔柔÷纫模可以用來自定義運(yùn)算符的字符列表請參考 運(yùn)算符

新的運(yùn)算符要使用 operator 關(guān)鍵字在全局作用域內(nèi)進(jìn)行定義靶擦,同時(shí)還要指定 prefix肠槽、infix 或者 postfix 修飾符:

prefix operator +++

上面的代碼定義了一個(gè)新的名為 +++ 的前綴運(yùn)算符。對于這個(gè)運(yùn)算符奢啥,在 Swift 中并沒有已知的意義秸仙,因此在針對 Vector2D 實(shí)例的特定上下文中,給予了它自定義的意義桩盲。對這個(gè)示例來講寂纪,+++ 被實(shí)現(xiàn)為“前綴雙自增”運(yùn)算符。它使用了前面定義的復(fù)合加法運(yùn)算符來讓矩陣與自身進(jìn)行相加赌结,從而讓 Vector2D 實(shí)例的 x 屬性和 y 屬性值翻倍捞蛋。你可以像下面這樣通過對 Vector2D 添加一個(gè) +++ 類方法,來實(shí)現(xiàn) +++ 運(yùn)算符:

extension Vector2D {
    static prefix func +++ (vector: inout Vector2D) -> Vector2D {
        vector += vector
        return vector
    }
}

var toBeDoubled = Vector2D(x: 1.0, y: 4.0)
let afterDoubling = +++toBeDoubled
// toBeDoubled 現(xiàn)在的值為 (2.0, 8.0)
// afterDoubling 現(xiàn)在的值也為 (2.0, 8.0)

自定義中綴運(yùn)算符的優(yōu)先級

每個(gè)自定義中綴運(yùn)算符都屬于某個(gè)優(yōu)先級組柬姚。優(yōu)先級組指定了這個(gè)運(yùn)算符相對于其他中綴運(yùn)算符的優(yōu)先級和結(jié)合性拟杉。優(yōu)先級和結(jié)合性 中詳細(xì)闡述了這兩個(gè)特性是如何對中綴運(yùn)算符的運(yùn)算產(chǎn)生影響的。

而沒有明確放入某個(gè)優(yōu)先級組的自定義中綴運(yùn)算符將會(huì)被放到一個(gè)默認(rèn)的優(yōu)先級組內(nèi)量承,其優(yōu)先級高于三元運(yùn)算符搬设。

以下例子定義了一個(gè)新的自定義中綴運(yùn)算符 +-,此運(yùn)算符屬于 AdditionPrecedence 優(yōu)先組:

infix operator +-: AdditionPrecedence
extension Vector2D {
    static func +- (left: Vector2D, right: Vector2D) -> Vector2D {
        return Vector2D(x: left.x + right.x, y: left.y - right.y)
    }
}
let firstVector = Vector2D(x: 1.0, y: 2.0)
let secondVector = Vector2D(x: 3.0, y: 4.0)
let plusMinusVector = firstVector +- secondVector
// plusMinusVector 是一個(gè) Vector2D 實(shí)例撕捍,并且它的值為 (4.0, -2.0)

這個(gè)運(yùn)算符把兩個(gè)向量的 x 值相加拿穴,同時(shí)從第一個(gè)向量的 y 中減去第二個(gè)向量的 y 。因?yàn)樗举|(zhì)上是屬于“相加型”運(yùn)算符忧风,所以將它放置在 +- 等默認(rèn)中綴“相加型”運(yùn)算符相同的優(yōu)先級組中默色。關(guān)于 Swift 標(biāo)準(zhǔn)庫提供的運(yùn)算符,以及完整的運(yùn)算符優(yōu)先級組和結(jié)合性設(shè)置狮腿,請參考 運(yùn)算符聲明腿宰。

注意

當(dāng)定義前綴與后綴運(yùn)算符的時(shí)候,我們并沒有指定優(yōu)先級缘厢。然而吃度,如果對同一個(gè)值同時(shí)使用前綴與后綴運(yùn)算符,則后綴運(yùn)算符會(huì)先參與運(yùn)算昧绣。

結(jié)果構(gòu)造器

結(jié)果構(gòu)造器是一種自定義類型规肴,支持添加自然的聲明式語法來創(chuàng)建類似列表或者樹這樣的嵌套數(shù)據(jù)捶闸。使用結(jié)果構(gòu)造器的代碼可以包含普通的 Swift 語法夜畴,例如用來處理判斷條件的 if拖刃,或者處理重復(fù)數(shù)據(jù)的 for

下面的代碼定義了一些類型用于繪制星星線段和文字線段贪绘。

protocol Drawable {
    func draw() -> String
}
struct Line: Drawable {
    var elements: [Drawable]
    func draw() -> String {
        return elements.map { $0.draw() }.joined(separator: "")
    }
}
struct Text: Drawable {
    var content: String
    init(_ content: String) { self.content = content }
    func draw() -> String { return content }
}
struct Space: Drawable {
    func draw() -> String { return " " }
}
struct Stars: Drawable {
    var length: Int
    func draw() -> String { return String(repeating: "*", count: length) }
}
struct AllCaps: Drawable {
    var content: Drawable
    func draw() -> String { return content.draw().uppercased() }
}

Drawable 協(xié)議定義了繪制所需要遵循的方法兑牡,例如線或者形狀都需要實(shí)現(xiàn) draw() 方法。Line 結(jié)構(gòu)體用來表示單行線段繪制税灌,給大多數(shù)可繪制的元素提供了頂層容器均函。繪制 Line 時(shí),調(diào)用了線段中每個(gè)元素的 draw()菱涤,然后將所有結(jié)果字符串連成單個(gè)字符串苞也。Text 結(jié)構(gòu)體包裝了一個(gè)字符串作為繪制的一部分。AllCaps 結(jié)構(gòu)體包裝另一個(gè)可繪制元素粘秆,并將元素中所有文本轉(zhuǎn)換為大寫如迟。

可以組合這些類型的構(gòu)造器來創(chuàng)建一個(gè)可繪制元素。

let name: String? = "Ravi Patel"
let manualDrawing = Line(elements: [
    Stars(length: 3),
    Text("Hello"),
    Space(),
    AllCaps(content: Text((name ?? "World") + "!")),
    Stars(length: 2),
    ])
print(manualDrawing.draw())
// 打印 "***Hello RAVI PATEL!**"

代碼沒問題攻走,但是不夠優(yōu)雅殷勘。AllCaps 后面的括號(hào)嵌套太深,可讀性不佳昔搂。namenil 時(shí)使用 “World” 的兜底邏輯必須要依賴 ?? 操作符玲销,這在邏輯復(fù)雜的時(shí)候會(huì)更難以閱讀。如果還需要 switch 或者 for 循環(huán)來構(gòu)建繪制的一部分摘符,就更難以編寫了贤斜。使用結(jié)果構(gòu)造器可以將這樣的代碼重構(gòu)得更像普通的 Swift 代碼。

在類型的定義上加上 @resultBuilder 特性來定義一個(gè)結(jié)果構(gòu)造器逛裤。比如下面的代碼定義了允許使用聲明式語法來描述繪制的結(jié)果構(gòu)造器 DrawingBuilder

@resultBuilder
struct DrawingBuilder {
    static func buildBlock(_ components: Drawable...) -> Drawable {
        return Line(elements: components)
    }
    static func buildEither(first: Drawable) -> Drawable {
        return first
    }
    static func buildEither(second: Drawable) -> Drawable {
        return second
    }
}

DrawingBuilder 結(jié)構(gòu)體定義了三個(gè)方法來實(shí)現(xiàn)部分結(jié)果構(gòu)造器語法蠢古。buildBlock(_:) 方法添加了在方法塊中寫多行代碼的支持。它將方法塊中的多個(gè)元素組合成 Line别凹。buildEither(first:)buildEither(second:) 方法添加了對 if-else 的支持草讶。

可以在函數(shù)形參上應(yīng)用 @DrawingBuilder 特性,它會(huì)將傳遞給函數(shù)的閉包轉(zhuǎn)換為用結(jié)果構(gòu)造器創(chuàng)建的值炉菲。例如:

func draw(@DrawingBuilder content: () -> Drawable) -> Drawable {
    return content()
}
func caps(@DrawingBuilder content: () -> Drawable) -> Drawable {
    return AllCaps(content: content())
}

func makeGreeting(for name: String? = nil) -> Drawable {
    let greeting = draw {
        Stars(length: 3)
        Text("Hello")
        Space()
        caps {
            if let name = name {
                Text(name + "!")
            } else {
                Text("World!")
            }
        }
        Stars(length: 2)
    }
    return greeting
}
let genericGreeting = makeGreeting()
print(genericGreeting.draw())
// 打印 "***Hello WORLD!**"

let personalGreeting = makeGreeting(for: "Ravi Patel")
print(personalGreeting.draw())
// 打印 "***Hello RAVI PATEL!**"

makeGreeting(for:) 函數(shù)將傳入的 name 形參用于繪制個(gè)性化問候堕战。draw(_:)caps(_:) 函數(shù)都傳入應(yīng)用 @DrawingBuilder 特性的單一閉包實(shí)參。當(dāng)調(diào)用這些函數(shù)時(shí)拍霜,要使用 DrawingBuilder 定義的特殊語法嘱丢。Swift 將繪制的聲明式描述轉(zhuǎn)換為一系列 DrawingBuilder 的方法調(diào)用,構(gòu)造成最終傳遞進(jìn)函數(shù)的實(shí)參值祠饺。例如越驻,Swift 將例子中的 caps(_:) 的調(diào)用轉(zhuǎn)換為下面的代碼:

let capsDrawing = caps {
    let partialDrawing: Drawable
    if let name = name {
        let text = Text(name + "!")
        partialDrawing = DrawingBuilder.buildEither(first: text)
    } else {
        let text = Text("World!")
        partialDrawing = DrawingBuilder.buildEither(second: text)
    }
    return partialDrawing
}

Swift 將 if-else 方法塊轉(zhuǎn)換成調(diào)用 buildEither(first:)buildEither(second:) 方法。雖然不會(huì)在自己的代碼中調(diào)用這些方法,但是轉(zhuǎn)換后的結(jié)果可以更清晰的理解在使用 DrawingBuilder 語法時(shí) Swift 是如何進(jìn)行轉(zhuǎn)換的缀旁。

為了支持 for 循環(huán)來滿足某些特殊的繪制語法记劈,需要添加 buildArray(_:) 方法。

extension DrawingBuilder {
    static func buildArray(_ components: [Drawable]) -> Drawable {
        return Line(elements: components)
    }
}
let manyStars = draw {
    Text("Stars:")
    for length in 1...3 {
        Space()
        Stars(length: length)
    }
}

上面的代碼中并巍,使用 for 循環(huán)創(chuàng)建了一個(gè)繪制數(shù)組目木,buildArray(_:) 方法將該數(shù)組構(gòu)建成 Line


回顧 基本運(yùn)算符

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末懊渡,一起剝皮案震驚了整個(gè)濱河市蚪黑,隨后出現(xiàn)的幾起案子达吞,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件井佑,死亡現(xiàn)場離奇詭異俺陋,居然都是意外死亡聚唐,警方通過查閱死者的電腦和手機(jī)椿访,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來阁最,“玉大人戒祠,你說我怎么就攤上這事∷僦郑” “怎么了姜盈?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長配阵。 經(jīng)常有香客問我馏颂,道長,這世上最難降的妖魔是什么棋傍? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任救拉,我火速辦了婚禮,結(jié)果婚禮上瘫拣,老公的妹妹穿的比我還像新娘亿絮。我一直安慰自己,他們只是感情好麸拄,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布派昧。 她就那樣靜靜地躺著,像睡著了一般拢切。 火紅的嫁衣襯著肌膚如雪蒂萎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天淮椰,我揣著相機(jī)與錄音五慈,去河邊找鬼纳寂。 笑死,一個(gè)胖子當(dāng)著我的面吹牛泻拦,可吹牛的內(nèi)容都是我干的毙芜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼聪轿,長吁一口氣:“原來是場噩夢啊……” “哼爷肝!你這毒婦竟也來了猾浦?” 一聲冷哼從身側(cè)響起陆错,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎金赦,沒想到半個(gè)月后音瓷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡夹抗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年绳慎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片漠烧。...
    茶點(diǎn)故事閱讀 40,001評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡杏愤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出已脓,到底是詐尸還是另有隱情珊楼,我是刑警寧澤,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布度液,位于F島的核電站厕宗,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏堕担。R本人自食惡果不足惜已慢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望霹购。 院中可真熱鬧佑惠,春花似錦、人聲如沸齐疙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽剂碴。三九已至把将,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間忆矛,已是汗流浹背察蹲。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工请垛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人洽议。 一個(gè)月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓宗收,卻偏偏與公主長得像,于是被迫代替她去往敵國和親亚兄。 傳聞我的和親對象是個(gè)殘疾皇子混稽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 中文文檔 一、位運(yùn)算符 位操作符通常在諸如圖像處理和創(chuàng)建設(shè)備驅(qū)動(dòng)等底層開發(fā)中使用审胚,使用它可以單獨(dú)操作數(shù)據(jù)結(jié)構(gòu)中原始...
    伯wen閱讀 1,490評論 0 2
  • Swift 運(yùn)算符基本運(yùn)算符高級運(yùn)算符(包括 C 或 Objective-C 所有按位和移位運(yùn)算符膳叨。) 與 C 的...
    Sunday_David閱讀 495評論 0 0
  • 與 C 語言中的算術(shù)運(yùn)算符不同洽洁,Swift 中的算術(shù)運(yùn)算符默認(rèn)是不會(huì)溢出的。 所有溢出行為都會(huì)被捕獲并報(bào)告為錯(cuò)誤菲嘴。...
    DevXue閱讀 456評論 0 1
  • 案例代碼下載 高級運(yùn)算符 除了基本運(yùn)算符中描述的運(yùn)算符之外饿自,Swift還提供了幾個(gè)執(zhí)行更復(fù)雜值操作的高級運(yùn)算符。這...
    酒茶白開水閱讀 859評論 0 0
  • 高級運(yùn)算符 文檔地址 作為 基本運(yùn)算符 的補(bǔ)充龄坪,Swift 提供了幾個(gè)高級運(yùn)算符執(zhí)行對數(shù)傳值進(jìn)行更加復(fù)雜的操作昭雌。這...
    hrscy閱讀 842評論 0 2