Swift 千金方 1 - 闭包

参考 Stanford CS193p 的例子进行一定的精简(目的是为了集中于闭包机制本身),用一个计算器功能函数的实例来介绍闭包的强大威力。


Swift 千金方的想法来自于《千金方》,意在记录那些我学习 Swift 遇到的有意思的技巧与思路,一步一步导出最终的结论。需要注意的是,因为开发环境以及语言版本可能存在的差异性,没有办法保证时效性,仅供参考。

我当前的开发环境

  • Xcode 7.2
  • OS X 10.11.2
  • Swift 2.1
  • iOS 9.2 (iPhone 6s, iPod Touch 6th, iPad 3)

实例介绍

计算器作为非常好的入门实例,其中有一个需要实现的功能就是加减乘除的处理,或者更抽象一点说,是运算符号的处理。如果仔细思考一下,就会发现运算符号有一些基本的特点:

  • 对于特定的运算符来说,其操作数的形式是一定的,比如说加法,那就是需要左操作数和右操作数
  • 而运算符可以通过操作数的数量来分类,比方说加减乘除等是一类,开根号这种只需要一个操作数的可以算作另一类
  • 在相同的类别里,计算的逻辑是非常相似的,比如说加减乘除其实都只是一个符号的差别

假设我们现在要写一个函数来完成加减乘除以及开方的操作,函数原型为

var operandStack = Array<Double>()
// 用来存操作数,直接 type inference 所以不用显式声明类型
func operate(operation: String) -> Double{
// TODO
}

如何设计,才能让代码最精简,并且可读性更强呢?接下来就一步一步来优化,从最开始比较笨的设计,一步一步利用 Swift 的特性来进行『升级』

思路分析

第一个思路:简单粗暴复制

笨办法,用来演示代码可以写得多糟糕,千万不要为了图省事儿这么写。因为太简单粗暴,就不用介绍思路,直接上代码:

func operate(operation: String) -> Double{
switch operation {
case "*":
if operandStack.count >= 2{
return operandStack.removeLast() * operandStack.removeLast();
}
case "/":
if operandStack.count >= 2{
return operandStack.removeLast() / operandStack.removeLast();
}
case "+":
if operandStack.count >= 2{
return operandStack.removeLast() + operandStack.removeLast();
}
case "-":
if operandStack.count >= 2{
return operandStack.removeLast() - operandStack.removeLast();
}
case "sqrt":
if operandStack.count >= 1{
return sqrt(operandStack.removeLast());
}
}
default: break
}

这一大段复制粘贴,简直酸爽,千万不要这么写代码!会被炒鱿鱼的!

第二个思路:把函数作为参数来统一逻辑

第一个思路的问题在于代码的大量重复,非常没有意义而且还增加维护的难度,有没有可能我传入一个函数,然后这个函数执行对应的运算逻辑,这样就不用无意义重复了,于是代码可以写成这样,注意注释中强调的问题:

func operate(operation: String) -> Double{
switch operation {
case "*": return performOperationTwo(multiply)
case "/": return performOperationTwo(divide)
case "+": return performOperationTwo(plus)
case "-": return performOperationTwo(minus)
case "sqrt": return performOperationOne(square)
default: break
}
}
func mutiply(op1: Double, op2: Double) -> Double {
return op1 * op2
}
func divide(op1: Double, op2: Double) -> Double {
return op1 / op2
}
func plus(op1: Double, op2: Double) -> Double {
return op1 + op2
}
func minus(op1: Double, op2: Double) -> Double {
return op1 - op2
}
func square(op: Double) -> Double {
return sqrt(op)
}
func performOperationTwo(operation: (Double, Double) -> Double) -> Double{
if operandStack.count >= 2{
return operation(operandStack.removeLast(), operandStack.removeLast())
}
}
// 注意,在 Swift 2.1 以前,原本这个函数和上一个函数可以用同一个名字,利用不同的
// 传入函数类型来进行区分
// 一个是 (Double, Double) -> Double
// 另一个是 Double -> Double
// 现在这种做法已经不可以,所以需要两个不一样的函数名称
func performOperationOne(operation: Double -> Double) -> Double{
if operandStack.count >= 1{
return operation(operandStack.removeLast())
}
}

