第七章 高质量的子程序
创建子程序的理由:降低复杂度;引入中间的、易懂的抽象;避免代码重复;支持继承;隐藏代码执行顺序;隐藏危险操作,如指针操作;提高可移植性;简化布尔式;方便维护;避免臃肿。
不要因为操作过于简单而不愿意将其写作子程序。简单的操作写成程序可以增加代码可读性,且便于后续修改、增加该操作。
功能内聚性:理想的内聚性,让一个子程序完成且仅完成一个操作
不理想的内聚性:
顺序的内聚性:子程序按照特定的顺序完成一系列操作,这些功能共享数据,只有完成全部操作才算作一个完整的功能。
通信的内聚性:子程序中不同操作利用了同样的数据,但不存在实际联系。
临时的内聚性:子程序中各个操作仅因为同时发生才被放在一起。
不可取的内聚性:
过程的内聚性:一个子程序中若干操作按照特殊顺序发生,但并无必然联系。
逻辑的内聚性:若干并无关联的操作在同一子程序中,由传入的控制标志决定执行哪个操作。
巧合的内聚性:子程序各部分完全没有关联。
为了改善不理想的内聚性和不可取的内聚性,应对子程序的操作拆分成若干子程序。
子程序名字:
描述子程序的功能
避免含糊不清的动词(HandleCalculation, ProcessInput…)。假如动词含糊不清是因为子程序本身功能含糊不清,应该对子程序功能进行拆分。推荐使用语义强烈明确的动词配合宾语的方式起名(PrintDocument, PaginateDocument…)。
不要通过数字编号给子程序命名。
根据需要确定子程序的长度,最佳长度为9~15个字符。
要能对返回值进行描述。
准确使用对仗词语,如open/close, increment/decrement…,可以增强可读性。
在一个项目中为常用操作确立命名规则。
子程序的长度:没必要强行限制,而最好按照功能内聚性决定。但要谨慎200行以上的子程序。
参数顺序:输入用途的参数 – 既作为输入又作为输出的参数 – 作为输出的参数
一些语言中有IN OUT关键字。在cpp等没有这样的关键字的语言中可以自定义关键字,但这样没有检查机制,而且容易让人困惑。
如果几个子程序用了相似的参数,应使它们保持顺序一致。
使用所有的参数,不然就去掉这一参数。
把状态或出错变量放在最末尾。
不要将输入变量用作工作变量。
在接口中对输入数据的假定进行说明,利用注释或断言。
把子程序参数限定于七个以内。
为子程序传递足以维持其抽象的参数。
应确保形参与实参匹配,不要忽视编译器给出的warning。
无返回值的函数可以返回状态参数;也可以将状态参数的引用作为参数传递给函数。
设定函数返回值时要检查每条路径,并不要返回指向局部变量的引用或指针。
c++中含参数的宏:在表达式中将参数用括号包裹;把表达式用括号包裹;假如有多条语句,用大括号包裹。
即使采用这些方法,宏还是很危险,应该尽量避免。
c++中宏的替换:以const表示常量;以inline表示简单的内联函数;以template表示min, max等标准函数;以enum表示枚举;以typedef表示类型变换。
inline:inline函数需要写在头文件中暴露出来,违背了封装原则,应该谨慎使用。
第八章 防御式编程
防御非法输入。方法:检查标准输入流的输入值;检查函数的参数;准备错误处理机制。
断言assertion。典型用途:
检查输入参数
确认文件或流的开启/关闭状态和权限
确认仅用于输入的变量未被子程序修改
检查指针是否非空
检查容器的大小、是否空或满
检查快而复杂的算法和慢但正确的算法结果是否一致
断言常用于开发阶段,产品代码中常常不进行编译。
断言与错误处理:用错误处理处理错误的情况,用断言处理绝不应发生的情况。
(这里的错误处理似乎指泛化的错误/不当情况的处理,而不是异常exception技术)
不要将应该执行的功能代码放入断言。
可以用断言来检查合约中的前条件、后条件、不变量。(见《程序员的修炼之道》)
对于非常复杂的项目,可以同时使用断言与错误处理。
对于错误情况的可能处理:
返回中立值;换用下一个正确数据;返回和上次相同的数据;换用最接近的合法值。这些方法适用于一般的项目,健壮性的要求高于正确性的要求。
把错误信息写入日志后继续执行。可以与其他操作结合。
返回错误码。
调用错误处理函数或对象。这样耦合度太高,而且如果发生了错误的内存溢出、覆盖了这一程序的地址或数据,会无法正确调用。
显示出错信息。
在局部处理错误。这样留有很大灵活度,但整体健壮性得不到保证。
关闭程序。常用于关乎生命安全的程序。
错误处理方式应该尽量一致。
异常机制
仅用于真正的、不可忽略的、无法自行解决的异常。考虑异常的替代方案,是不是非得使用异常不可。
不要用异常推卸责任。
避免在构造函数、析构函数中抛出异常以免内存泄漏。
在恰当的抽象层次抛出异常,不要在抛出异常时泄露实现机制。
在异常信息中加入导致异常的全部信息。
不要catch异常之后不进行处理。
要了解函数库可能抛出的异常。
可以采取集中的异常处理机制,尤其是打印日志一类常规的处理机制。
使用同一、标准的异常类。
隔离错误:将错误隔离开来,如在公用方法中假设输入参数不可靠并进行处理。其他部分,如私有方法,可以假设数据无误。
方便编写代码;方便出错时检查错误来源于输入数据还是程序内部错误。
调试代码:用于在程序内部检查运行状态以及是否有错。
开发阶段对性能要求不高,不用吝惜资源。应该尽早采用调试代码。
进攻式编程:开发阶段将错误尽可能暴露出来,产品中尽可能自我修复错误。
可取方式:
采用断言。
完全填充分配到的内存以检测内存分配错误。
完全填充文件或流以检测格式错误。
假如switch语句中的default,或if-else结构中最后的else不应被达到(access),应该在那里打印警告语句甚至抛出异常,使得错误不会被忽视。
删除对象前填充垃圾数据。
在商用版本中:不要移除检查重要错误的代码。移除检查微小错误,或者会让程序硬性崩溃的代码。保留可以让程序稳妥地崩溃的代码。为技术人员保留错误信息,并确定该信息是友好的。
可以用make或宏定义处理,或编写自己的预处理脚本。
不要过度使用防御式编程、使代码臃肿不堪。要在小错误的修改成本和臃肿的代码、复杂的编写带来的成本之间权衡。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!