close

今天讓我們進入第三堂課,利用之前學的 MVC 觀念將計算計的 Model 部分拉出來另行編寫。

這堂課講了很多新的東西,需要花點時間理解吸收。

下面同樣先附上課程在 Youtube 上的超連結。

這一次我們要在之前的 Calculator Project 的基礎上建立一個模型 (Model) ,所以我們要在 Xcode 上另外創建一個 File 。

選擇 iOS -> Source -> Swift File ,建立一個名字為 "CalculatorBrain" 的文件,跟我們之前建立的 Calculator Project 放在一起。

Swift File 是一個完全空白的文件,因為等一下要建立的 Model 就是一個獨立於 UI 並且不需要繼承的類 (Class) 。


不同於之前建立的 Project File 內的 ViewController.swift 中一開始就有 "import UIKit" ,我們的 File 只有一行 "import Foundation " , 下面的 "class CalCulatorBrain" 是我們要自己寫入的,因為我們所要做的就是建立一個名為 CalculatorBrain 的類 (Class) 。

Foundation 是一個核心服務層,裡面沒有什麼 UI 相關的東西。

而要注意的是,千萬不要自作聰明自己把 UIKit 導入進去,模型裡不需要 UI 。

在開始接下的內容之前,我們必須先了解兩個工具,Array (數組) 和 Enumerations (枚舉) 。

有些人可能會覺得 Array 不是應該是陣列嗎?

事實上我也這麼覺得,不過祖國同胞翻譯成數組,考慮到我的很多參考資料都是來自於那邊,大家就先暫時配合吧。

反正基本上,我們還是忠實原著,討論的時候可以直接講 "Array" ,這樣會比較有國際感~


Array ,姑且稱之為「數組」,它的數列表可以存儲同個類型的多個值,相同的值可以放置在多個不同的位置。

數據值在存儲進數組前必須有明確的類型宣告,也就是通過顯式的類型標註或類型推斷。

它的語法相對簡單,形式為 Array<sometype> ,也可以簡潔點用 [sometype] 來寫,sometype 指的是數據類型。  


Enumerations ,中文為「枚舉」,它定義了一個通用類型 (Optional) 的一組相關的值,使我們在編碼中以安全的方式使用這些值。

枚舉中,不必一定要給每個成員提供一個值,而如果有提供的話,該值可以為字串、字符、整型值或浮點值。

在 Swift 中,枚舉是 First Class ,它有很多只被類 (Class) 所支持的特徵。例如計算屬性 (Computed Property) ,用於提供枚舉當前值的附加訊息。實例方法 (Instance Method) ,用以提供枚舉所代表的值與相關功能。枚舉也可以定義構造函數 (initializer) 來提供一個初始成員值。

它的價值講白話一點就是,我可以宣告一個參數在不同的條件下等同於不同的結果。


接下來我們要提供一個棧 (Stack) 來存取我們所輸入的內容,並讓模組可以進行運算動作。

所以我們利用 Array 和 Enum 來編輯內容如下。

接下來我們需要一個動作將所輸入的值寫入棧裡面,這基本上屬於整個完整步驟的其中一步。所以假設我們單純寫一串敘述來表達動作的話,之後在其他地方可能都要全部在重寫一次,這樣會非常繁瑣。所以建議的方法是直接將其建立為函數性質,之後要用的話就直接調用函數,整個編碼會簡潔一些。

這裡我創造一個函數,由外部調用一個 operand 參數導入 operand() 函數之內,這個 operand() 函數是 multitype 的枚舉值之一,故根據函數內的敘述,當此函數被調用時,它會將運算值放進數組類型的棧 (updatStack) 之中。

Ps : 在 Swift 中,即使名稱一樣,但只要類型不同就會被視為不同的東西,不會產生衝突。


有了可用於運算的值之後 (operand) ,我們就可以將其調用並做數學處理動作,所以下面需在建立一個函數來代表這個動作。

不過在這之前,我們又有一個新的工具需要說明,那就是 Dictionary (字典) 。

Dictionary,字典是一個存儲多個相同類型值的容器,每個值 (Value) 都對應唯一的一個鍵 (Key) ,Key 是作為字典中 Value 的標示。

敘述上字典與數組看起來是同一種東西,但差別在於前者並沒有順序的概念。

字典在使用時必須明確定義存儲的鍵 (Key) 與值 (Value) 的類型,同樣利用顯性的類型標註或是類型推斷來定義。

