回顾一下我们上一话中的代码:
@IBAction func operate(sender: UIButton) { let operation = sender.currentTitle! switch operation{ case "×": performOperation {$0 * $1} case "÷": performOperation {$1 / $0} case "+": performOperation {$0 + $1} case "−": performOperation {$1 - $0} case "√": performOperation {sqrt($0)} default: break } } func performOperation(operation:(Double,Double) -> Double) { if appendStack.count >= 2 { displayValue = operation(appendStack.removeLast() , appendStack.removeLast()) enter() } } func performOperation(operation:Double -> Double) { if appendStack.count >= 1 { displayValue = operation(appendStack.removeLast() ) enter() } }上面这部分代码跟控制器如何展示视图没有任何关系,只涉及到数据的运算,viewcontroller中的其他代码都是跟视图有关,而这部分代码会求出我们所需要的数据,所以应该分离出来作为MVC中的M。新建一个swift文件,点击顶部工具栏中的file,选中newfile,选择swift file。
命名为CalculatorBrain,Swift中类的名字第一个字母大写,如果名字由多个单词组成,那么每个单词的首字母大写,不需要用任何分隔符号隔开,这种命名方法成为驼峰命名法。
打开新文件,里面的代码很简单,除了头注释之外只有一行:
import FoundationFoundation是一个核心的服务层,它没有关于UI的太多东西,这很好因为我们的Model应该是独立于UI的,永远都不要在模型中导入UIKit。我们在其中构建我们自己的类,不需要继承其他类,这只是个最基本的类:
class CalculatorBrain { }
var opStack = [Op]()
enum Op { case Operand case UnaryOperation }
enum Op { case Operand(Double) case UnaryOperation(String,Double ->Double) }
case BinaryOperation(String,(Double,Double) ->Double)
func pushOperand(operand:Double){ opStack.append(Op.Operand(operand)) }
func performOperation(symbol:String){ }
var knownOps = [String:Op]() //var knownOps = Dictionary<String,Op>()
var brain = CalcuatorBrain()的时候,因为没有参数,所以调用的就是CalculatorBrain中的init()这个方法。我们在init方法中初始化字典的值:
init() { knownOps["×"] = Op.BinaryOperation("×"){$0 * $1} knownOps["÷"] = Op.BinaryOperation("÷"){$1 / $0} knownOps["+"] = Op.BinaryOperation("+"){$0 + $1} knownOps["−"] = Op.BinaryOperation("−"){$1 - $0} knownOps["√"] = Op.UnaryOperation("√"){sqrt($0) } }
knownOps["√"] = Op.UnaryOperation("√",sqrt)
init() { knownOps["×"] = Op.BinaryOperation("×",*) knownOps["÷"] = Op.BinaryOperation("÷"){$1 / $0} knownOps["+"] = Op.BinaryOperation("+",+) knownOps["−"] = Op.BinaryOperation("−"){$1 - $0} knownOps["√"] = Op.UnaryOperation("√",sqrt) }
每当CalculatorBrain被创建的时候,这些运算就会被创建。我们在方法中取到运算类型。
func performOperation(symbol:String){ let option = knownOps[symbol] }
下面来说下Swift里面的公有和私有,在类中如果你想要某个方法或者属性是私有的那么就在它的定义前面加上private,如果不加,那么默认都是共有的。我们需要让需要私有的东西私有化,以保证它不会被其他的程序改动。在这个程序中,压栈和初始化这些操作应该是公有的,而Op应该是私有的,所以我们需要:
private enum Op
func evaluate()-> Double?{ }
这种运算是递归的,比如我们压入4和5,然后压入一个+,那么运算的时候会先取出+,再获取5和4作为加法的运算数,得到结果9,把9压入栈中。如果有多个运算,比如栈中的值是:* 4 + 5 6.那么首先获取乘法运算,然后得到乘法的第一个运算数4,然后获得加法运算,那么会首先进行加法运算,把得到的结果11压入栈中作为乘法运算的第二个运算数,最后得到结果44压入栈中。
我们需要继续定义一个同名的方法:
func evaluate(ops:[Op]) -> (result:Double?,remainingOps:[Op]){ }
虽然同名,但是程序可以根据参数的不同而调用不同的方法,它的返回值比较有意思,这是一个Tuple(元组)类型,逗号分隔开它的元素,你可以给每个元素命名来作为键值。那么我们在方法中使用如下代码:
if !ops.isEmpty{ let op = ops.removeLast() }上面的代码标示如果操作栈不是空的话,我们希望可以在删除栈中最后一个元素的同时取到它的值,但是你会发现这句报错了。为了解释这个问题,首先介绍一些相关概念,Swift中类和结构体的用法非常类似,但是它们有两个差别:1类可以继承,但是结构体不能。2结构体传递的是值,而类传递的是引用。结构体通常仅用于基础的数据类型,比如数组、字典这样的元素,甚至Double和Int都是结构体,这很棒因为它们可以有自己的方法。所以在evaluate方法中无论我给ops数组,它都会被拷贝。其次在你引入参数的时候其实它的前面隐含了一个let:
func evaluate( let ops:[Op])
var remainingOps = ops if !ops.isEmpty{ let op = remainingOps.removeLast() }你可能觉得这么复制不是会拖慢程序的速度么?但其实Swift不会真的复制它,直到你真正的改变它的时候,所以当传入一个拷贝的时候,并不是真的复制了,而是传入一种指针,一种它知道从哪来的指针,不是通过引用而是通过值。甚至这个数组有一万个元素,它也不会做一万次复制,只有在我真正改变的地方,它就不得不进行复制了,并且这可能甚至不会做一个完整的复制,它可能只去追踪改变的地方,这真的很智能。现在我们用开关语句来判断得到的栈顶Op
switch op { case .Operand(let operand): return (operand,remainingOps) }
private func evaluate(ops:[Op]) -> (result:Double?,remainingOps:[Op]){ var remainingOps = ops if !ops.isEmpty{ let op = remainingOps.removeLast() switch op { case .Operand(let operand): return (operand,remainingOps) case .UnaryOperation(_, let operation): let operandEvaluation = evaluate(remainingOps) if let operand = operandEvaluation.result { return (operation(operand),operandEvaluation.remainingOps) } case .BinaryOperation(_, let operation): let op1Evaluation = evaluate(remainingOps) if let operand1 = op1Evaluation.result { let op2Evaluation = evaluate(op1Evaluation.remainingOps) if let operand2 = op2Evaluation.result{ return (operation(operand1,operand2),op2Evaluation.remainingOps) } } } } return (nil,ops) }这是一个标准的递归,如果不理解的可以多看几遍仔细想想,递归到操作数就返回数值,如果递归到了操作符就继续递归直到递归到最底层再逐层上弹,在递归过程中只要发生无法计算的情况就会返回nil。大家可能注意到switch中没有写default,这是因为枚举类型Op只有三种情况,我们在Switch中已经考虑了全部情况,所以不用写Default方法。
现在让我们来完善那个没有参数的evaluate方法:
func evaluate()-> Double?{ let (result,remainder) = evaluate(opStack) return result }
下一步把运算模型的代码和控制器相关联起来,有时候使用联合视图的时候左边和右边不能展示我们想要的内容,你会发现每次点击工程目录中的文件都出现在左边,那么如何出现在右边呢,按住option键再次点击文件,就会在右边的屏幕上打开。现在我们在左边展示CalculatorBrain代码,右侧展示ViewController的代码:
删掉viewController中以前写的计算代码,就是那些我们在本话最开始展示的代码,现在viewController中的代码简洁多了,因为我们控制视图有这些代码就够了。
甚至appendStack也不需要了,因为我们在CalcuatorBrain中有opStack来处理。现在在vc中增加初始化一个CalcuatorBrain:
var brain = CalculatorBrain()我们需要修改CalcuatorBrain中的两个方法,让它们有返回值。
func pushOperand(operand:Double) -> Double?{ opStack.append(Op.Operand(operand)) return evaluate() } func performOperation(symbol:String) -> Double?{ if let operation = knownOps[symbol] { opStack.append(operation) } return evaluate() }
vc中的enter方法代码修改如下:
@IBAction func enter() { userIsInTheMiddleOfTypingANumber = false if let result = brain.pushOperand(displayValue){ displayValue = result } else { displayValue = 0 //如果出错了那么暂且设为0 } }
@IBAction func operate(sender: UIButton) { if userIsInTheMiddleOfTypingANumber{ enter() } if let operation = sender.currentTitle{ if let result = brain.performOperation(operation) { displayValue = result } else { displayValue = 0 } } }
现在来运行下试试有没有错误,可以看到界面没有变化,我们并没有修改控制器和模型的交互部分。
我们输入13按一下enter键,可以看到label中显示的是13.0,证明enter功能没有问题,然后输入 5和一个+,结果显示如下:
如果我们故意输错:
结果正确,但是这并不是一个好的提示。
最后要做的是把整个计算流程可视化,我们要做的就是用println显示栈中的内容:
func evaluate()-> Double?{ let (result,remainder) = evaluate(opStack) println("\(opStack) = \(result) with \(remainder) left over") return result }
再输入一个4:
再点击一个+:
数组中的Op再被转化成String的时候,系统并不知道该如何转化,所以把它显示成了Enum Value。那么该如何把它识别成一个String呢,做法是在Op的定义中添加计算属性:
private enum Op:Printable { case Operand(Double) case UnaryOperation(String,Double ->Double) case BinaryOperation(String,(Double,Double) ->Double) var description:String{ get{ switch self{ case .Operand(let operand): return "\(operand)" case .BinaryOperation(let symbol, _): return symbol case .UnaryOperation(let symbol, _): return symbol } } } }
这里的Printable并不是类,这也不是个继承关系,它是个接口,现在我们运行一下看看:
现在正常了,大家来试试吧。