讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 5.2 運算符 >

5.2 運算符

Swift運算符(如+和>等)並不是語言提供的神奇之物。事實上,它們都是函數;它們是被顯式聲明和實現的,就像其他函數一樣。這也是我在第4章指出的+可以作為reduce調用的最後一個參數進行傳遞的原因所在;reduce接收一個函數(該函數接收兩個參數)並返回一個與第一個參數類型相同的值;+實際上是函數的名字。這還解釋了Swift運算符如何針對不同的值類型進行重載的方式。你可以對數字、字符串或數組使用+,每種情況下+的含義都不同,因為名字相同但參數類型不同(簽名不同)的兩個函數是不同的;根據參數類型,Swift可以確定你調用的是哪個+函數。

這些事實不僅僅是有趣的背後實現細節。它們對於你和代碼來說都有實際的含義。你可以重載已有的運算符,並應用到自定義的對象類型上。甚至還可以創建新的運算符!本節將會對此進行介紹。

首先,我們來介紹運算符是如何聲明的。顯然,要有某種句法形式(這是個計算機科學術語),因為調用運算符函數的方式與通常的函數的方式是不同的。你不會說+(1,2),而是說1+2。即便如此,第2個表達式中的1和2都是+函數調用的參數。那麼,Swift是如何知道+函數使用了這種特殊語法呢?

為了探究問題的答案,我們來看看Swift頭文件:


infix operator + {
    associativity left
    precedence 140
}
  

這是個運算符聲明。運算符聲明表示這個符號是個運算符,它有多少個參數,關於這些參數存在哪些使用語法。真正重要的地方在於花括號之前的部分:關鍵字operator,它前面是運算符類型,這裡是infix,後跟運算符的名字。類型有:

infix

該運算符接收兩個參數,並且運算符位於兩個參數中間。

prefix

該運算符接收一個參數,並且運算符位於參數之前。

postfix

該運算符接收一個參數,並且運算符位於參數之後。

運算符也是個函數,因此你還需要一個函數聲明,表明參數的類型與函數的結果類型。Swift頭文件就是一個示例:


func +(lhs: Int, rhs: Int) -> Int
  

這是Swift頭文件中聲明的諸多+函數中的一個。特別地,它是兩個參數都是Int的聲明。在這種情況下,結果本身就是個Int(局部參數名lhs與rhs並不會影響特殊的調用語法,它表示左側與右側)。

運算符聲明與相應的函數聲明都要位於文件頂部。如果運算符是個prefix或postfix運算符,那麼函數聲明就必須要以單詞prefix或postfix開頭;默認是infix,可以省略。

我們可以重寫運算符來應用到自定義的對象類型上!下面看個示例,假設有一個裝有細菌的瓶子(Vial):


struct Vial {
    var numberOfBacteria : Int
    init(_ n:Int) {
        self.numberOfBacteria = n
    }
}
  

在將兩個Vial合併起來時,你會得到一個由兩個Vial中的細菌共同構成的一個Vial。因此,將兩個Vial加起來的方式就是將它們中的細菌加到一起:


func +(lhs:Vial, rhs:Vial) -> Vial {
    let total = lhs.numberOfBacteria + rhs.numberOfBacteria
    return Vial(total)
}
  

如下代碼用於測試新的+運算符重寫:


let v1 = Vial(500_000)
let v2 = Vial(400_000)
let v3 = v1 + v2
print(v3.numberOfBacteria) // 900000
  

對於復合賦值運算符來說,第1個參數是被賦值的一方。因此,要想實現這種運算符,必須要將第1個參數聲明為inout。下面為Vial類實現該運算符:


func +=(inout lhs:Vial, rhs:Vial) {
    let total = lhs.numberOfBacteria + rhs.numberOfBacteria
    lhs.numberOfBacteria = total
}
  

下面是測試+=重寫的代碼:


var v1 = Vial(500_000)
let v2 = Vial(400_000)
v1 += v2
print(v1.numberOfBacteria) // 900000
  

對Vial類重寫==比較運算符也是很有必要的。這需要讓Vial使用Equatable協議,當然,它不會自動使用Equatable協議,需要我們來實現:


func ==(lhs:Vial, rhs:Vial) -> Bool {
    return lhs.numberOfBacteria == rhs.numberOfBacteria
}
extension Vial:Equatable{}
  

既然Vial是個Equatable,那麼它就可以用於indexOf這樣的方法上了:


let v1 = Vial(500_000)
let v2 = Vial(400_000)
let arr = [v1,v2]
let ix = arr.indexOf(v1) // Optional wrapping 0
  

此外,互補的不等運算符!=也會自動應用到Vial上,這是因為它已經根據==運算符定義到所有的Equatable上了。出於同樣的原因,如果對Vial重寫了<並讓其使用Comparable,那麼另外3個比較運算符也會自動應用上。

接下來實現一個全新的運算符。作為示例,我向Int注入一個運算符,它會將第1個參數作為底數,將第2個參數作為指數。我將^^作為運算符符號(我本想使用^,不過它已經被佔用了)。出於簡化的目的,我省略了邊際情況的錯誤檢查(如指數小於1等):


infix operator ^^ {
}
func ^^(lhs:Int, rhs:Int) -> Int {
    var result = lhs
    for _ in 1..<rhs {result *= lhs}
    return result
}
  

代碼就是這些!下面來測試一下:


print(2^^2) // 4
print(2^^3) // 8
print(3^^3) // 27
  

在定義運算符時,考慮到運算符與其他包含了運算符的表達式之間的關係,你應該指定優先級與結合性規則。我不打算介紹細節,如果感興趣可以參考Swift手冊。手冊還列出了可作為自定義運算符名的特殊字符:


/ = - + ! * % < > & | ^ ? ~
  

運算符名還可以包含其他很多符號字符(除了其他字母數字的字符),這些字符更難輸入;請參考手冊瞭解正式的列表。