字典的語法同樣不難,Dictionary<KeyType, ValueType> ,KeyType 代表鍵的類型,ValueType 代表值的類型。同樣可以將語法簡化為 [KeyType:ValueType]

字典的內容只有 Key 和 Value ,表示為類型或是名稱。

但假如我們需要為字典裡的屬性設置一個值或者實例時,我們就需要額外設置一個建構器 (Initializers) 來建構其初始值。


下面是將運算符號的動作已函數表達後進一步完善的編碼。

運算符號的輸入,這裡採用函數 operationProcess 來表示。

使用上來說,我希望當我輸入運算符號後,程式可以幫我得到計算結果,並將其寫入我的棧 (Stack) 內。

但這裡有個前提,那就是我必須先有個結果,我才有資料可以寫入棧內,於是各位可以在上面的編碼中看到字典和建構器的編輯。


首先我先建立一個新的屬性 operationMethod ,並定義它的類型為字典 (Dictionary) ,Swift 會經由類型判斷來得知我的意思。

String 是我的 Key 的類型,對計算機來說就是加減乘除的符號。 multitype 是我的枚舉,作為 Key 所對應的值。

接下來我要將這個字典的實例寫入我的程式中,以 init() { }來表示。

以加法為例,

operationMethod["+"] = multitype.BinaryOperation("+"){$1 + $0}

operationMethod["+"] 意指調用我的字典,並且設定 "+" 作為 Key ,設置其對應的值為 multitype.BinaryOperation("+"){$1 + $0} ,調用枚舉 multitype 中的 BinaryOperation(String, (Double, Double) -> Double) 函數做計算。最完整的程式碼除了這邊的宣告以外,還需要另外對加法器編寫一個函數去設置運算的實例。但我們前一堂課已經學過了閉包的應用,所以這邊就簡化為現在的 ("+"){$1 + $0} 形式。

為了幫助大家複習,我再將完整的程式碼寫一次給大家看。

1st stage

func BinaryOperation(String, Operation: (Double, Double) -> Double) { }

func plus(String, (Double, Double) -> Double) {

return $0 + $1 }

knownOps["+"] = Op.BinaryOperation("+", plus)

2nd stage

knownOps["+"] = Op.BinaryOperation("+", {(Double, Double) -> Double) in

return $0 + $1 })

3rd stage

knownOps["+"] = Op.BinaryOperation("+", {$0 + $1 })

4th stage

knownOps["+"] = Op.BinaryOperation("+") {$0 + $1 }


於是我們得到了一個可以幫助我們得到運算結果的字典與建構器。

所以剛剛的 operationProcess 函數內可以加上下面的實例。

let operation = operationMethod[symbol] {updateStack.append(operation) }

當輸入運算符號並得到運算結果之後,將結果寫入 updateStack 這個棧裡。

不過這邊有個問題,這份 Swift File 只是一個 Model ,它並沒有辦法自行得到結果,所以我們必須將這段敘述設置為 optional ,以避免錯誤。

所以敘述的最前面我們還要加上一個 "if" ,結果如下。

if let operation = operationMethod[symbol] {updateStack.append(operation) }


再來一個新東西,那就是 Access Control (訪問控制) 。分為三個級別, Public, Internal 和 Private 。

訪問控制可以限定使用者在源代碼或是模塊中訪問的級別,也就是控制哪些代碼可以訪問,哪些不能訪問。當我們想隱藏一些功能實現的細節,並明確指定哪些接口是使用者可以使用的,哪些是看不到的。

利用它,我們可以明確的幫類、結構體和枚舉設置訪問級別,也可以給屬性、函數、初始化方法、基本類型和下標索引設置訪問級別。也可以限定協議在一定的範圍內使用,包括裡面的全局常量、變量和函數。

Swift 中的訪問控制是基於模塊和源文件兩個概念。模塊指的是 Framework 和 App Bundle ,源文件指的是 Swift 中的 Swift File ,它通常屬於一個模塊,通常一個源文件包含一個類,類中包含函數、屬性等類型。

等等會將我們的程式碼如同影片中一樣,某些宣告前面加上 "Private" 做權限設定。


下面我們繼續將程式碼進一步完善。


可以看到最下面我們創了一個函數 evaluate() ,我們想要讓它從棧中求值並回傳結果,於是我們設置其返回類型為 "Double" 的資料。

但在上面大家可以看到我寫的是 "Double?" ,實際上我設置的是一個 "optional" 。

