回顾一下我们上一话中的代码:
@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 { }
这个类中的数据结构不同于之前在viewcontroller中的数据结构,除了保存double类型的操作数,我们还需要保存操作符,初始化这样一个数组,用来模拟堆栈:
var opStack = [Op]()
Op类型采用enum(枚举)类型,枚举的定义方法和类很相似,枚举也可以有方法和属性,但是它的属性只能是计算类属性,并且枚举是没有继承特性的。来说说什么时候该用枚举,当你的某样东西在某种情况下是一个值,在另一情况下是另一个值,但是不可能同时拥有这两个值的时候,使用枚举是非常好的。
enum Op { case Operand case UnaryOperation }
如果是在其他编程语言中,枚举显然已经没法继续定义了,但是Swift有个非常酷的特性,你可以将数据与枚举中的case关联起来,如下:
enum Op { case Operand(Double) case UnaryOperation(String,Double ->Double) }
怎么样,这样是不是一目了然,如果是操作数,那么就是个Double的类型,如果是运算符,那么就有一个String来描述它,还有一个函数来做运算,注意在Swift中函数也是一种数据类型。很多时候我们用一对花括号{}写一个闭包来实现这个函数,或者在类中定义一个真正的函数。除了一元运算,我们还需要一个二元运算:
case BinaryOperation(String,(Double,Double) ->Double)
func pushOperand(operand:Double){ opStack.append(Op.Operand(operand)) }
上面的方法用来向栈中压入一个枚举类型,可以看到我们很容易就把double类型与枚举关联起来了。现在我们来构建操作符的方法:
func performOperation(symbol:String){ }
这个方法的参数是加减乘除等运算符,那么如何存储这些运算符和相应的运算呢,为了程序的可扩展性,我们使用字典这个结构:
var knownOps = [String:Op]() //var knownOps = Dictionary<String,Op>()
使用这两种写法都是可以的,当然第一种写法比较简单,也是推荐的写法。下面介绍一点初始化的内容,上一话中也说过了,类初始化的时候必需保证类的所有属性都被初始化,我们使用init(){}这个方法来初始化一个类,当你使用语句:
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) } }
大家应该还记得闭包的用法吧。最后一个开方的运算我们知道Swift有类的自动识别功能,那么这里的运算是传入一个Double的值然后返回一个开放后的Double值,运用了一个函数sqrt,那么我们可以这样写:
knownOps["√"] = Op.UnaryOperation("√",sqrt)
那么问题来了,上面的加减乘除运算能不能写成这样的形式呢?答案是YES!因为在Swift中+、*这些运算符号本身就是方法,只不过它们被设置成可以放到两个运算数中间而不用通过()赋值的写法,进一步简化后的初始化方法如下:
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] }
那么option是什么类型呢,你可能觉得它是个Op,但其实它的类型是Optional Op,因为你的传入的symbol可能不存在在我们的knownOps定义中,这时候就会返回nil,代表没有找到。所以在字典中查找东西的时候总是会返回可选型的值。
下面来说下Swift里面的公有和私有,在类中如果你想要某个方法或者属性是私有的那么就在它的定义前面加上private,如果不加,那么默认都是共有的。我们需要让需要私有的东西私有化,以保证它不会被其他的程序改动。在这个程序中,压栈和初始化这些操作应该是公有的,而Op应该是私有的,所以我们需要:
private enum Op
这个时候你会发现类中所有使用Op的方法或属性都会提示错误,因为Op是私有的,所以这些操作也必须是私有的。现在我们需要一个方法来返回OpStack中的运算结果。
func evaluate()-> Double?{ }
这个运算的返回值也是可选型,因为如果栈中的元素不能进行运算那么结果会返回一个nil。
这种运算是递归的,比如我们压入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])
也就是说它们是只读的,所以这个ops数组我们既不能给它添加元素,也不能删除它的元素。你可以把在引入参数时加一个var,这样就不会报错了,但是这是ops依旧是一个拷贝,我们不想这样做,更好的做法是类类中初始化一个局部变量
var remainingOps = ops if !ops.isEmpty{ let op = remainingOps.removeLast() }你可能觉得这么复制不是会拖慢程序的速度么?但其实Swift不会真的复制它,直到你真正的改变它的时候,所以当传入一个拷贝的时候,并不是真的复制了,而是传入一种指针,一种它知道从哪来的指针,不是通过引用而是通过值。甚至这个数组有一万个元素,它也不会做一万次复制,只有在我真正改变的地方,它就不得不进行复制了,并且这可能甚至不会做一个完整的复制,它可能只去追踪改变的地方,这真的很智能。现在我们用开关语句来判断得到的栈顶Op
switch op { case .Operand(let operand): return (operand,remainingOps) }
这里我用了一个let来定义case中Operand这种情况中相关的Double,返回的格式和我们方法定义的返回类型是一致的,下面完善这个Switch:
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 }
这次我们定义了一个局部的元组,evaluate传入了整个opStack。
下一步把运算模型的代码和控制器相关联起来,有时候使用联合视图的时候左边和右边不能展示我们想要的内容,你会发现每次点击工程目录中的文件都出现在左边,那么如何出现在右边呢,按住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() }
也就是说我们每次点击键盘都会进行evaluate操作,这些操作是可选类型的,作为返回值,这些返回值将用来在vc中进行判断,显示在视图中。
vc中的enter方法代码修改如下:
@IBAction func enter() { userIsInTheMiddleOfTypingANumber = false if let result = brain.pushOperand(displayValue){ displayValue = result } else { displayValue = 0 //如果出错了那么暂且设为0 } }
vc中的operate方法修改如下:
@IBAction func operate(sender: UIButton) { if userIsInTheMiddleOfTypingANumber{ enter() } if let operation = sender.currentTitle{ if let result = brain.performOperation(operation) { displayValue = result } else { displayValue = 0 } } }
这就是IOS开发的神奇之处,你只用很少的代码就可以控制流程。我们的计算器不仅功能完善,还具有很好的扩展性,你可以自己定制运算。
现在来运行下试试有没有错误,可以看到界面没有变化,我们并没有修改控制器和模型的交互部分。
我们输入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 }
这种写法是不是超酷的?运行一下看看,输入一个3:
再输入一个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并不是类,这也不是个继承关系,它是个接口,现在我们运行一下看看:
现在正常了,大家来试试吧。