这样一来,虽然原本函数里面的逻辑非常清晰了,但是却增加了七个函数,而且很多还长得差不多。接下来,就可以利用 Swift 的语言特点来进行精简了

第三步:使用匿名函数来精简代码

multiply 函数为首的计算代码其实都可以通过如下形式写成匿名函数,就清晰很多

func operate(operation: String) -> Double{
switch operation {
case "*": return performOperationTwo({
(op1: Double, op2: Double) -> Double in
return op1 * op2
})
case "/": return performOperationTwo({
(op1: Double, op2: Double) -> Double in
return op1 / op2
})
case "+": return performOperationTwo({
(op1: Double, op2: Double) -> Double in
return op1 + op2
})
case "-": return performOperationTwo({
(op1: Double, op2: Double) -> Double in
return op1 - op2
})
case "sqrt": return performOperationTwo({
(op: Double) -> Double in
return sqrt(op)
})
default: break
}
}
func performOperationTwo(operation: (Double, Double) -> Double) -> Double{
if operandStack.count >= 2{
return operation(operandStack.removeLast(), operandStack.removeLast())
}
}
// 注意,在 Swift 2.1 以前,原本这个函数和上一个函数可以用同一个名字,利用不同的
// 传入函数类型来进行区分
// 一个是 (Double, Double) -> Double
// 另一个是 Double -> Double
// 现在这种做法已经不可以,所以需要两个不一样的函数名称
func performOperationOne(operation: Double -> Double) -> Double{
if operandStack.count >= 1{
return operation(operandStack.removeLast())
}
}

这样一来我们就去掉了原来那几个显式声明的函数,但是这样看起来还是有些乱,能不能再进一步呢?

第四步:利用 Swift 的类型推断进一步精简代码

当然可以!因为我们在 performOperationTwoperformOperationOne 中传入的参数是有类型的,所以在声明匿名函数的时候,其实可以省略掉类型,甚至返回类型和 return 关键字本身都可以省略,例如

case "*": return performOperationTwo({
(op1: Double, op2: Double) -> Double in
return op1 * op2
})

可以改为

case "*": return performOperationTwo({ (op1, op2) in op1 * op2 })

还可以再精简一些,匿名函数其实连传入参数名称都可以省略

case "*": return performOperationTwo({ $0 * $1 })

但是大括号在小括号里总有点怪怪的,这个也有办法解决,如果这个传入的函数是参数列表里的最后一个,其实可以把大括号放到外面去,像这样:

case "*": return performOperationTwo(){ $0 * $1 }

更进一步,因为这个函数是唯一的传入参数,小括号其实都可以省略,于是变成像这样

case "*": return performOperationTwo{ $0 * $1 }

最后就可以把代码改写成如下形式

func operate(operation: String) -> Double{
switch operation {
case "*": return performOperationTwo{ $0 * $1 }
case "/": return performOperationTwo{ $0 / $1 }
case "+": return performOperationTwo{ $0 + $1 }
case "-": return performOperationTwo{ $0 - $1 }
case "*": return performOperationOne{ sqrt($0) }
default: break
}
}
func performOperationTwo(operation: (Double, Double) -> Double) -> Double{
if operandStack.count >= 2{
return operation(operandStack.removeLast(), operandStack.removeLast())
}
}
// 注意,在 Swift 2.1 以前,原本这个函数和上一个函数可以用同一个名字,利用不同的
// 传入函数类型来进行区分
// 一个是 (Double, Double) -> Double
// 另一个是 Double -> Double
// 现在这种做法已经不可以,所以需要两个不一样的函数名称
func performOperationOne(operation: Double -> Double) -> Double{
if operandStack.count >= 1{
return operation(operandStack.removeLast())
}
}

这样一来就完成了。

总结

通过以上四个步骤,我们把原来的一大段重复且可读性很差的代码优化成了非常精简和可读可拓展的代码,所以我们在写代码的过程中不能只是想着完成就好(这就是为什么我不喜欢刷题),而是应该从更高的层次去理解并实现代码。

捧个钱场?