這是因為雖然邏輯上來說,這邊的確就是應該回傳 Double 類型的資料,但有時候使用者並不會乖乖照 SOP 來使用計算機,會有人會先亂按一通,於是我們寫程式時就要將防呆設計也列入考量範圍,所以我們要將它設為 Opional 的 Double 。意思是說,在沒有運算結果可以回傳時,程式會回傳一個 "nil" ,而當有運算結果時,程式才會回傳一個 Double 的資料。

從這個例子我們了解到了什麼時候適合使用 "NIL" ,那就是當我們想要回傳一些資料,卻又有可能沒辦法回傳的時後。


接著我另外在上面寫了一個一樣名稱為 evaluate 的函數用來求值。

這時候有人可能會問,”什麼!?為什麼要另外在建立一個函數?為什麼兩個名字一樣,不會衝突嗎?“。我知道會有人腦袋會冒出許多問號,因為我也看到我的腦袋上滿是問號~

首先,兩個函數的名稱可以一樣嗎?

是的,可以一樣。對 Swift 來說,只要他們的類型不一樣,名稱即使一樣依然被視為不同的物件。下面我幫大家標示一下兩個類型的差異,大家可以發現明顯的不同。

1. func evaluate(stack: [multitype]) -> (result: Double?, copyStack: [multitype])

2. func evaluate() -> Double?

另外一個問題是,為什麼要另外建立一個函數,不能用枚舉來區分不同類型嗎?

恩,這是因為講師使用遞歸方式來進行計算動作,所以必須讓函數回傳 Tuples 類型的結果。但是最後輸出時,我們只需要一個 Double 類型的結果,所以必須再轉換一次。


接著我們要繼續將求值的敘述寫進我們的程式中,大家應該早就發現了到目前為止,我們的模組只有 "將輸入的數字與運算符號寫入棧內" 和 ”取得資料並將其根據不同條件做對應的數學運算“ 兩個功能,”從哪裡得到值去做運算“ 和 ”將結果輸出“ 這兩個步驟都還沒有被提到,所以我們現在要將它們補上。

先從遞歸的部分講起,這一部份的目的是將棧中的資料一個一個提出來,並且根據條件做各種運算動作。

func evaluate(stack: [multitype]) -> (result: Double?, copyStack: [multitype])

這裡的 evaluate 函數會導入 multitype 類型的數組,並且回傳一個 (Double, [multitype]) 類型的結果。

接著設定一個大前提,導入的棧裡面有資料才進行動作,否則直接回傳 nil 為結果。

接著要編寫一段程式,將棧內的資料分離為結果與棧內剩餘資料兩個部分。

var copyStack = stack

let op = copyStack.removeLast()

這樣一來,op 就等於提取出來的函數 (copyStack 裡面不是數值,而是各種類型的函數),而 copyStack.removelast() 就變成剩餘的東西。

op 所提取出來的是最後輸入的資料,可能是數字,也可能是運算符號,而先前我們建立的兩個函數則會將 Double 類型的數字輸入與 String 類型的運算符號輸入改以類型的模式寫入棧內。

 func pushCaptureOperand(operand: Double) -> Double? {

        updateStack.append(multitype.Operand(operand))  }

 func operationProcess(symbol: String) -> Double? {

        if let operation = operationMethod[symbol] {

            updateStack.append(operation) }  }

operationMethod["+"] = multitype.BinaryOperation("+"){$1 + $0}

operationMethod["-"] = multitype.BinaryOperation("-"){$1 - $0}

operationMethod["×"] = multitype.BinaryOperation("×"){$1 * $0}

operationMethod["÷"] = multitype.BinaryOperation("÷"){$1 / $0}

operationMethod["√"] = multitype.UnaryOperation("√"){sqrt($0)}

operationMethod["e"] = multitype.UnaryOperation("e"){exp($0)}


提取出 op 之後,利用 switch 進行條件動作,當最後輸入項為數字時,即回傳數字。當最後輸入為運算符號時,則回傳 operation 函數。

就以二次運算為例來解釋一下程式碼的運作方式。

case .BinaryOperation(_, let operation):

                let op1Evaluation = evaluate(copyStack)    遞歸

                if let op1 = op1Evaluation.result {

                    let op2Evaluation = evaluate(op1Evaluation.copyStack)  遞歸

                    if let op2 = op2Evaluation.result {

                    return (operation(op1, op2), op2Evaluation.copyStack)

                    }

                }

