加载中...
一个函数多处 return 是好风格吗?
第1节:代码改善:一个“坑爹”的文字类冒险游戏
第2节:在禁止多重继承的情况下,如何设计“直立智慧猩猩”类?
第3节:C++多线程代码中的“乱序”执行现象
第4节:C++中函数指针有什么作用呢?
第5节:为什么我用c++写的游戏那么简陋?
第6节:多线程读写socket导致的数据混乱的原因是什么?
第7节:WebSocket 是什么原理?为什么可以实现持久连接?
第8节:怎样在c++中实现instanceof?
第9节:一个函数多处 return 是好风格吗?
第10节:C++中虚函数相比非虚函数的优势
第11节:为什么 C::C::C::C::foo() 能编译成功?
第12节:如何静态反射C++枚举的名字
第13节:看C++大叔如何拥 java 妹子入怀……
第14节:坨——理解递归实现“汉诺塔”代码的关键
第15节:C++编译器如何实现 const(常量)?
第16节:C++如何为断言加上消息
第17节:初学C++到什么水平,算是合格的初级开发工程师?
第18节:C++编程要避免使用单例模式吗?
第19节:学习C++要学boost库吗?
第20节:C++的继承就是复制吗?
第21节:C++构造函数失败,如何中止创建对象?
第22节:C++学完多线程后,学什么呢?
第23节:string_view 适合用做函数的返回值类型吗?
第24节:为指针取别名,为何影响const属性?
第25节:std::enable_shared_from_this 的存在意义?
课文封面

函数在开始正事前,如需要先做各种前提条件判断,此时,判断出问题就立刻 return 是好的做法。

问题

我在函数中用到多处 return,老师说将来招聘的HR(人力资源)看到这种用法,会直接说“再见”?

提问者给了一个例子。他的写法是:

bool foo() { if (x == x) return true; else if (x == y) return true; return false; }

按老师(估计是大学里的老师)的说法,似乎应该通过引入一个新的变量,以确保函数仅在末尾处有一个 return :

bool foo() { bool flag = false; if(x == x) flag = true; else if (x== y)flag = true; return flag; }

南老师的回答

“不要多个return”,这种说法确实曾经有过,但年代相当久远(60年前吧?我1995年时上的《软件工程》方面的大学教材,都已经只是提到,但不刻意强调了。

举个例子:通常一个函数就是要做一件事,但进入函数之后,在正式做事之前,很多时候,不得不先检查一些必要条件,称为“前置条件检查”;并且,经常在条件不成立时,除了打打日志之外,其它事情都不做,只能返回——这时推荐的做法,就是多个 return。

比如,有这么一个需求:

假设要写一个函数,入参是一个文件名 。
干的活就是打开这个文件,然后检查这个文件是不是一张图片,
如果是一张图片,再检测它是不是一张黄图。

重点:
说的时候,我们习惯按“美好路径”说下去,但其实做的时候,就得考虑各种“倒霉路径”。

  • ① 入参是一个文件名 —— 这个文件名会不会是空的?
  • ② “活”就是打开这个文件……——文件实际存在吗?就算存在,当前程序有打开权限吗?
  • ③ 检查这个文件是不是一张图片,如果是……——如果不是呢?
  • ④ 再检测它是不是一张黄图——到这里,我们才开始要做真正的业务。

这些判断如果落空,就是一堆的 return :

// 示例伪代码 结果 检测黄图 (string 文件名) { assert (文件名 != 空); // 文件名为空,通常这是程序员的错,直接断言 if (!文件存在(文件名)) { 打印日志(“玩我呢?文件都不存在”); return 文件不存在的结果; } 文件 f = 打开(文件名) ; if ( f 不 ok ) { 打印日志(“文件打不开啊,那个爱看图的家伙,是不是又在线占着这图不放?”); return 文件不OK的结果; } 数据 d = 读出全部数据(f); if ( d 不 OK) { 打印日志(“读不出数据啊,我是不配有权限是吗?”); return 文件权限不OK的结果; } 格式 fmt = 格式判断(d); if (fmt 不是 图片啊!) { 打印日志(“你们又玩我,我这鉴黄半天,结果这不是一张图”); return 文件格式不是图片的结果; } // 真正干活的留在最后 return 黄(d, AI)? 黄图的结果 : 其它颜色的结果; }

这样写的最大好处,让代表结构更扁平,代码更容易阅读,加上集中处理错误,代码逻辑更清晰。其关键点在于:条件一个个判断,任意一个不成立,就直接 返回,如此当程序 “过五关斩六将”,终于到达所有条件都成立的时候,代码仍然在函数体内的最顶层(没有缩进)。如果走反过来的逻辑:判断所有条件成立,然后做正事,这时候就不是 “一个一个”条件判断,而是“一级一级”条件判断,形如:

// 反例: 结果 检测黄图 (string 文件名) { 结果 r = "一切正常"; assert (文件名 != 空); if (文件存在) { 文件 f = 打开(文件名) ; if ( f 打开成功了 ) { 数据 d = 读出全部数据(f); if ( d 读得很OK ) { 格式 fmt = 格式判断(d); if (fmt 是 图片) { // 真正干活在这里: r = 黄(d, AI)? 黄图的结果 : 其它颜色的结果 } else { 打印日志(“你们又玩我,我这鉴黄半天,结果这不是一张图”); r = 格式不对的错误; } } else { 打印日志(“读不出数据啊,我是不配有权限是吗?”); r = 读不出数据的错误; } } else { 打印日志(“文件打不开啊,那个爱看图的家伙,是不是又在线占着这图不放?”); r = 打不开文件的错误; } } else { 打印日志(“玩我呢?文件都不存在”); r = 文件不存在的错误; } return r; // 满足只一处 return 的要求,但不仅代价大,而且意义不大 }

附加知识:为什么以前的软件工程会要求函数仅一处 return ?

因为很早很早以前的软件设计,属于重“工程”,即周期长,因此有大量的时间来做编码前的设计;并且依赖于大量手绘流程图——问题就出在这个流程图上……

当一个函数有多处 return,就意味着这函数在流程图上表达时,会有多个出口,这时候流程图就会不太好画,画出来也不好看……于是,大家就提倡“单进单出”,最终就是一个函数,最好只有,并且是仅在函数结束处,才 return 。

给个例图:

流程图

看左边的传统流程图,一个“开始”一个“停止”,非常简洁。而右边的盒式流程图(N-S图),我特加描红的底边,就是流程的结束,这是它的最大优点或特点:盒子的底边,就是一切流程分支结束的位置。