翻譯:tommy60703 校對:rocooshiang

基本運算子


本頁包含內容:

運算子是檢查、改變、合並值的特殊符號或短語。例如,加號+將兩個數相加(如let i = 1 + 2)。複雜些的運算例如邏輯 AND 運算子&&(如if enteredDoorCode && passedRetinaScan),又或直接讓 i 值加 1 的累加運算子++i等。

Swift 支援大部分標準 C 語言的運算子,且改進許多特性來減少常見的編碼錯誤。如,指派運算子(=)不回傳值,以防止把想要判斷相等運算子(==)的地方寫成指派運算子導致的錯誤。數值運算子(+-*/%等)會檢測並不允許溢位,以此來避免保存變數時由於變數大於或小於其型別所能儲存的範圍時導致的異常結果。當然允許你使用 Swift 的溢位運算子來實作溢位。詳情參見溢位運算子

有別於 C 語言,在 Swift 中你可以對浮點數進行餘數運算(%),Swift 還提供了 C 語言所沒有的,用來表達兩數之間的值的區間運算子,(a..<ba...b),這方便我們表達一個區間內的數值。

本章節只描述了 Swift 中的基本運算子,進階運算子包含了進階運算子、如何自定義運算子,以及如何進行自定義型別的運算子多載。

術語

運算子有一元,二元和三元運算子。

  • 一元運算子對單一操作物件操作(如-a)。一元運算子分前綴運算子和後綴運算子,前綴運算子需要緊跟在操作物件之前(如!b),後綴運算子需要緊跟在操作物件之後(如i++)。
  • 二元運算子操作兩個操作物件(如2 + 3),是中綴的,因為它們出現在兩個操作物件之間。
  • 三元運算子操作三個操作物件,和 C 語言一樣,Swift 只有一個三元運算子,就是三元條件運算子(a ? b : c)。

受運算子影響的值叫運算元,在表達式1 + 2中,加號+是二元運算子,它的兩個運算元是值12

指派運算子

指派運算(a = b),表示用b的值來初始化或更新a的值:

let b = 10
var a = 5
a = b
// a 現在等於 10

如果指派的右邊是一個 tuple,它的元素可以馬上被分解多個常數或變數:

let (x, y) = (1, 2)
// 現在 x 等於 1, y 等於 2

與 C 語言和 Objective-C 不同,Swift 的指派運算子並不回傳任何值。所以以下程式碼是錯誤的:

if x = y {
    // 此句錯誤, 因為 x = y 並不回傳任何值
}

這個特性保護你不會把(==)錯寫成(=)了,由於if x = y是錯誤程式碼,Swift 從底層幫你避免了這些程式碼錯誤。

數值運算子

