https://yq.aliyun.com/articles/7175
https://segmentfault.com/a/1190000000342848
书上对这一部分的规范很有必要。比如,命名应该以问题为导向,关注“what”,而不是“how”,eg.一条员工数据可以是inputRec或者emloyeeData,前者是how,反映计算概念的计算机术语,而后者直指问题领域。因此,后者更佳。我常常犯这样的问题,用动宾组合来命名变量,学习之后,才知道动宾适合对子程序命名,反映其功能.
变量
变量初始化原则
- 声明的时候初始化
- 在靠近变量第一次使用的位置初始化,就近原则。
- 理想情况下,在靠近第一次使用变量的位置声明和定义该变量,但是在JS里面却习惯将变量声明提前。
- 注意计数器和累加器的修改。
- 在类的构造函数中初始化数据成员
- 确定是否需要重新初始化
- 把每个变量用于唯一用途 #### 变量作用域优化 作用域指变量在程序内的可见和可引用范围。介于同一变量多个引用点之间的代码可称为”攻击窗口(window of vulnerability)”,应把变量的引用点尽可能集中在一起,减小”攻击窗口“的范围。
- 尽量缩短变量的引用范围
- 尽量缩短变量的存活时间
- 把相关语句提取成单独的子程序
- 尽量少使用全局变量。使用全局变量可以让程序写起来很方便,因为全局变量可以随时访问和使用,但是这样很难维护和管理,如果换人来维护这些代码他很难知道这些变量在哪里在什么时候会被修改。 #### 变量命名原则
- 规范命名的目的是提高程序的可读性同时易于调试
- 变量名需要准确描述其代表的事物
- 变量名的平均长度在10到16个字符时更易于调试。这并不是说你要把所有的变量都控制在这个范围,命名的最终目的提高可读性和可维护性,当你检查代码时发现大部分变量名都很短或者含义不清时,那你的命名肯定有问题
- 长名变量适合全局变量,短名的适合局部变量
- 将计算值限定词作为后缀。Total,Sum,Average,Max,Min,Str,Pointer等表示计算的限定词一般放在后面。
- 使用业界约定俗称的变量。比如i,j,temp,flag这些,不用解释都知道。
- 使用团队命名规范,不同团队,不同语言的命名原则会有不同,优先服从规范。 代码阅读的次数远大于编写的次数,确保你的名字更易于阅读,而不是易于编写。 #### 变量缩写原则
- 使用标准的缩写,如min,sub,str等
- 去掉所有非前置元音,如computer->cmptr,screen->scrn,apple->appl
- 去掉虚词and,or,the等
- 使用单词的前几个字母,统一在每个单词的第N个字母后截断
- 去除无用后缀,如ing,ed等
- 保留每个音节中最引人注意的发音
- 确保不要因为缩写而改变了变量的含义,或者缩写后的变量名有歧义或者很难理解
语句
直线型代码
组织直线型代码最主要的原则就是按照依赖关系进行排列。所谓依赖关系就是下一行代码是否会依赖上一行代码的执行,是则为顺序相关依赖,否则为顺序无关依赖。可以用好的子程序名,参数列表,注释来让顺序相关依赖变得更明显。如果代码之间没有顺序依赖关系,那就设法使相关的语句尽可能地接近。
条件语句
if语句使用原则
- 先处理正常路径,再处理不常见情况
- 考虑else语句。虽然5到8成的代码都会有else语句,但有些情况是在程序一开始就做一个if判断,是则返回,不执行后面的代码,这样可以避免将后面的代码全都嵌套在else子语句中。但无论是否有else,请都将子句用大括号括起来。
- 简化复杂的条件检测。在if/elseif语句中,经常会有很复杂的逻辑判断,为了提高可读性,可将这些逻辑判断封装成布尔函数。
- 考虑将if/elseif 替换成case. ##### case 语句 case语句适合处理简单易分类的数据,如果你的数据并不简单,请使用if/elseif语句。
- 按字母/数字顺序排列各种情况
- 优先处理正常情况
- 按执行效率排列case语句
- 如果在某个case后面没有break,请注释说明。
- 利用default子句来检测错误
表驱动法
直接访问表
在前端开发,针对后台返回的错误码,通常不会直接用if/else判断错误码来显示相应地错误信息,而是将错误码-错误提示存放在”表“对象中,通过传入错误码来返回错误提示,这就是最简单的表驱动法——直接访问表。
当然我们可能会遇到更加复杂的情况,比如某活动要给1到100岁的人提供优惠,不同年龄的人群优惠可能相同也可能不同。如果将年龄作为key,优惠作为value,那么最笨得方法是存储100个键值对,当然这里面的值会有重复的。
解决方法就是做键值转换,将年龄转化成另外一个键,然后让该键对应到具体优惠。
索引访问表
键值转换提供了一个很好地思路,那就是将表的”查询条件“和”查询记录"分开管理,建立索引。索引访问表适合处理表记录占用空间比较大得情况,操作索引中的记录往往比操作主表本身的记录更方便廉价,并且由于索引和主表是分开的,同一个主表可以根据不同查询条件建立不同索引,灵活性更强,后期可维护性也更好。
阶梯访问表
索引访问的一个问题就是如果键的取值范围很大的话,那建立的索引就会很长很占空间,阶梯访问表则是对某些情况下的一种优化。
阶梯访问的基本思想是:表中的记录对于不同的数据范围有效,而不是不同的数据点。相对于索引访问,通常将输入数据映射到指定数据范围,饭后取得对于值的过程是比较耗时的,这其实是一种用时间换空间的方式。具体采用哪种表驱动方法,就看时间和空间哪个对你更重要了。
阶梯访问的基本思想是:表中的记录对于不同的数据范围有效,而不是不同的数据点。相对于索引访问,通常将输入数据映射到指定数据范围,饭后取得对于值的过程是比较耗时的,这其实是一种用时间换空间的方式。具体采用哪种表驱动方法,就看时间和空间哪个对你更重要了。
高质量的子程序
创建子程序最主要的目的是提高程序的可管理性,当然也有其他一些好的理由。其中,节省代码空间只是一个次要原因,更重要的是能提高可读性、可靠性和可修改性。
高质量的子程序可以:
高质量的子程序可以:
- 降低和隔离复杂度
- 引入中间层,易懂的代码
- 提高可移植性
- 改善性能
- 隐藏实现细节,隐藏全局数据
- 限制变化带来的影响
- 形成中央控制点
- 达到特定的重构目的
高质量的子程序应该是功能上高内聚的,有着良好的命名。说到命名,一直很矛盾,怎样才能算是一个好的命名?按什么标准?书中给了参考:
- 描述子程序所做的所有事情。要完整的描述一个子程序,名字可能会很长,这个时候除了使用缩写,还应该思考一下这样的子程序本身是不是有问题。
- 避免使用无意义或模糊的词。计算机是明确的,doSomething这样的函数名只是用来教学。
- 不要通过数字来标识。看到handle1,handle2这样的命名是不是很愤怒,哈哈。
- 根据需要确定子程序名字的长度。研究表明,变量名的最佳长度是9到15个字符。我不知道这个调查是针对特定编程语言还是所有编程语言,按理说应该是语言无关,但我怎么有种感觉,Java或者C++代码的命名普遍比JS中的要长? -给函数命名时要对返回值有所描述。就是说看到函数名就知道它会返回什么。比如xxx.isReady()看名字就知道返回布尔型,xxx.next()返回下一个与xxx相关的对象。
- 给过程起名时使用语气强烈的动宾形式。比如printDocument,checkOrderInfo。但是在面向对象语言中,比如JS,通常不用加宾语,因为宾语就是对象本身,比如document.print(),orderInfo.check()。
- 准确使用对仗词。比如add/remove,open/close。fileOpen对fileClose,fileOpen对fClose就会很奇怪。
- 为常用操作确定命名规则。
书中还说了一个比较有趣的问题,子程序可以写多长?理论上认为的子程序最佳长度是一屏代码或打印出来一到两页纸的长度,约20~200行(原书是50~150行)。人们已经在子程序长度的问题上做了大量统计和研究,但并非所有的这些统计都适合现代编程。不过有一点,如果你的子程序超过了200行,那你就要小心了。
子程序通常会有参数,如何组织这些参数也是门学问。下面是一些指导原则:
- 按照输入-可修改-输出的顺序排列参数,也可以考虑按照该排列规则对参数进行规范命名。
- 让所有子程序参数排列顺序保持一致。
- 使用所有参数。很遗憾,这是JS的先天缺陷,你需要更加小心。
- 把状态或者出错变量放到最后。
- 不要把子程序的参数用作工作变量,应该在子程序中使用局部变量。
calcDemo(inputVal){
inputVal = inputVal + currentAdder(inputVal)
// do something with inputVal
...
return inputVal
}
这样的代码虽然没有任何错误,但是容易造成误解,因为最后返回的inputVal已经不是最初传入的inputVal了,正确的做法是在函数内部使用局部变量指向inputVal然后返回该局部变量。这里是工程代码,不是在竞赛网站上,不能为了简洁而简洁,少写一行代码并不会给你加分。
- 在接口中对参数的假定加以说明。
- 限制子程序的参数个数。7是个很神奇的数字,让你的参数保持在七个以内。
- 为子程序传递用以维持其接口抽象的变量或对象。我在很多代码中发现,函数参数并不是一个个变量,而是一个对象,通过该对象来传递参数。
这是一个富有争议的问题。假如一个对象有10个属性,但是处理方法只用到了3个属性,那么直接传递对象就暴露了其他属性,这破坏了封装原则,增加了代码耦合。另一种观点则认为传递整个对象能使子程序更加灵活,使接口更加稳定易于扩展。
那到底何时传变量,何时传对象呢?作者认为关键在于子程序的接口想要表达何种抽象。如果要表达的抽象是子程序期望的特定数据,那么应该直接传数据,如果要表达的抽象是想拥有某个特定对象,就应该传对象。
比如,你发现在调用子程序之前都要先创建一个对象,调用完后又从对象中取出这些数据,那说明你需要的是数据而非对象。如果你发现自己经常需要修改子程序的参数表,而每次修改的参数都来自同一个对象,那说明你需要的是整个对象。
说完参数,最后来说说返回值。如果把函数按语义划分,可以分为“函数”和“过程”,”函数”有返回值,而“过程”返回void或者没有返回值。什么时候使用”函数“,什么时候使用”过程”,其实通过函数名就应该能确定下来。比如xxx.next()和xxx.fire(),前者一看就是”函数“,而后者是”过程“。
如果你使用”函数“,肯定会存在返回错误返回值的风险,尤其是当函数内有多条分支时。为减小这一风险,请确保:
如果你使用”函数“,肯定会存在返回错误返回值的风险,尤其是当函数内有多条分支时。为减小这一风险,请确保:
- 检查所有可能的返回路径
- 不要返回指向局部数据的引用或者指针
防御式编程
防御式编程的核心其实就是容错。当子程序遭遇到各种非法输入数据时也能工作。对于这些非法数据,通常有三种方式来处理:
1. 检查所有来源于外部的数据。文件,用户,网络等接口的数据都属于外部数据,这些都是不安全的。
2. 检查子程序所有的输入参数。子程序的输入数据来源于其它子程序,这里做检查是为了防止程序内部产生了非预期的数据。
3. 决定如何处理错误的输入数据。根据项目需求,你可以返回错误码,记录日志,返回一个默认的合法值或返回与前次相同的数据,具体方案视需求而定。
1. 检查所有来源于外部的数据。文件,用户,网络等接口的数据都属于外部数据,这些都是不安全的。
2. 检查子程序所有的输入参数。子程序的输入数据来源于其它子程序,这里做检查是为了防止程序内部产生了非预期的数据。
3. 决定如何处理错误的输入数据。根据项目需求,你可以返回错误码,记录日志,返回一个默认的合法值或返回与前次相同的数据,具体方案视需求而定。
第一点和第二点都是数据校验,第三点是对校验结果的处理方式。一切错误都来自于输入输出。理论上对于所有外部数据都要进行校验,因为这些数据都是不可靠不确定的,需要通过一个”过滤系统”将其过滤成确定类型的数据。这个”过滤系统”就是隔栏(barricade)。在隔栏的外面应该使用错误处理技术,在内部应该使用断言。因为隔栏内部的数据都是被清理过的,如果在内部出错那应该是程序的错误而非数据的错误。
还有一种容错方式叫异常。异常是把代码中得错误或异常事件传递给调用方代码的一种特殊手段。异常跟断言的使用情景相似,都是用来处理那些罕见或者永远不应该发生得情况。书中给出了使用异常的一些建议:
- 用异常通知程序的其他部分,进行错误消息传递。
- 只有在其他编码方式无法解决的情况下才使用异常。
- 不要把本可在局部处理的错误当成一个未捕获的异常抛出去。
- 避免使用空得catch语句,这是一种不负责任的写法。
- 了解所有函数库可能抛出的异常。
- 建立一套几种的异常处理机制。
- 考虑异常的替换方案,确保你的程序是真的需要处理异常。
过度的防御式编程会使程序变得臃肿缓慢,增加软件的复杂度,变得难以维护。所以在进行编码时呀考虑好什么地方需要防御,然后调整优先级,因地制宜。
《Code Complete》笔记(一)
构建(Construction)的确是软件工程中最主要的环节。特别的,在我们的信息学竞赛这一规模很小的“工程”中,Design和Testing可探讨的空间不大不大。Design过程中出错几乎无药可救,Testing一般不会出什么错;或者说,OI中的Design和Testing不属于“工程”的范畴,可称作工程的只有Construction,也就是Programming。
第2章
在隐喻的层面,软件业界中已经广泛地认识到了增量开发的重要性,而信息学竞赛的语境中还使用着一套相当落后的隐喻(写信?),甚至有很多人从来没用过什么软件工程意义上的隐喻。应该把写程序看成一个“培育”的过程,一点一点做,最终长出珍珠;建造/修饰的隐喻体系也不错。这两个隐喻体系和分点测试的OI在精神层面上是暗合的。
在中国的比赛的环境中,“买得到的东西”比较少,但正因为如此,我们应该更娴熟地掌握和运用它们。比如说,我已经很长时间以来都没有写过qsort了。
规划得当的项目具有“在后期改变细节设计”的能力。我们书写的代码也应当如此。比如说,把我的网络流程序的存储方式由邻接表改成邻接矩阵(或者相反),我可以保证一切需要改变的不超过5行代码。这需要一种成熟和良好的书写模式/习惯。
总的来说,隐喻这种东西对于OI来说,在软件工程的层面的启示并不大,我们使用更多的是利用隐喻来理解算法。最简单的,其实“Queue”或者“Tree”都是隐喻。
第3章
OI中,构建前的前期准备让人困惑。似乎就是想出来算法吧。但是,你真的“想出来”算法了吗?或者说……你真的准备好了吗?在写程序之前,和写程序之后,把思路在脑海中理顺一遍似乎是个好主意。
按照正确的顺序做事情,这很重要,在上一章或上上一章也提过。先把圣诞树立起来,再对它作装饰。或者说,先写好DFS的框架,再去剪枝。(注意这也只是一个类比而已。)熟练的OIer写出的DFS框架应该是很容易嵌入剪枝代码的。
发现错误的时间要尽可能接近引入错误的时间。因为修复错误的代价是随着错误诞生的时间呈指数式增长的。我不确定这是否意味着我们应该在写完每一个函数之后先看一遍再开始写下面的东西,因为看上去太浪费时间。也许做到这一点最好的办法还是尽量的增量开发吧。
选择序列式开发法和迭代式开发法各有不同的理由,他们适用范围就不同。但我认为仅仅一个理由就可以让OI采用迭代式开发法(在这个语境下“增量式”似乎是更好的描述?):分点测试。
“需求”的概念在软件开发中性命攸关,但在OI中似乎真的没有它的位置。“架构”这种东西似乎也一样。
OI中,解决一道题有多少时间需要花费在前期准备上?似乎是因题而异的。唯一可以确定的大约有两点:如果你发现看完题后一秒钟就可以开始写代码了了,那么最好再看一遍;如果这道题的“前期准备”花了10min(或者一个更为妥帖的时间限制)屏幕上还没有一行像样的代码,那么暂时放弃。
第4章
编程语言的选择对软件项目影响巨大,这是经过严格的研究证实的,它甚至影响程序员面对问题时的思维。想必OI中也是这样,比如说你可以看到USACO月赛中从Bronze到Silver至Gold的组别中C/C++对Pascal的比率严格递增。或者说,NOI中使用C/C++的选手比例肯定会比NOIp中高。这也许是互为因果的。
编程约定是一个有用的东西。在我的程序中,它会以变量或函数处的一行注释体现。——变量的意义,表示特殊意义的值(例如-1表示还未计算),调用的先决条件,等等。它们是在实现前写的,而何时需要它们则是一个相当经验性的东西。
《Code Complete》笔记(二)
在OI这种极限运动中,与Design这东西有关的主要还是Algorithm Design,至于实现方面的Design由于代码量真的很小应该并不需要做,只需要按照惯常的风格和习惯去做就行了。所以说专业编程中的设计工作很险恶,但OI基本上还是在一个温室的环境里进行的。
设计的过程了无章法,就像在最终得出优美的正确算法之前会有很多稀奇古怪的东西出现。但“设计”就是为了犯错——让可能会在实现过程中出现的错误尽早地出现和被解决。这是一个启发式的过程。
软件的首要技术使命是管理复杂度(managing complexity),这一点应该会让所有OIers颔首。但也许你首要想到的是时间/空间复杂度之类的东西。不过最先需要解决的问题是思维复杂度,它决定经你手写出程序的正确性。正确性,还有完整性(不能只写了一半),是比时空复杂度更先需要解决的问题。既然大脑不能把一个程序全部装进去,那么就需要把程序模块化,保证每个子程序的短小精悍和整体把握。设计的目标是易于理解,而不是smart。设计范畴内的每个特征都值得注意。
设计是分层次的,在某些OI程序里我们做到这一点可以减轻思维复杂度(尽管大多数不需要分层次的思维方式)。我们分出的模块(子系统)可能是:构图模块、网络流模块、二分答案模块,其中模块之间只有一条链型的依赖(也有可能是树形的,但一般还不会复杂到DAG)。在OI程序中使用类的情况不多,事实上我现在认为应该试图尽量减少,只有在不得不这样的情况下才去声明一个类(例如程序中用到了两个平衡树);上面所说的类不包括没有成员函数的结构体。
设计有一些启发式方法。在OI中要抽象,必须要从题目的背景中跳脱出来。封装(一般指封装到函数)也是不错的注意,它让你看不到不必要的复杂度。
信息隐藏、变化、松散耦合、设计模式等概念虽然异常重要,与OI关系却不大。其它的启发式方法中对OI可能有帮助的有:为测试而设计、考虑使用蛮力突破、画一个图、保持设计的模块化。109页改变自Polya的总结有点意思。至于剩下的过于具体(或者说高级?)的设计方法在OI中更是没有必要了。
最后提及的相关书目中的某些是将来(可能是较久的将来)一定要看的。
第6章
“在计算时代的早期,程序员基于语句思考编程问题。到了20世纪七八十年代,程序员开始基于子程序去思考编程。进入21世纪,程序员以类为基础思考编程问题。”
理解类首先要理解ADT,事实上在OI中用到的类一般都仅仅是ADT而已。
类的接口的设计是一门有意思的学问,但OIers暂时不需要研究这个。
包含是has a,继承是is a,老生常谈了。不过说实话在OI程序中这两种最常见的对象间的关系我好像都没用过。
在软件工程中创建类的原因很充分,但我现在倾向于认为在OI中可以尽量避免创建它,因为写起来麻烦一些,而且可能会给代码增加不必要的复杂度。比较充分的一个原因是在代码中会被复用(比如说需要用两个平衡树),还有就是建模。
《Code Complete》笔记(三)
第7章
创建子程序的最初始目的还是为了避免重复,但是在现代编程中,这不是唯一的目的。降低复杂度,更高层次的抽象,隐藏某些信息是目的中最主要的,其它目的也都很有启发性,因为它们可以最明确无误地告诉我们何时使用子程序。
即便一个看上去过于简单没必要写成子程序的操作,只要它确实多次重复,出于更好的(是的,没有最好的)可读性的目的,还是提倡把它写成子程序。另外,短小但重复的代码带来的另一个问题是无法变化。
在子程序的层面,内聚性的意思就是一个子程序里面做的事情应该是彼此紧密相关的。功能上的内聚性是首要的,在OI中我们应该做到一个子程序只做一件事。
子程序的名字很重要,虽然在OI中似乎不用拘泥什么规则,但最好还是形成自己固定且清晰的命名习惯。当你发现你不能用简短清晰的名字说明子程序所作的事情时,这个子程序的设计大概有问题,比如说有副作用。
也许子程序的长度是一个有意思的研究领域,但是在OI中大约还没有真正“长”的子程序。我近来不喜欢在OI中为子程序而子程序的做法,也就是说任何程序都有Input、Solve、Output这样的子程序(我以前这么做)。我现在认为在OI中不重复的代码完全没必要做子程序。
对于参数表的参数顺序,首要原则我认为是重要的变量放在前面,同时参数列表相似的子程序其顺序应该一致。
函数(在C-like的语言中,特指非void函数)的返回值在某些执行路径中可能会忘记返回值,g++对此也没警告。这是我经常犯的错误……很多时候都会为此调试半天……一定要注意这个。
对于子程序的性能,不要臆测,要知道只有Profile能告诉你真正的瓶颈所在。
第8章
在OI中,一般是完全不需要“防御式编程”的。我常常采用的方法是给需要传入的数据满足一定条件的函数处加上注释,而不是在程序中采取断言之类的措施。
第9章
在写程序前写高质量的伪代码的确是个提高代码质量的好主意,但在竞赛中还是把这个过程留在脑海中吧。不过以后有空的话可以考虑把所有常用的算法自己写一份伪代码,写完以后与CLRS之类的书上的伪代码进行比较。
《Code Complete》笔记(四)
第10章
变量,显然是程序里最多使用的东西。我认为这是在OI中改善程序清晰度最重要的一个环节。
声明应该尽量靠近变量第一次使用的位置,初始化靠近声明。尽量减少变量的作用域(存活时间),能局部就不要全局,循环变量不在循环体外部使用的话就声明在内部。能采用const的不要采用神秘数值,如果保证只出现一次的话可以例外。不要采用节省一两个字节的奇怪技巧,比如说同一个变量会先后代表两个不同的事物。确保所有声明的变量都有被使用。
并不像伪代码编程过程之类的方法学,这些软件工程技巧都是“零代价”的。所以为何不将它们引入到OI中呢?
第11章
在OI中,关于变量的命名,唯一的目的是保证你自己能够在整个过程中都能完全清晰的理解。可能的情况下让它尽量自描述可以帮助理解。更值得称道的是你自己养成的从不会搞错的根深蒂固的命名规则。“何时采用命名规则”的清单里,没有OI能对上号的。变量名字没有意义(i,j,k)不要紧,值得担心的是变量名字的字面意义和实际用途不一致。
第12章
避免使用神秘数值,说过好多遍了。事实上,一般使用const声明的原因是因为你可以方便地改动它。
整数需要关注的问题是除法和溢出。除法的规则已经了解,溢出的问题需要加强估算。(我还是记不住2 147 483 647这个神秘数……还是(~0)>>1好了。)
浮点数的加减运算也会出问题,在数量级相差巨大的数之间。等量判断的常识应该是众所周知的。
数组中,需要关心的是下标的溢出,以及嵌套循环中的“下标串话(cross-talk)”。
第13章
结构体可以明确数据的关系。例如几个看上去无关的数组其实是在表示一组元素的多个不同属性。这时若定义一个struct,并使用这个struct的数组可以使代码清晰一些,但也会加大代码长度。值得权衡。当它们可能被作为一个整体来使用时,例如交换甚至排序,那么毫无疑问应该用struct了。
指针是令人亦爱亦恨的东西。它的确很方便,能简化一些东西,但也是很多很多错误的源泉。至于指针的理解,呵呵,每一个真正的C程序员都理解的。
把指针操作限制在子程序或类里面我是同意的。不过若一个名为NextLink()的函数的唯一一行就是i=i->next的话,也太形式主义了点。
应该把指针看作更“易碎”的变量来看待。对待变量的原则——声明与初始化与首次使用尽量接近之类——应该更加严格地施加于指针之上。指针的运用还是应该尽量减少,但决不应该“惧怕”到自己用数组模拟一种指针出来,那会能使强类型变弱。当指针仅仅是为了使接受它为参数的子程序能够修改此变量的时候应该使用引用。
全局变量在OI中的地位有点微妙。由于一个OI程序本身研究的是一个很“局部”的问题。所以所有和整个问题相关的变量(比如说输入进来的数据)都可以全局。真正需要避免的是把确实“局部”的东西,例如循环中的i、j、k都弄成全局的。
《Code Complete》笔记(五)
第19章
在表明bool值的时候,应该采用true和false而非1或0。如果从“理论”的方面论述这一点,可以说1和0是“神秘数”而true和false就不是。
比较长的判断语句中的bool表达式是一个容易出错的地方。在bool表达式方面降低复杂度的方法有:把它提炼到一个函数里然后忘掉它、试图用DeMorgan律简化它、用括号使它更清晰。
要理解很多语言中对于&&和||运算都有“短路求值”的处理,如果想避免它可以使用&和|。
按照数轴的顺序编写数值表达式,也就是说写MIN<i&&i<MAX而非i>MIN&&i<MAX。
在写复合语句时先写好开头和末尾的大括号,再填充内容。一种良好的习惯是将if、for后面跟的一条语句也放到一个单独的块里。
在程序中要避免三四层以上的深层嵌套,因为他们带来可能不必要的思维复杂度。可以考虑的解决方法有:重复判断一部分条件(违反DRY原则,故似乎不大可取);重构某些代码到子程序中(这个比较好);使用break语句的小trick;试图转化为一组if-else-if语句。
结构化编程就是仅仅是用顺序、选择、迭代三种控制结构的编程方法学。控制流是复杂度来源的很大一个方面。度量(思维)复杂度有一种简单的方法,但在OI中大多时候是没必要这样度量的。
第20章
软件质量的特性中有很多是与OI无关的,外在特征中也就正确性和效率需要注意,内在特征几乎都不用过多考虑。软件工程学之所以存在,很大程度上就是因为这些特征是互相矛盾的。
有时靠阅读和检查代码发现错误会比测试高效得多,极限编程(XP)的结对编程和代码检查是降低出错率的重要手段。在OI中不妨借鉴这一点,当发现错误时,先不忙着调试,先把代码看一遍。但是这样做的前提似乎是代码风格应该足够优秀,能被轻松地理解,甚至能被速读。
第21章
协同构建这部分内容,或许ACM/ICPC选手可以从中得到启迪,但对于目前的OI还是一点用途都没有。
第22章
测试在OI中最主要的体现是自己编些数据来测试正确性,这比较接近于集成测试。但与单元测试的思想类似的方法似乎也是应该应用的,也就是在编写完一个感觉上很易错的模块时应该马上独立地测试它。
《Code Complete》笔记(六)
第23章
调试本身并不是改进代码质量的方法,可以认为它只是一种补救措施、不得已的手段。在OI中应该尽量避免调试,应该在一开始避免错误的发生。调试的效率在人与人之间有巨大差距,所以培养调试能力十分有必要。
寻找缺陷和理解缺陷是调试中最费时的步骤。事实上在这点上不应该吝惜时间,一定要确保已经完全理解缺陷后再开始动手修改。
把需要尝试的事情在纸上逐条列出,这也许是一个加速调试得好途径,因为调试时的人大多都昏头昏脑的,最好有一个能让你保持清醒的东西。
优先检查最近修改过的代码。尽量对程序保持全局理解。永远不要随机地、尝试地修改代码,每一处代码改动都需要有明确的理由。一次只做一个改动。
心理因素让人看到他“希望”看到的东西,这对调试的影响有点大,所以要保持优秀且一致的编程习惯,要让变量名之间的“心理距离”尽量远。
把编译器警告级别设为最高,这是默认的。Profiler也可以当作调试工具,有时从不正常的运行时间或调用次数中可以看出错误。
第24章
一帆风顺的软件项目是神话,实际中的代码是经常需要经受剧烈变化的。好在实际中引起变化的(最?)重要原因——需求的变化——在我们的OI中不会出现,只要你别读错题了。
软件演化的基本准则是,演化应当提升程序的内在质量。
重构的定义是“在不改变软件外部行为的前提下,对其内部结构进行改变,使之更容易理解并便于修改”。需要重构的理由在OI中同样充分的有:代码重复,冗长的子程序,嵌套过深,过长的参数列表,命名不当。
特定的重构方法很多,也很系统,但在OI中还是应该尽量在一开始就写出更好的代码,毕竟你没有太多时间。
如果确定要对代码——特别是正确(指不可能WA)的代码——进行某种大幅度的修改(不一定是重构),一定先备份一下。
第25章
性能调整在OI中也许没有你一开始想象得那么重要,似乎复杂度才更王道。任何情况下“性能”都不是头等大事,正确性才是。
程序需求方面,就是可能不切实际的复杂度分析,少数时候实际运行时间无法用理论复杂度衡量。程序的设计就是基本的算法了,如果这里面出了致命问题,再多的代码调整也无济于事。最后的手段才是代码调整。
代码调整的问题在于,高效的代码并不一定就是“更好”的代码。80/20法则是一定要注意的,Profile一下嘛……“随时随地”的优化是绝对不可取的。
常见操作的相对效率那个表真是好东西。它说我们整数的赋值、整数的加减乘、浮点数的加减乘、调用一个函数、用下标访问数组等操作所需的时间都相差无几!这还真是惊人的结论……(有空还是自己验证一下。)
优化前,一定要保存代码的可运行版本。
第26章
OI中可以考虑的代码调整技术:用查询表替代复杂表达式(没用过呢还)、使用惰性求值(常用的思想);展开(这个很有点意思)、哨兵值(这个我比较喜欢)、把最忙的循环放在最里层(可能被忽视的常识)、削减强度(值得注意);尽量减少数组引用、使用缓存机制(事实上很高级的东西,值得简单地尝试);利用代数恒等式(如果真的有而你没发现的话……)、削弱运算强度(常用的)、使用正确的常量类型(以前没有注意过,也许真的很重要)、删除公共子表达式(为了程序清晰也应该做);把子程序写成内联(“现代”计算机说这样做不一定就会性能提升)。
1.相同的东西要拎出来抽象成方法使用:
这个东西是吃了大亏的,其实抽象方法这种东西你要是每次第二次要调用它的时候就把他抽象出来的话,那样是很清楚的,但要是第二次图省事就copy paste大法,到后来如果要改这段代码的话就需要改好多个地方的,如果别人问你要这个功能,你给他的是一段代码的话,他那边用过去,你这边改了需求,最后就会出现integration的错误,数据上显示出来之后去找真正的错误原因就要找好久,虽然在重构一书中有这么一句为了追求工程速度与代码优美的取舍,如果第二次你可以采取copypaste,但实际上如果真这么干的话,第三次一般会犯拖延症,然后就各种难看以及难维护的代码就出现了。
这个东西是吃了大亏的,其实抽象方法这种东西你要是每次第二次要调用它的时候就把他抽象出来的话,那样是很清楚的,但要是第二次图省事就copy paste大法,到后来如果要改这段代码的话就需要改好多个地方的,如果别人问你要这个功能,你给他的是一段代码的话,他那边用过去,你这边改了需求,最后就会出现integration的错误,数据上显示出来之后去找真正的错误原因就要找好久,虽然在重构一书中有这么一句为了追求工程速度与代码优美的取舍,如果第二次你可以采取copypaste,但实际上如果真这么干的话,第三次一般会犯拖延症,然后就各种难看以及难维护的代码就出现了。
2.transaction script:
在刚工作那段时间的时候,都是跟着别人代码的风格来写,比如写个数据存储的transaction code,本来的风格是写个注释,然后跟个十几行的调用dao代码,最后一个transaction得写个100+行,代码可读性就是基于那几行注释了,但别人来看就会很累,而抽象成方法,就那么几行调用方法名的代码,而且能很清楚的知道干了什么,自己以后有什么需求要改,只需要改那个对象的相对应方法。
在刚工作那段时间的时候,都是跟着别人代码的风格来写,比如写个数据存储的transaction code,本来的风格是写个注释,然后跟个十几行的调用dao代码,最后一个transaction得写个100+行,代码可读性就是基于那几行注释了,但别人来看就会很累,而抽象成方法,就那么几行调用方法名的代码,而且能很清楚的知道干了什么,自己以后有什么需求要改,只需要改那个对象的相对应方法。
3.关于if else以及switch:
这个东西一般取决于从页面上取个值,然后都是'1' '2' '3'或者英文缩写这些,如果代码里出现个if ('1'.equals(direction)),不是你自己,别人不看html代码是肯定不能看懂的了,然后我就会把'1' '2'这些变量提取为consts,比如MODULENAME_DIRECTION_APPROVED='1',这样之后写这些equals代码也会好读很多,但如果一个模块有10+个else if,那就又变得很难看了,用switch的话就会好看一些,但如果多个对象都要有这个方法处理的话,其实更好的方法是在做之前就做好设计,也就是code complete一书里所提到的无论多么紧急的项目,在coding之前都要做好设计,不然扩展性问题就会很严重,就比如两个类Lion和Cat,两个都是猫科动物,两个又都是动物,那肯定是再抽象一个父类出来,之后对他们的对象处理可以在父类中加方法,之后继承的子类要么调用父类的方法,需求不同override掉方法,这样就会方便得多,也不会出现各种else if的难看方法。
http://www.cnblogs.com/linlu1142/archive/2012/10/31/2748346.html书上对这一部分的规范很有必要。比如,命名应该以问题为导向,关注“what”,而不是“how”,eg.一条员工数据可以是inputRec或者emloyeeData,前者是how,反映计算概念的计算机术语,而后者直指问题领域。因此,后者更佳。我常常犯这样的问题,用动宾组合来命名变量,学习之后,才知道动宾适合对子程序命名,反映其功能.