好了,我們說的夠多了,讓我們來看看運算子超載(Operator Overloading)是怎麼一回事吧。
挑戰
這次的教學單的任務非常的容易:擴充乘法運算符的數量為標準功能。你將會用它來進行字串的聯級運算(concatenation operator),但過程卻比想像中的簡單。所以你可以想像我們將完成下面的事情:
"abc" * 5 = "abc" + "abc" + "abc" + "abc" + "abc" = "abcabcabcabcabc"
在開始埋頭苦幹的撰寫程式碼之前,你可以先停下來想想看你會怎麼來解決這個問題?你會把他分成哪幾個步驟呢?當然,下面是我的做法:
- 建立結果變數(result variable) 並賦予它初始值- 預設的字串。
- 建立一個從2開始的迴圈,並且在函式接受到的數字做結束,每個迴圈的執行週期只做一件事情– 把字串加到結果變數上。
- 將結果顯示到Xcode的console上.
好了,這就是我構思的演算法-我們接著進入到實作的部分吧。
基本的運算子重載(Basic Operator Overloading)
首先,我們開啟Xcode 並且開啟playground模式。刪除playground模式下所有的內容,並且新增乘法運算函數的原形:
func *(lhs: String, rhs: Int) -> String {
}
我們剛剛建立的函數有兩個參數 – 左邊參數的型態為String
右邊參數的型態是 Int
-最後我們回傳String
型態並回傳乘法的執行結果。
有三件事情,你應該在在函式(function)的本體裡面完成。首先,建立結果變數(result variable)並賦予它初始值(initial value)-函式內的String
是可變的-你很快就會需要來變動這個值了:
var result = lhs
接下來我們執行迴圈,這個迴圈將從2開始執行,一執行到跟函式內接收到的Int數值為止才結束,我們將使用for in
的語句來實現流程控制與範圍操作的功能實現:
for _ in 2...rhs {
}
注意: 我們使用了通配符模式(Wildcard Pattern)來實作這部分的程式。通配符模式匹配並忽略任何值,包含一個底線(_)。當你不關心被匹配的值時,可以使用此模式。更多關參考的資料可以參考 這裡.
只有一件事你應該在Loop循環中執行-更新結果到結果變數(result variable)的字串變數中:
result += lhs
注意: 你也可以把上面的函示改成下面的程式來實作 –我們上面的程式碼會比較簡潔,因為我們採用加法賦值運算符(addition assignment operator)的模式。(譯者註:看來這一章節,原作者想讓大家有更多收穫,教我們很多東西呢,不熟悉加法賦值運算符的同學們可以多多學習喔。):
result = result + lhs
終於,到了回傳變數的時候到了。
return result
好了,現在我們來使用我們剛剛完成的運算符吧:
let u = "abc"
let v = u * 5
好了,就是這樣!這邊只剩下一個問題,我們只能針對字串做乘法運算。那要怎麼實現其他的類型呢?(好比說加法)所以,我們接著使用泛型運算子 (generic operators)來解決這個問題
泛型運算子(Generic Operators)
通用型別 (Generic Types) 預設不被我們的運算函示所支援,所以我們需要一個為她來建立一個協定型別(protocol Type) 。我們在playground下面添加這樣的協定型別(protocol Type):
protocol Type {
}
現在,我們已經為為先的指派運算子(assignment operator)函數加上了協定(protocol):
func +=(inout lhs: Self, rhs: Self)
這個函式擁有左邊跟右邊兩個型態為self的運算元-這是一種很特別的方式來敘述任何型別的資料都可以在協定(protocol)裡面實現。左邊的運算元被標示為inout 寫入讀出(in-out)參數,因為他的值會被修改而且會從函式裡面回傳。
另外,你也可以定義加法運算符函數的原型:
func +(lhs: Self, rhs: Self) -> Self
這個函式也擁有左邊跟右邊兩個型態為self的運算元,並且返回相加結果作為作為類型Self的值。在這種情況下,你並不需要使用inout
參數了。
下一步,我們來建立String
,Int
,Double
和Float
等類型的擴充來實現協定型別(protocol Type):
extension String: Type {}
extension Int: Type {}
extension Double: Type {}
extension Float: Type {}
注意:因為你不希望任何東西添加到默認類型擴展的實現都是空的。您只需讓他們為了遵守協議。
現在添加乘法運算符函數的原型到playground檔案中:
func *(lhs: T, rhs: Int) -> T {
}
這個函數有有兩個參數-左邊的運算元的類型是T
右手邊的運算元的類型是Int
-並返回乘法結果的值類型為T
。您可以使用類型約束,使泛型類型T
符合類型協議Type
( Type
protocol),所以現在明白加法賦值運算了嗎?
注意: 你可以定義類型約束來替代使用關鍵字的方法-雖然我們原先的做法比較簡潔:
func *(lhs: T, rhs: Int) -> T
該功能的實現是跟我們先前的情況是一模一樣的:
var result = lhs
for _ in 2...rhs {
result += lhs
}
return result
注意: 你可以加法運算來替代上面的功能-請確保在這個case中,它需要被添加在函式的prototype到protocol內,:.
現在我們來看看泛型運算子(generic operators)的運作狀況吧:
let x = "abc"
let y = x * 5
let a = 2
let b = a * 5
let c = 3.14
let d = c * 5
let e: Float = 4.56
let f = e * 5
好了,就是這樣!這邊只剩下一個問題:你使用的是標準的乘法運算符。也許你會感到有一點困惑。如果我們將它改成其他的操作方式會變得更好。
好吧,我們來看看我們怎麼來我們怎麼將它改造成客制化的運算子。
客制化的運算子
把下面的程式碼添加到playground檔案裡面去,並開始這方面的教學吧。
infix operator ** {associativity left precedence 150}
這裡到底發生了怎麼事情呢,我們一步一步的來說明它:
- 我們客製化的乘法運算符號的名字是`**`.
- 它的類型是`infix` 因為他是二進位的運算使用兩個運算子。
- 它將會由左至右來進行運算,所以他的結合方向(associativity)是往左邊。
- 它的優先權是`150` – 同層級的優先權是標準的乘法計算,因為他是比較高優先權的運算子。
注意:這邊有更多關於運算子的優先級別結合方向的相關資料。
客製化運算子的函數原型跟我們標準版的非常相近-只有名字不一樣:
func **(lhs: T, rhs: Int) -> T {
}
這個函式的實作也跟先前的一模一樣:
var result = lhs
for _ in 2...rhs {
result += lhs
}
return result
我們來試看看客製化運算子的威力吧:
let g = "abc"
let h = g ** 5
let i = 2
let j = i ** 5
let k = 3.14
let l = k ** 5
let m: Float = 4.56
let n = m ** 5
好了,就這樣!我們只剩下一個問題-這個運算子的複合版本到現在還沒定義,我們將在下一個章節解決這樣的問題。
複合運算子(compound operator)
這個複合運算子的類型,優先層級和結合方向跟我們先前提到的案例一樣-只有命名不同
infix operator **= {associativity left precedence 150}
接著我們添加複合運算子(compound operator)的原型函式在(playground)內:
func **=(inout lhs: T, rhs: Int) {
}
這個函數沒有返回類型,因為他左邊的運算元inout
。
只有一件事情你應該在函式的本體裡面完成-使用我們先前已經建構好的客製化的運算子定義來回傳乘法結果:
lhs = lhs ** rhs
現在,讓我們來測試我們建構好的運算子吧:
var o = "abc"
o **= 5
var q = 2
q **= 5
var s = 3.14
s **= 5
var w: Float = 4.56
w **= 5
好了,沒有其他更簡單的方式了!
恭喜你
運算子重載,再謹慎地使用下,可以非常的強大-我希望你可以方法可以讓他在你專案中被使用到。
作為參考,你可以 下載Playground 檔案在GitHub上。我已經在Xcode 7.3 跟Swift 2.2的環境中測試過了。
如果你對於這個教學課程或以及運算子重載(operator overloading)有任何見解,請在下面留言並分享你的想法。