zoukankan      html  css  js  c++  java
  • 《代码大全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。

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

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

  • 相关阅读:
    数据库索引的作用和长处缺点
    iOS安全攻防(三):使用Reveal分析他人app
    SVN server的搭建
    腾讯2014年实习生招聘笔试面试经历
    JAVA数组的定义及用法
    一年成为Emacs高手(像神一样使用编辑器)
    给想上MIT的牛学生说几句
    四个好看的CSS样式表格
    dede 留言簿 多个
    破解中国电信华为无线猫路由(HG522-C)自己主动拨号+不限电脑数+iTV
  • 原文地址:https://www.cnblogs.com/jennawu/p/booknotesCodeCompleteII.html
Copyright © 2011-2022 走看看