《代码大全2》读书笔记(二)

第七章 高质量的子程序

 

> 创建子程序的理由:降低复杂度;引入中间的、易懂的抽象;避免代码重复;支持继承;隐藏代码执行顺序;隐藏危险操作,如指针操作;提高可移植性;简化布尔式;方便维护;避免臃肿。

> 不要因为操作过于简单而不愿意将其写作子程序。简单的操作写成程序可以增加代码可读性,且便于后续修改、增加该操作。

 

> 功能内聚性:理想的内聚性,让一个子程序完成且仅完成一个操作

> 不理想的内聚性:

>> 顺序的内聚性:子程序按照特定的顺序完成一系列操作,这些功能共享数据,只有完成全部操作才算作一个完整的功能。

>> 通信的内聚性:子程序中不同操作利用了同样的数据,但不存在实际联系。

>> 临时的内聚性:子程序中各个操作仅因为同时发生才被放在一起。

> 不可取的内聚性:

>> 过程的内聚性:一个子程序中若干操作按照特殊顺序发生,但并无必然联系。

>> 逻辑的内聚性:若干并无关联的操作在同一子程序中,由传入的控制标志决定执行哪个操作。

>> 巧合的内聚性:子程序各部分完全没有关联。

> 为了改善不理想的内聚性和不可取的内聚性,应对子程序的操作拆分成若干子程序。

 

> 子程序名字:

>> 描述子程序的功能

>> 避免含糊不清的动词(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、具名常量。

>> 加载时:从外部数据源读取数据。

>> 对象实例化时。

>> 调用函数时。

 

> 数据结构和处理数据的控制结构可以一一对应。顺序数据,如若干个不同数据,对应顺序结构。选择数据结构对应选择结构。迭代式数据结构,如容器、文件,对应循环结构。

 

> 一个变量只应有一个功能,避免隐含含义。

 

 

第十一章 变量名的力量

 

> 变量名应该完全、准确地表示变量指代的事物。避免x xx xxx一类无意义变量名,避免dat tmp一类泛泛的变量名。

> 长度:最佳为10~16个字符,8~20个字符也可以。

> 变量名与作用域:

>> 短变量名,如i tmp常常用作临时变量。有些人因为短变量名有风险,所以建议即使临时变量也不要用短变量名。无论如何,短变量名“暗示”了局部作用域。

>> 全局变量名应该用限定词避免名字冲突,如cpp中的namespace,java中的package

> 计算值限定词,如min max total average应该放在最末尾。这样可以突出重点属性。

> 同样注意对仗词。

 

> 特定变量:

>> 循环变量:常用i j k。但假如循环很长,i j k容易混淆,或变量离开循环后还会使用,建议取有意义的名字。

>> 状态变量:避免flag status这样的无意义名字。

>> 临时变量:要警惕。尽量少用temp这样的名字。

>> 布尔值:常用名:done error found success ok…应该给布尔值取包含真/假两种状态的名字,如success,暗示了只有success和unsuccess两种状态。可以使用is开头的布尔值名字,如isFound。不要用否定意义的布尔值,如notFound。

>> 枚举类型:名字中应该暗示枚举类型本身的名字,如名为Color的枚举类型内部命名应为Color_Red, Color_Green等。不过对于一些枚举类型的使用很像类的语言,可以省略,如应该命名为Color.Red, Color.Green而非Color.Color_Red, Color.Color_Green。

>> 常量:应该根据常量的意义而非常量的值命名。

 

> 在共享开发、代码可读性很重要、需要经常或长周期维护的情况下,命名规则非常重要。

  命名规则的正式程度应该依情况而定。

> 一些可供参考命名规则:

>> 区分类与变量:

>>> 首字母大写表示类,全部小写表示变量:常用于cpp, java。缺点:差别太小;有些语言大小写不敏感,不适用于混合语言开发。

>>> 字母全部大写表示类,全部小写表示变量。缺点:cpp java中全部大写被表示常量;不适用于混合语言开发。

>>> 用t_前缀表示类型。优点:差别清晰;方法普适。缺点:不美观。

>>> 用a前缀表示变量。缺点:需要改变所有变量名,很麻烦。

>>> 对变量采用更明确的名字。

  这里没有一个十全十美的方法。个人出于习惯偏爱第一种与第五种的结合。

>> 全局变量:用g_前缀标识。

>> 成员变量:用m_前缀标识。

疑问:很多语言中成员变量直接用.运算符调用即可,为何要特殊标识?

>> 具名常量:用c_前缀标识。

疑问:个人以为具名常量可以用明确的名称表示,加上前缀不美观、不直接。而且通常的编译器会检查是否对具名常量进行修改,不至于产生错误操作。

>> 枚举类型的元素:同上。

>> 只读变量:该问题在java这样传引用的语言中尤为严重。在一些语言中可以用const保护变量,java这样的语言中可以用const前缀标识。

>> 格式化命名:统一采用一种格式化命名法,如下划线分割(c)、驼峰命名法(java)。

 

> 与语言相关的命名规则:(在此只记录个人常用语言)

>> c:c ch指字符,i j指整数下标,n指数量,p指指针,s指字符串,宏定义、typedef名字全部大写,变量名、子程序名全部小写,用下划线分割。

>> cpp:i j指整数下标,p指指针,常量、typedef、宏定义全部大写,当且仅当全部大写时用下划线分割,类、变量采用驼峰命名法,类首字母大写,变量首字母小写。

>> java:java的命名风格从一开始就规定了,借鉴了一些c cpp的成功经验。i j是整数下标,常量全部大写、用下划线分割,类(与接口)、变量名同cpp、采用驼峰命名法,访问器子程序用get和set前缀。

> 混合语言编程时,应该使命名风格一致,即使会违背部分语言的惯例。

 

第十一章:未完,tbc.

 

总结:这本书比我想象中的要琐碎很多,讨论了很多细节性的规范,难怪这么厚。

里面大部分的规范是适用于大部分语言的,尽管会不符合一部分语言的习惯;另一些规范是适用于常用语言的,如java cpp。

许多规范对我很有启发,但仍有一些让我感到不符合我的美观或实践,还有一些在我常用的语言中无法良好地实践。

这些规范都是作者的成功经验,我们应该在开发过程中批判性、尝试性地采纳。

原文地址:https://www.cnblogs.com/jennawu/p/booknotesCodeCompleteII.html