Swift 支援所有數值型別基本的四則運算:

  • 加法(+
  • 減法(-
  • 乘法(*
  • 除法(/
1 + 2       // 等於 3
5 - 3       // 等於 2
2 * 3       // 等於 6
10.0 / 2.5  // 等於 4.0

與 C 語言和 Objective-C 不同的是,Swift 預設不允許在數值運算中出現溢位情況。但你可以使用 Swift 的溢位運算子來達到你有目的的溢位(如a &+ b)。詳情參見溢位運算子

加法運算子也用於String的拼接:

"hello, " + "world"  // 等於 "hello, world"

兩個Character值或一個String和一個Character值,相加會生成一個新的String值:

let dog: Character = "🐶"
let cow: Character = "🐮"
let dogCow = dog + cow
// dogCow 現在是 "🐶🐮"

詳情參見字元和字串的拼接

餘數運算

餘數運算(a % b)是計算b的多少倍剛剛好可以容入a,回傳多出來的那部分(餘數)。

注意: 餘數運算(%)在其他語言也叫取模運算。然而嚴格說來,我們看該運算子對負數的操作結果,"餘數"比"取模"更合適些。

我們來談談取餘數是怎麼回事,計算9 % 4,你先計算出4的多少倍會剛好可以容入9中:

Art/remainderInteger_2x.png

2 倍,非常好,那餘數是 1(用橙色標出)

在 Swift 中這麼來表達:

9 % 4    // 等於 1

為了得到a % b的結果,%計算了以下等式,並輸出餘數作為結果:

a = (b × 倍數) + 餘數

倍數取最大值的時候,就會剛好可以容入a中。

94代入等式中,我們得1

9 = (4 × 2) + 1

同樣的方法,我來們計算 -9 % 4

-9 % 4   // 等於 -1

-94代入等式,-2是取到的最大整數:

-9 = (4 × -2) + -1

餘數是-1

在對負數b餘數時,b的符號會被忽略。這意味著 a % ba % -b的結果是相同的。

浮點數餘數計算

不同於 C 語言和 Objective-C,Swift 是可以對浮點數取餘數的。

8 % 2.5 // 等於 0.5

這個範例中,8除於2.5等於30.5,所以結果是一個Double0.5

Art/remainderFloat_2x.png

累加和累減運算

和 C 語言一樣,Swift 也提供了方便對變數本身加 1 或減 1 的累加(++)和累減(--)運算子。其操作物件可以是整數和浮點數。 ‌

var i = 0
++i      // 現在 i = 1

每呼叫一次++ii的值就會加 1。實際上,++ii = i + 1的簡寫,而--ii = i - 1的簡寫。

++--既是前綴又是後綴運算子。++ii++--ii--都是有效的寫法。

我們需要注意的是這些運算子修改了i後有一個回傳值。如果你只想修改i的值,那你就可以忽略這個回傳值。但如果你想使用回傳值,你就需要留意前綴和後綴操作的回傳值是不同的。

  • ++前綴的時候,先自増再回傳。

  • ++後綴的時候,先回傳再累加。

例如:

var a = 0
let b = ++a // a 和 b 現在都是 1
let c = a++ // a 現在 2, 但 c 是 a 累加前的值 1

上述範例,let b = ++a先把a加 1 了再回傳a的值。所以ab都是新值1

let c = a++,是先回傳了a的值,然後a才加 1。所以c得到了a的舊值 1,而a加 1 後變成 2。

除非你需要使用i++的特性,不然推薦你使用++i--i,因為先修改後回傳這樣的行為更符合我們的邏輯。

一元負號

數值的正負號可以使用前綴-(即一元負號)來切換:

let three = 3
let minusThree = -three       // minusThree 等於 -3
let plusThree = -minusThree   // plusThree 等於 3, 或 "負負3"

一元負號(-)寫在運算元之前,中間沒有空格。

一元正號

一元正號(+)不做任何改變地回傳運算元的值。

let minusSix = -6
let alsoMinusSix = +minusSix  // alsoMinusSix 等於 -6

雖然一元+做無用功,但當你在使用一元負號來表達負數時,你可以使用一元正號來表達正數,如此你的程式碼會具有對稱之美。

複合指派(Compound Assignment Operators)

如同強大的 C 語言,Swift 也提供把其他運算子和指派運算(=)組合起來的複合指派運算子,加賦運算(+=)是其中一個範例:

var a = 1
a += 2 // a 現在是 3

表達式a += 2a = a + 2的簡寫,一個加賦運算就把加法和指派兩件事完成了。

注意: 複合指派運算沒有回傳值,let b = a += 2這類別程式碼是錯誤。這不同於上面提到的累加和累減運算子。

表達式章節裡有複合運算子的完整列表。 ‌

比較運算

所有標準 C 語言中的比較運算都可以在 Swift 中使用。

  • 等於(a == b
  • 不等於(a != b
  • 大於(a > b
  • 小於(a < b
  • 大於等於(a >= b
  • 小於等於(a <= b

注意: Swift 也提供恆等===和不恆等!==這兩個比較符來判斷兩個物件是否參考同一個物件實例。更多細節在類別別與結構

每個比較運算都回傳了一個顯示表達式是否成立的布林值:

1 == 1   // true, 因為 1 等於 1
2 != 1   // true, 因為 2 不等於 1
2 > 1    // true, 因為 2 大於 1
1 < 2    // true, 因為 1 小於2
1 >= 1   // true, 因為 1 大於等於 1
2 <= 1   // false, 因為 2 並不小於等於 1

比較運算多用於條件語句,如if條件:

let name = "world"
if name == "world" {
    println("hello, world")
} else {
    println("I'm sorry \(name), but I don't recognize you")
}
// 輸出 "hello, world", 因為 `name` 就是等於 "world"

關於if語句,請看控制流程程

三元條件運算(Ternary Conditional Operator)

三元條件運算的特殊在於它是有三個運算元的運算子,它的原型是 問題 ? 答案1 : 答案2。它簡潔地表達根據問題成立與否作出二選一的操作。如果問題成立,回傳答案1的結果; 如果不成立,回傳答案2的結果。

使用三元條件運算簡化了以下程式碼:

if question {
  answer1
} else {
  answer2
}

這裡有個計算表格行高的範例。如果有表頭,那行高應比內容高度要高出 50 像素; 如果沒有表頭,只需高出 20 像素。

let contentHeight = 40
let hasHeader = true
let rowHeight = contentHeight + (hasHeader ? 50 : 20)
// rowHeight 現在是 90

這樣寫會比下面的程式碼簡潔:

let contentHeight = 40
let hasHeader = true
var rowHeight = contentHeight
if hasHeader {
    rowHeight = rowHeight + 50
} else {
    rowHeight = rowHeight + 20
}
// rowHeight 現在是 90

第一段程式碼範例使用了三元條件運算,所以一行程式碼就能讓我們得到正確答案。這比第二段程式碼簡潔得多,無需將rowHeight定義成變數,因為它的值無需在if語句中改變。

三元條件運算提供有效率且便捷的方式來表達二選一的選擇。需要注意的事,過度使用三元條件運算就會由簡潔的程式碼變成難懂的程式碼。我們應避免在一個組合語句使用多個三元條件運算子。

空值聚合運算子

空值聚合(nil coalescing)運算子(a ?? b)如果optional a有值的話就解析,但若a是空值,就回傳預設值b。 這運算元a永遠是個optional型別。而運算子b必須得式a的型別。

空值聚合運算子是以下表達式的簡碼。

a != nil ? a! : b

上述程式碼使用三元條件運算子,並且當a不是空值時,強制解析(a!)來存取a,若a是空值則回傳b。空值聚合運算子題空一個更優雅的方式,並以一個明確可讀的形式來封裝類似的條件確認及解析。

注意: 若a是非空值,那就不會運算b。這就是所謂的最小化求值

下面這個例子使用空值聚合運算子來選擇預設顏色值或是一個optional使用者定義的顏色值。

let defaultColorName = "red"
var userDefinedColorName: String?   // 預設為空值

var colorNameToUse = userDefinedColorName ?? defaultColorName
// userDefinedColorName 是空值, 所以 colorNameToUse 就被設成預設的 「red」。

變數userDefinedColorName是定義成optional字串型別,且其預設值為空值。因為userDefinedColorName變數是個optional型別,你可以使用空值聚合運算子來決定他的值。在上面的例子當中,這運算子被用來定義一個Sting型別colorNameToUse變數的初始值。因為userDefinedColorName是空值,這表達式userDefinedColorName ?? defaultColorName 回傳 defaultColorName,也就是「red」。

如果你把userDefinedColorName設成非空值,並且執行空值聚合運算子來確認,則userDefinedColorName就會被解析,而不是使用預設值:

userDefinedColorName = "green"
colorNameToUse = userDefinedColorName ?? defaultColorName
// userDefinedColorName非空值,所以colorNameToUse被設成「green」。

區間運算子

Swift 提供了兩個方便表達一個區間的值的運算子。

閉區間運算子

閉區間運算子(a...b)定義一個包含從ab(包括ab)的所有值的區間。a的值必不大於b。 ‌ 閉區間運算子在迭代一個區間的所有值時是非常有用的,如在for-in迴圈中:

for index in 1...5 {
    println("\(index) * 5 = \(index * 5)")
}
// 1 * 5 = 5
// 2 * 5 = 10
// 3 * 5 = 15
// 4 * 5 = 20
// 5 * 5 = 25

關於for-in,請看控制流程程

半閉區間運算子

半閉區間運算子(a..<b)定義一個從ab但不包括b的區間。 之所以稱為半閉區間,是因為該區間包含第一個值而不包括最後的值。就像閉區間運算子一樣,a的值必不大於b。如果ab相等,那結果的區間就會是空。

半閉區間運算子的實用性在於當你使用一個 0 始的列表(如陣列)時,非常方便地從 0 數到列表的長度。

let names = ["Anna", "Alex", "Brian", "Jack"]
let count = names.count
for i in 0..<count {
    println("第 \(i + 1) 個人叫 \(names[i])")
}
// 第 1 個人叫 Anna
// 第 2 個人叫 Alex
// 第 3 個人叫 Brian
// 第 4 個人叫 Jack

陣列有 4 個元素,但0..<count只數到 3(最後一個元素的索引),因為它是半閉區間。關於陣列,請查閱陣列

邏輯運算

邏輯運算的操作物件是邏輯布林值。Swift 支援基於 C 語言的三個標準邏輯運算。

  • 邏輯非(!a
  • 邏輯且(a && b
  • 邏輯或(a || b

邏輯非

邏輯非運算子(!a)對一個布林值取反,使得true變成falsefalse變成true

它是一個前綴運算子,需出現在運算元之前,且不加空格。讀作非 a,然後我們看以下範例:

let allowedEntry = false
if !allowedEntry {
    println("ACCESS DENIED")
}
// 輸出 "ACCESS DENIED"

if !allowedEntry語句可以讀作「如果非 alowed entry」,接下一行程式碼只有在如果「非 allow entry」為true,即allowEntryfalse時被執行。

在範例程式碼中,小心地選擇布林常數或變數有助於程式碼的可讀性,並且避免使用雙重邏輯非運算,或混亂的邏輯語句。

邏輯且

邏輯且運算子(a && b)表達了只有ab的值都為true時,整個表達式的值才會是true

只要任意一個值為false,整個表達式的值就為false。事實上,如果第一個值為false,那麼是不去計算第二個值的,因為它已經不可能影響整個表達式的結果了。這被稱做「捷徑計算(short-circuit evaluation)」。

以下範例,只有兩個Bool值都為true值的時候才允許進入:

let enteredDoorCode = true
let passedRetinaScan = false
if enteredDoorCode && passedRetinaScan {
    println("Welcome!")
} else {
    println("ACCESS DENIED")
}
// 輸出 "ACCESS DENIED"

邏輯或

邏輯或運算子(a || b)是一個由兩個連續的|組成的中綴運算子。它表示了兩個邏輯表達式的其中一個為true,整個表達式就為true

和邏輯且運算子類似,邏輯或也使用「捷徑計算」,當左端的表達式為true時,將不計算右邊的表達式了,因為它不可能改變整個表達式的值了。

以下範例程式碼中,第一個布林值(hasDoorKey)為false,但第二個值(knowsOverridePassword)為true,所以整個表達是true,於是允許進入:

let hasDoorKey = false
let knowsOverridePassword = true
if hasDoorKey || knowsOverridePassword {
    println("Welcome!")
} else {
    println("ACCESS DENIED")
}
// 輸出 "Welcome!"

組合邏輯

我們可以組合多個邏輯運算來表達一個複合邏輯:

if enteredDoorCode && passedRetinaScan || hasDoorKey || knowsOverridePassword {
    println("Welcome!")
} else {
    println("ACCESS DENIED")
}
// 輸出 "Welcome!"

這個範例使用了含多個&&||的複合邏輯。但無論怎樣,&&||始終只能操作兩個值。所以這實際是三個簡單邏輯連續操作的結果。我們來解讀一下:

如果我們輸入了正確的密碼並通過了視網膜掃描; 或者我們有一把有效的鑰匙; 又或者我們知道緊急情況下重置的密碼,我們就能把門打開進入。

前兩種情況,我們都不滿足,所以前兩個簡單邏輯的結果是false,但是我們是知道緊急情況下重置的密碼的,所以整個複雜表達式的值還是true

使用括號來表明優先級

為了一個複雜表達式更容易讀懂,在合適的地方使用括號來表明優先級是很有效的,雖然它並非必要的。在上個關於門的權限的範例中,我們給第一個部分加個括號,使用它看起來邏輯更明確:

if (enteredDoorCode && passedRetinaScan) || hasDoorKey || knowsOverridePassword {
    println("Welcome!")
} else {
    println("ACCESS DENIED")
}
// 輸出 "Welcome!"

這括號使得前兩個值被看成整個邏輯表達中獨立的一個部分。雖然有括號和沒括號的輸出結果是一樣的,但對於讀程式碼的人來說有括號的程式碼更清晰。可讀性比簡潔性更重要,請在可以讓你程式碼變清晰地地方加個括號吧!