06代码大全读书笔记之二
第七章 高质量的子程序
- 创建子程序的理由:降低复杂度;引入中间的、易懂的抽象;避免代码重复;支持继承;隐藏代码执行顺序;隐藏危险操作,如指针操作;提高可移植性;简化布尔式;方便维护;避免臃肿。
- 不要因为操作过于简单而不愿意将其写作子程序。简单的操作写成程序可以增加代码可读性,且便于后续修改、增加该操作。
- 功能内聚性:理想的内聚性,让一个子程序完成且仅完成一个操作
- 不理想的内聚性:
- 顺序的内聚性:子程序按照特定的顺序完成一系列操作,这些功能共享数据,只有完成全部操作才算作一个完整的功能。
- 通信的内聚性:子程序中不同操作利用了同样的数据,但不存在实际联系。
- 临时的内聚性:子程序中各个操作仅因为同时发生才被放在一起。
- 不可取的内聚性:
- 过程的内聚性:一个子程序中若干操作按照特殊顺序发生,但并无必然联系。
- 逻辑的内聚性:若干并无关联的操作在同一子程序中,由传入的控制标志决定执行哪个操作。
- 巧合的内聚性:子程序各部分完全没有关联。
- 为了改善不理想的内聚性和不可取的内聚性,应对子程序的操作拆分成若干子程序。
- 子程序名字:
- 描述子程序的功能
- 避免含糊不清的动词(HandleCalculation, ProcessInput…)。假如动词含糊不清是因为子程序本身功能含糊不清,应该对子程序功能进行拆分。推荐使用语义强烈明确的动词配合宾语的方式起名(PrintDocument, PaginateDocument…)。
- 不要通过数字编号给子程序命名。
- 根据需要确定子程序的长度,最佳长度为9~15个字符。
- 要能对返回值进行描述。
- 准确使用对仗词语,如open/close, increment/decrement…,可以增强可读性。
- 在一个项目中为常用操作确立命名规则。
- 子程序的长度:没必要强行限制,而最好按照功能内聚性决定。但要谨慎200行以上的子程序。
- 参数顺序:输入用途的参数 – 既作为输入又作为输出的参数 – 作为输出的参数
- 一些语言中有IN OUT关键字。在cpp等没有这样的关键字的语言中可以自定义关键字,但这样没有检查机制,而且容易让人困惑。
- 如果几个子程序用了相似的参数,应使它们保持顺序一致。
- 使用所有的参数,不然就去掉这一参数。
- 把状态或出错变量放在最末尾。
- 不要将输入变量用作工作变量。
- 在接口中对输入数据的假定进行说明,利用注释或断言。
- 把子程序参数限定于七个以内。
- 为子程序传递足以维持其抽象的参数。
- 应确保形参与实参匹配,不要忽视编译器给出的warning。
第八章 防御式编程
- 防御非法输入。方法:检查标准输入流的输入值;检查函数的参数;准备错误处理机制。
- 断言assertion。典型用途:
- 检查输入参数
- 确认文件或流的开启/关闭状态和权限
- 确认仅用于输入的变量未被子程序修改
- 检查指针是否非空
- 检查容器的大小、是否空或满
- 检查快而复杂的算法和慢但正确的算法结果是否一致
- 断言常用于开发阶段,产品代码中常常不进行编译。
- 断言与错误处理:用错误处理处理错误的情况,用断言处理绝不应发生的情况。
(这里的错误处理似乎指泛化的错误/不当情况的处理,而不是异常exception技术) - 不要将应该执行的功能代码放入断言。
- 可以用断言来检查合约中的前条件、后条件、不变量。(见《程序员的修炼之道》)
- 对于非常复杂的项目,可以同时使用断言与错误处理。
- 对于错误情况的可能处理:
- 返回中立值;换用下一个正确数据;返回和上次相同的数据;换用最接近的合法值。这些方法适用于一般的项目,健壮性的要求高于正确性的要求。
- 把错误信息写入日志后继续执行。可以与其他操作结合。
- 返回错误码。
- 调用错误处理函数或对象。这样耦合度太高,而且如果发生了错误的内存溢出、覆盖了这一程序的地址或数据,会无法正确调用。
- 显示出错信息。
- 在局部处理错误。这样留有很大灵活度,但整体健壮性得不到保证。
- 关闭程序。常用于关乎生命安全的程序。
- 错误处理方式应该尽量一致。
- 异常机制
- 仅用于真正的、不可忽略的、无法自行解决的异常。考虑异常的替代方案,是不是非得使用异常不可。
- 不要用异常推卸责任。
- 避免在构造函数、析构函数中抛出异常以免内存泄漏。
- 在恰当的抽象层次抛出异常,不要在抛出异常时泄露实现机制。
- 在异常信息中加入导致异常的全部信息。
- 不要catch异常之后不进行处理。
- 要了解函数库可能抛出的异常。
- 可以采取集中的异常处理机制,尤其是打印日志一类常规的处理机制。
- 同一、标准的异常类。
- 误:将错误隔离开来,如在公用方法中假设输入参数不可靠并进行处理。其他部分,如私有方法,可以假设数据无误。
方便编写代码;方便出错时检查错误来源于输入数据还是程序内部错误。 - 代码:用于在程序内部检查运行状态以及是否有错。
开发阶段对性能要求不高,不用吝惜资源。应该尽早采用调试代码。 - 式编程:开发阶段将错误尽可能暴露出来,产品中尽可能自我修复错误。
可取方式:- 断言。
- 填充分配到的内存以检测内存分配错误。
3.全填充文件或流以检测格式错误。
4.如switch语句中的default,或if-else结构中最后的else不应被达到(access),应该在那里打印警告语句甚至抛出异常,使得错误不会被忽视。
5.除对象前填充垃圾数据。
- 版本中:不要移除检查重要错误的代码。移除检查微小错误,或者会让程序硬性崩溃的代码。保留可以让程序稳妥地崩溃的代码。为技术人员保留错误信息,并确定该信息是友好的。
可以用make或宏定义处理,或编写自己的预处理脚本。 - 过度使用防御式编程、使代码臃肿不堪。要在小错误的修改成本和臃肿的代码、复杂的编写带来的成本之间权衡。
伪代码编程过程
- 的步骤:创建总体设计;设计子程序;复查并测试。
- 子程序的步骤:设计子程序;检查设计;编写代码;复查并测试。
- 码:用近似于英文的语句精确描述子程序中的特定操作,可以忽略语法细节,专注于抽象层次、可以忽略下一层次的实现细节。
- 优点:
- 有利于审查、修改子程序设计。
- 支持迭代精华的思想。
- 使注释量变小,可以以伪代码作为注释。
- 设计子程序的过程:
- 检查前条件。
- 定义需要解决的问题。要足够细致,包括:需要隐藏的信息;输入输出;确保前条件成立;确保后条件成立。
- 命名,原则如前述。
- 决定如何测试。
- 查看标准库中是否有已有工具。
- 考虑错误处理。
- 少数情况下需要考虑效率。
- 选择数据结构与算法。
- 编写伪代码。
- 复查伪代码。
- 在伪代码中尝试几种设计,留下最好的想法。
- 设计子程序、编写良好的伪代码之后,编写代码会变得容易:声明函数,有些语言中可能需要提前声明变量(c),将每行伪代码转化为注释,并对应地填充代码即可。
- 假如代码过多,可以将某一部分代码量过大的伪代码分离为单独的子程序,或递归地编写伪代码,用伪代码填充伪代码,直到可以简单地将伪代码展开为代码。
- 检查代码
- 自己先行检查或同行检查。
- 编译:将编译器警告级别提高,使用lint一类检查工具,并检查每一个error和warning。
- 逐行调试。
- 消除错误。
- 收尾:检查接口;检查设计的内聚性、防御性;检查变量,如名字是否合理、是否未被使用等;检查语句逻辑;检查布局;检查文档;去除冗余注释。
- 伪代码替代方案:
- 测试先行的编程方式。
- 重构。
- 契约式设计,见《程序员修炼之道》中有详细阐述。
第十章 变量
- 隐式声明可能带来错误。
应该尽量避免隐式声明(问题:python中无法避免隐式声明);遵循良好的命名规则并经常检查。 - 变量初始化策略
- 在声明时初始化。
- 第一次使用时初始化。
- 理想情况下,在第一次使用时生命并初始化变量。
- 可能情况下使用final和const。
- 特别注意计数器、累加器。
- 在类的构造函数中初始化其成员。
- 检查是否需要重新初始化。
- 由编译器初始化所有变量。
- 注意编译器的警告信息。
- 如果用输入数据初始化变量,记得检查输入信息合法性。
- 使用内存访问工具检查指针是否错误。
- 程序开始时将工作内存初始化为特定值,已检查可能的错误。
- “攻击窗口”:两次引用同一变量中间的间隔期间,变量可能被修改。
为了减少“攻击窗口”,尽量使变量局部化;减小变量引用之间的跨度;减少变量存活时间,理想的存活时间为第一次引用到最后一次引用。 - 减小变量作用域的一般原则:
- 循环变量应该在循环开始中再定义,即for(int i=0; …; …)而非一开始就定义一个i。
- 将相关联的、涉及同一些变量的代码放在一起。必要的话,抽出来作为一个子程序。
- 采用最严格的可见性,需要的话再进行拓展。
- 变量的持续性:有些时候变量已经“死亡”,但引用时仍然返回原值,使人误以为没有错误。
应该:- 用调试代码或断言检查错误的变量取值。
- 抛弃变量时设定其为不合理的值,如delete指针后将指针设为null。
- 编写代码时假定其无持续性。
- 养成使用变量前声明并初始化变量的习惯。
- 绑定时间:在编写时绑定(硬编码)不灵活、难拓展。应该晚绑定。
- 晚绑定时间:
- 在编译时绑定:如宏定义、const、具名常量。
- 加载时:从外部数据源读取数据。
- 对象实例化时。
- 调用函数时。
- 数据结构和处理数据的控制结构可以一一对应。顺序数据,如若干个不同数据,对应顺序结构。选择数据结构对应选择结构。迭代式数据结构,如容器、文件,对应循环结构。
- 一个变量只应有一个功能,避免隐含含义。
第十一章 变量名的力量
- 变量名应该完全、准确地表示变量指代的事物。避免x xx xxx一类无意义变量名,避免dat tmp一类泛泛的变量名。
- 长度:最佳为1016个字符,820个字符也可以。
- 变量名与作用域:
- 短变量名,如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前缀。
- 混合语言编程时,应该使命名风格一致,即使会违背部分语言的惯例。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义