這段程式碼裡面進行了兩次遞歸。假設我的們棧內 (updateStack) 有 [6.0, 6.0, +] 三個資料,執行函數 evluate 時第一步驟就會將 "operationMethod["+"] " 提出,並且根據字典 operatoinMethod 判斷出其等於 "multitype.BinaryOperation("+"){$1 + $0}" ,於是 switch 條件滿足 .BinaryOperation ,而此時棧內剩下的值,也就是 copyStack 為 [operand(6.0), operand(6.0)] 。

既然得知此時需要執行二元運算,我們必須提供兩個運算數字給它,於是我們進行第一次遞歸,將第一個數字 op1 調用出來。設一個函數 op1Evaluation 為evaluate(sopyStack) 的結果,可以將前面的剩餘值 [operand(6.0), operand(6.0)] 的最後一筆資料調用並且從原本的 copyStack 中刪除,於是 op1Evaluation 得到一組回傳的 Tuples , (operand(6.0), [operand(6.0)]) ,第一個運算數字 op1 即為 op1Evaluation.result = operand(6.0) 。此時滿足 switch case .operand(let operand) 的條件,故 op1Evaluation.result 的回傳值為 6.0 , op1 = 6.0 。

依此類推,下面我們可以接著得到 op2 = 6.0 的結論,最後回傳 (operation(op1, op2), op2Ecaluation.copyStack) ,結果為 operation(op1, op2) ,調用字典內的算法後得到運算結果。


最後將這個 Model 導入我們之前的 Calculator ,先在程式碼中加入 "var brain = ClaudeCalculatorBrain()" ,之後就可以調用這個 model 中的功能。

有個 model 幫助我們實現一些運算過程,原本程式碼中一些部分就可以刪除並且修改,使其變得更加簡潔,如下所示。


為了提升可讀性,在完成程式碼之後,我們可能會想要加入一段 print 的敘述,讓我們的操作過程可以直白的顯示,如下圖所示。

ClaudeCalculator.ClaudeCalculatorBrain.(multitype in _A76F005D5939BF9B8506FA78DCB470D0).Operand(2.0)] with result Optional(2.0) and remainder []

[ClaudeCalculator.ClaudeCalculatorBrain.(multitype in _A76F005D5939BF9B8506FA78DCB470D0).Operand(2.0), ClaudeCalculator.ClaudeCalculatorBrain.(multitype in _A76F005D5939BF9B8506FA78DCB470D0).Operand(3.0)] with result Optional(3.0) and remainder [ClaudeCalculator.ClaudeCalculatorBrain.(multitype in _A76F005D5939BF9B8506FA78DCB470D0).Operand(2.0)]

[ClaudeCalculator.ClaudeCalculatorBrain.(multitype in _A76F005D5939BF9B8506FA78DCB470D0).Operand(2.0), ClaudeCalculator.ClaudeCalculatorBrain.(multitype in _A76F005D5939BF9B8506FA78DCB470D0).Operand(3.0), ClaudeCalculator.ClaudeCalculatorBrain.(multitype in _A76F005D5939BF9B8506FA78DCB470D0).BinaryOperation("×", (Function))] with result Optional(6.0) and remainder []


恩,我剛剛所輸入的是 [2, 3, x] ,但結果出現了我很難理解的東西,這是因為 Array 的字串化有局限性,我們必須額外給它一段轉換敘述。

private enum multitype: CustomStringConvertible {

        case Operand(Double)

        case UnaryOperation(String, Double -> Double)

        case BinaryOperation(String, (Double, Double) -> Double)

        var description: String {

            switch self {

            case Operand(let operand):

                return "\(operand)"

            case UnaryOperation(let symbol, _):

                return "\(symbol)"

            case BinaryOperation(let symbol, _):

                return "\(symbol)"

            }

        }

    }

我讓我們的 enum 調用一個 protocol "CustomStringConvertible" ,它支援我們建立一個名為 "description" 的字串屬性來幫助我們將各個 case 的結果做字串轉換,於是同樣的動作下,我的顯示變成下面的樣子。

[2.0] with result Optional(2.0) and remainder []

[2.0, 3.0] with result Optional(3.0) and remainder [2.0]

[2.0, 3.0, ×] with result Optional(6.0) and remainder []


恩,這樣看起來正常多了。

那麼這一堂課的部分就先到這裡結束,有些語焉不詳的地方是因為我也常常感到困惑,所以就不太細部講解了。

我們下一堂課見。


By 宅宅阿軒


arrow
arrow
    文章標籤
    iOS8 Swift
    全站熱搜

    Cloud 發表在 痞客邦 留言(0) 人氣()