人生充满选择,编程也是

人生在世,我们每天都需要进行三项重大选择:早餐吃什么,午餐吃什么,晚餐吃什么。这一度让我感到非常为难,于是我养成了一个习惯,只要在附近的餐馆发现了几种还不错的食物,我就会连续一段时间一直吃它们,直到吃腻,再尝试一下别的选择,直到又发现还比较对胃口的,就再一直吃,如此循环往复。

对于程序员来说,做选择是一件很客观理性的事情,需要根据现有条件进行分析判断从而做出一个正确的或者最优的决定。比如吃什么这个事情,可能的限制条件有很多,譬如:

  • 我是个很懒的人,吃饭地点就定在公司附近500米,于是选择范围限定在了500米以内的餐馆
  • 我有鼻炎,不能吃辣,而且我对青椒心理过敏,所以辛辣食物以及带青椒的食物被排除
  • 这个月手头有点紧,一顿50以上的不考虑
  • ……

由于我不是个吃货,很少有明确想吃什么的时候,所以对于吃饭这件事情来说影响因素实在太多,而且很多因素都很主观,如果要考虑所有因素的话就得深入探索自己的内心世界(性能消耗太大,大脑已死机)……如果不同的因素之间产生了矛盾就需要进行权衡,做出合理的牺牲,最普适的方法就是给所有的因素定一个优先级或者权重,这实在是太麻烦了(唉我突然发现可以做个App,让用户选择几个选项,譬如吃不吃辣,限定就餐范围等等,然后进行自动计算帮助用户选择今天吃什么,或许会挺有市场- -),于是我个人选择了暴力破解法——随机尝试,碰到合适的就多吃几顿。

好了说正经的,其实编程跟做人一样,也时刻面临着选择,暂且抛开架构选择、模式选择、语言选择、框架选择等内容,今天我想谈谈程序语言中的条件分支结构。

很多人觉得,对于偏向于业务的后端开发人员来说,整天写得最多的就是各种增删改查,其实不是的,他们明明是整天在调接口和写if-else(大雾)。我早先是写Java的,现在在公司差不多是自己独立开发一个iOS项目,也是说服务器端的API也是自己写(用C#),也算得上半个后端,但是我平常不会写很多if-else。大量的if-else嵌套不仅可读性差而且容易出错又难以调试,所以其实不管做什么开发,只要是写代码,大量的if-else都是应该尽量避免的。那应该如何避免呢?

首先,理清思路,保持冷静,不要做无谓的判断,不要一时意乱情迷热血上涌就写下这样的代码(以Swift为例):

//你爱或者不爱我
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)更容易被翻译成条件传送。

上面说了这么多关于性能的东西,不过我觉得在实际开发过程中还是应该着眼于代码的可读性和可维护性,性能优化还是要靠性能分析工具确定性能瓶颈进行针对性的优化。毕竟 ——

代码是写给人看的, 只是恰好能在机器上运行。

然而如果某种写法具有良好的可读性,又恰好有可能对性能提高有所帮助的话,那能用就用吧,譬如:

func getMin(num1: Int, num2: Int) -> Int {
    var min = num1
    if num1 > num2 {
        min = num2
    }
    return min
}

直接改为:

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之后。举个例子:

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的常见用法:

///对可以判等类型的判断
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,提高代码可读性的几种方法:

  • 理清思路,优化逻辑,合并重复的判断,不做无谓的判断。
  • 使用卫语句。
  • 使用条件表达式。
  • 使用模式匹配。

把代码写正确并不难,难的是写出高质量的代码,与诸君共勉。写得腰都酸了~大家国庆快乐^ ^。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Java byte code 的学习意义 为啥要学java bytecode,这就跟你问我已经会python了为...
    shanggl阅读 1,595评论 0 3
  • Swift提供了多种控制流声明。包括while循环来多次执行一个任务;if,guard和switch声明来根据确定...
    BoomLee阅读 1,887评论 0 3
  • 一、文明秩序之行人、非机动车整治 截止至19时,龙华大队共查处行人、单车违法201宗。 二、文明秩序之禁摩限电...
    傅烨阅读 284评论 0 0
  • 小x最近失恋了,男朋友是她之前上学时候的学霸,小x很喜欢男朋友,因为男朋友又帅成绩又好,这是小x所没有的。小x性格...
    马马7777777阅读 770评论 0 1
  • 乡村的夜晚,总是很宁静。 晚上九时,城市里还是车水马龙,在乡村已是万籁俱寂,大多数人都入睡了。在这一片寂静之中,我...
    心之暖暖阅读 95评论 0 0