《代码大全2》阅读笔记3
第七章 高质量的子程序
> 创建子程序的理由:降低复杂度;引入中间的、易懂的抽象;避免代码重复;支持继承;隐藏代码执行顺序;隐藏危险操作,如指针操作;提高可移植性;简化布尔式;方便维护;避免臃肿。
> 不要因为操作过于简单而不愿意将其写作子程序。简单的操作写成程序可以增加代码可读性,且便于后续修改、增加该操作。
> 功能内聚性:理想的内聚性,让一个子程序完成且仅完成一个操作
> 不理想的内聚性:
>> 顺序的内聚性:子程序按照特定的顺序完成一系列操作,这些功能共享数据,只有完成全部操作才算作一个完整的功能。
>> 通信的内聚性:子程序中不同操作利用了同样的数据,但不存在实际联系。
>> 临时的内聚性:子程序中各个操作仅因为同时发生才被放在一起。
> 不可取的内聚性:
>> 过程的内聚性:一个子程序中若干操作按照特殊顺序发生,但并无必然联系。
>> 逻辑的内聚性:若干并无关联的操作在同一子程序中,由传入的控制标志决定执行哪个操作。
>> 巧合的内聚性:子程序各部分完全没有关联。
> 为了改善不理想的内聚性和不可取的内聚性,应对子程序的操作拆分成若干子程序。
> 子程序名字:
>> 描述子程序的功能
>> 避免含糊不清的动词(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或宏定义处理,或编写自己的预处理脚本。
> 不要过度使用防御式编程、使代码臃肿不堪。要在小错误的修改成本和臃肿的代码、复杂的编写带来的成本之间权衡。
第九章 伪代码编程过程
> 创建类的步骤:创建总体设计;设计子程序;复查并测试。
> 创建子程序的步骤:设计子程序;检查设计;编写代码;复查并测试。
> 伪代码:用近似于英文的语句精确描述子程序中的特定操作,可以忽略语法细节,专注于抽象层次、可以忽略下一层次的实现细节。
> 优点:
>> 有利于审查、修改子程序设计。
>> 支持迭代精华的思想。
>> 使注释量变小,可以以伪代码作为注释。
> 设计子程序的过程:
>> 检查前条件。
>> 定义需要解决的问题。要足够细致,包括:需要隐藏的信息;输入输出;确保前条件成立;确保后条件成立。
>> 命名,原则如前述。
>> 决定如何测试。
>> 查看标准库中是否有已有工具。
>> 考虑错误处理。
>> 少数情况下需要考虑效率。
>> 选择数据结构与算法。
>> 编写伪代码。
>> 复查伪代码。
>> 在伪代码中尝试几种设计,留下最好的想法。
> 设计子程序、编写良好的伪代码之后,编写代码会变得容易:声明函数,有些语言中可能需要提前声明变量(c),将每行伪代码转化为注释,并对应地填充代码即可。
> 假如代码过多,可以将某一部分代码量过大的伪代码分离为单独的子程序,或递归地编写伪代码,用伪代码填充伪代码,直到可以简单地将伪代码展开为代码。
> 检查代码
>> 自己先行检查或同行检查。
>> 编译:将编译器警告级别提高,使用lint一类检查工具,并检查每一个error和warning。
>> 逐行调试。
>> 消除错误。
> 收尾:检查接口;检查设计的内聚性、防御性;检查变量,如名字是否合理、是否未被使用等;检查语句逻辑;检查布局;检查文档;去除冗余注释。
> 伪代码替代方案:
>> 测试先行的编程方式。
>> 重构。
>> 契约式设计,见《程序员修炼之道》中有详细阐述。
第十章 变量
> 隐式声明可能带来错误。
应该尽量避免隐式声明(问题:python中无法避免隐式声明);遵循良好的命名规则并经常检查。
> 变量初始化策略
>> 在声明时初始化。
>> 第一次使用时初始化。
>> 理想情况下,在第一次使用时生命并初始化变量。
>> 可能情况下使用final和const。
>> 特别注意计数器、累加器。
>> 在类的构造函数中初始化其成员。
>> 检查是否需要重新初始化。
>> 由编译器初始化所有变量。
>> 注意编译器的警告信息。
>> 如果用输入数据初始化变量,记得检查输入信息合法性。
>> 使用内存访问工具检查指针是否错误。
>> 程序开始时将工作内存初始化为特定值,已检查可能的错误。
> “攻击窗口”:两次引用同一变量中间的间隔期间,变量可能被修改。
为了减少“攻击窗口”,尽量使变量局部化;减小变量引用之间的跨度;减少变量存活时间,理想的存活时间为第一次引用到最后一次引用。
> 减小变量作用域的一般原则:
>> 循环变量应该在循环开始中再定义,即for(int i=0; …; …)而非一开始就定义一个i。
>> 将相关联的、涉及同一些变量的代码放在一起。必要的话,抽出来作为一个子程序。
>> 采用最严格的可见性,需要的话再进行拓展。
> 变量的持续性:有些时候变量已经“死亡”,但引用时仍然返回原值,使人误以为没有错误。
应该:
>> 用调试代码或断言检查错误的变量取值。
>> 抛弃变量时设定其为不合理的值,如delete指针后将指针设为null。
>> 编写代码时假定其无持续性。
>> 养成使用变量前声明并初始化变量的习惯。
> 绑定时间:在编写时绑定(硬编码)不灵活、难拓展。应该晚绑定。
> 晚绑定时间:
>> 在编译时绑定:如宏定义、const、具名常量。
>> 加载时:从外部数据源读取数据。
>> 对象实例化时。
>> 调用函数时。
> 数据结构和处理数据的控制结构可以一一对应。顺序数据,如若干个不同数据,对应顺序结构。选择数据结构对应选择结构。迭代式数据结构,如容器、文件,对应循环结构。
> 一个变量只应有一个功能,避免隐含含义。