人生在世,我们每天都需要进行三项重大选择:早餐吃什么,午餐吃什么,晚餐吃什么。这一度让我感到非常为难,于是我养成了一个习惯,只要在附近的餐馆发现了几种还不错的食物,我就会连续一段时间一直吃它们,直到吃腻,再尝试一下别的选择,直到又发现还比较对胃口的,就再一直吃,如此循环往复。
对于程序员来说,做选择是一件很客观理性的事情,需要根据现有条件进行分析判断从而做出一个正确的或者最优的决定。比如吃什么这个事情,可能的限制条件有很多,譬如:
- 我是个很懒的人,吃饭地点就定在公司附近500米,于是选择范围限定在了500米以内的餐馆
- 我有鼻炎,不能吃辣,而且我对青椒心理过敏,所以辛辣食物以及带青椒的食物被排除
- 这个月手头有点紧,一顿50以上的不考虑
- ……
由于我不是个吃货,很少有明确想吃什么的时候,所以对于吃饭这件事情来说影响因素实在太多,而且很多因素都很主观,如果要考虑所有因素的话就得深入探索自己的内心世界(性能消耗太大,大脑已死机)……如果不同的因素之间产生了矛盾就需要进行权衡,做出合理的牺牲,最普适的方法就是给所有的因素定一个优先级或者权重,这实在是太麻烦了(唉我突然发现可以做个App,让用户选择几个选项,譬如吃不吃辣,限定就餐范围等等,然后进行自动计算帮助用户选择今天吃什么,或许会挺有市场- -),于是我个人选择了暴力破解法——随机尝试,碰到合适的就多吃几顿。
好了说正经的,其实编程跟做人一样,也时刻面临着选择,暂且抛开架构选择、模式选择、语言选择、框架选择等内容,今天我想谈谈程序语言中的条件分支结构。
很多人觉得,对于偏向于业务的后端开发人员来说,整天写得最多的就是各种增删改查,其实不是的,他们明明是整天在调接口和写if-else(大雾)。我早先是写Java的,现在在公司差不多是自己独立开发一个iOS项目,也是说服务器端的API也是自己写(用C#),也算得上半个后端,但是我平常不会写很多if-else。大量的if-else嵌套不仅可读性差而且容易出错又难以调试,所以其实不管做什么开发,只要是写代码,大量的if-else都是应该尽量避免的。那应该如何避免呢?
首先,理清思路,保持冷静,不要做无谓的判断,不要一时意乱情迷热血上涌就写下这样的代码(以Swift为例):
代码语言:javascript复制//你爱或者不爱我
if you.love(me) || !you.love(me) {
// 爱就在那里
Love.isRightThere()
//不增不减
Love.level
Love.level--
}
我们假设func love(someone: Person) -> Bool
这个函数是个幂等函数,也就是每次调用它产生的结果都是一致的,那上面这段代码显然是有问题的,因为you.love(me) || !you.love(me)
这部分是永真的(无论true || false还是false || true,结果都是true),所以这是句废话,可以直接删掉。你可能觉得上面这段代码就是个玩笑,平常谁会写出这样的代码,那我再举个例子:return a == b ? a : b
,是不是觉得还挺正常的?你再仔细看看……
好了,我们接下去说。if-else很多时候会被用来进行边界条件的处理,对于这种情况,我们最好是提前return而不是用else,而且在Swift2.0之后,多了一个新的关键字——guard
,非常好用。譬如:
func handle(optionalData: Int?) -> Bool {
if let data = optionalData {
if data > 0 && data < 31 {
doSomethingWith(data)
return true
} else {
return false
}
} else {
return false
}
}
这段代码的逻辑很简单,接受一个可能为空的整数,先判断它是否为空,如果不为空就取它的值,然后判断是否在0到31之间,如果在的话就把值传递给doSomethingWith(data: Int)
函数然后执行,返回true,其余情况都返回false。错是没错,但是真的丑啊- -#,我们把它改成这样:
func handle(optionalData: Int?) -> Bool {
guard let data = optionalData where data > 0 && data < 31 else {
return false
}
doSomethingWith(data)
return true
}
做的事情完全相同,只是在函数开始的时候做了提前返回,也就是使用了所谓的卫语句,之后只需要正常处理数据就好了,大大提高了代码可读性。
还有就是要善于使用条件表达式,就是<条件> ? <表达式1> : <表达式2>
这种。看过《CSAPP》(《深入理解计算机系统》)的朋友应该记得,书中有提到现代处理器通过使用流水线(pipelining)来获得高性能,当执行顺序代码的时候,流水线中充满了待执行的指令。但是当机器遇到条件分支时,它常常还不能确定是否会进行跳转,处理器采用非常精密的分支预测逻辑试图猜测每条跳转指令是否会执行。只要它的猜测还比较可靠,指令流水线中就会充满指令。然而如果预测跳转出错,那就得丢掉它为该跳转指令后所有指令所做的工作,然后再用正确跳转后的指令去填充流水线,这就是错误预测惩罚。相对于基于控制的条件转移,有一种替代策略是数据的条件转移,这种方法先计算一个条件操作的两种结果,然后再根据条件是否满足从而选取一个,只有在一些受限制的情况下,这种策略才可行,一旦可行,就可以用一条简单的条件传送指令(现代处理器都拥有)来实现它。当然在语言层面是不能直接控制的,不过至少对于GCC(GNU开发的编译器套件)来说,条件表达式(? :
)比条件分支语句(if-else
)更容易被翻译成条件传送。
上面说了这么多关于性能的东西,不过我觉得在实际开发过程中还是应该着眼于代码的可读性和可维护性,性能优化还是要靠性能分析工具确定性能瓶颈进行针对性的优化。毕竟 ——
代码是写给人看的, 只是恰好能在机器上运行。
然而如果某种写法具有良好的可读性,又恰好有可能对性能提高有所帮助的话,那能用就用吧,譬如:
代码语言:javascript复制func getMin(num1: Int, num2: Int) -> Int {
var min = num1
if num1 > num2 {
min = num2
}
return min
}
直接改为:
代码语言:javascript复制func getMin(num1: Int, num2: Int) -> Int {
return num1 > num2 ? num2 : num1
}
还有??
操作符的使用,譬如判断某个值是否为空,如果为空则给它一个默认值:
//最不好的方式
func getData(optionalData: Int?) -> Int {
let defaultData = 0
if optionalData == nil {
return defaultData
} else {
return optionalData!
}
}
//稍微好一点的方式(if let)
func getData(optionalData: Int?) -> Int {
let defaultData = 0
if let data = optionalData {
return data
} else {
return defaultData
}
}
//推荐方式(??)
func getData(optionalData: Int?) -> Int {
let defaultData = 0
return optionalData ?? defaultData
}
值得一提的是,??
这个操作符在Swift中的定义有两个版本:
@warn_unused_result
@rethrows public func ??<T>(optional: T?, @autoclosure defaultValue: () throws -> T?) rethrows -> T?
@warn_unused_result
@rethrows public func ??<T>(optional: T?, @autoclosure defaultValue: () throws -> T) rethrows -> T
我们的例子满足的是第二种情况,可以注意到这里使用了@autoclosure这个关键字,它可以把一句表达式自动封装为一个闭包,也就是说在??真正取值之前defaultValue这个表达式的值并没有被计算出来准备好,而是会延迟到判定optional为nil之后。举个例子:
代码语言:javascript复制let optional: Int? = 2
//optional不为nil,所以后面的9 * 1000 / 6 55根本不会执行
let value = optional ?? 9 * 1000 / 6 55 //value = 2
这里跟《CSAPP》中的说法似乎有点矛盾(当然书中也说了,只有在一些受限制的情况下,条件传送策略才可行),Swift语言的设计者并不想让使用??
的代码被翻译成条件传送形式,而是认为使用闭包进行延迟计算可以避免不必要的开销。
最后让我们谈谈switch
语句吧,在C语言中,switch
语句可以根据一个整数索引值进行多重分支,不仅提高了C代码的可读性,而且通过使用跳转表(jump table)这种数据结构使得实现更加高效。GCC根据case
的数量和匹配值的稀少程度(sparsity)来翻译switch
语句,当case
数量比较多(如4个以上),值的范围跨度比较小时,就会使用跳转表,跳转表是一个存储着代码段内存地址(即函数的指针)的数组,可以根据索引直接跳转到相应代码段然后执行,和一组很长的if-else语句相比,使用跳转表的优点是执行switch
语句的时间与case
的数量无关。
而在Swift中,switch后面跟的不是索引,而是一个待匹配的值。Swfit的模式匹配还比较初级,只支持相等匹配和范围匹配,使用~=
作为模式匹配的操作符,switch其实就是用它来进行模式匹配的。我们看看API中~=
的几个声明版本:
@warn_unused_result
public func ~=<I : ForwardIndexType where I : Comparable>(pattern: Range<I>, value: I) -> Bool
@warn_unused_result
public func ~=<T : Equatable>(a: T, b: T) -> Bool
@warn_unused_result
public func ~=<T>(lhs: _OptionalNilComparisonType, rhs: T?) -> Bool
/// Returns `true` iff `pattern` contains `value`.
@warn_unused_result
public func ~=<I : IntervalType>(pattern: I, value: I.Bound) -> Bool
它们接收不同的参数,从上往下依次是:某种可比较类型(数字和String)的范围输入和该类型的值、可以判等的类型、可以与nil比较的类型、一个范围输入和某个特定值的类型。返回值都是Bool。下面列举几种switch的常见用法:
代码语言:javascript复制///对可以判等类型的判断
let password = "password"
switch password {
case "password":
print("登录成功")
default:
print("密码错误")
}
//与枚举类型配合
enum Direction {
case North
case South
case East
case West
}
func goTo(direction: Direction) {
switch direction {
case .North:
print("go to north")
case .South:
print("go to south")
case .East:
print("go to east")
case .West:
print("go to west")
}
}
///对范围的判断
let num = 0
switch num {
case -1...1:
print("In Range")
default:
print("Out Range")
}
简单总结一下减少if-else,提高代码可读性的几种方法:
- 理清思路,优化逻辑,合并重复的判断,不做无谓的判断。
- 使用卫语句。
- 使用条件表达式。
- 使用模式匹配。
把代码写正确并不难,难的是写出高质量的代码,与诸君共勉。写得腰都酸了~大家国庆快乐^ ^。