上一周,我阅读了三种常规的代码结构,即顺序结构、条件判断结构与循环结构。
这一周,我先阅读了一些不常见的控制结构及其组织策略,然后阅读了一种至今使用得不多的方法,即表驱动方法。
第十七章 不常见的控制结构
17.1 子程序中的多次返回
这种结构其实我经常写,所以看到它被称为“不常见的控制结构”我还相当惊奇。不过好在书中也没有说这种结构有什么“原罪”,只是指出了它不太常规,并提到了这样一些多处返回的正确使用方法:
- 用来增加代码的可读性。假如在函数中途就计算出了结果,那么直接返回比最后一起返回更容易理解。
- 在检查到错误的条件(如错误的输入、未满足的前条件)后,直接清理现场、立刻退出,可以增强可读性,减少条件判断的层次。否则每个条件判断都用一个if-else处理,会导致非常深的缩进层次,非常难懂。
这突然让我想起了上周阅读if-else时,我便疑惑有的时候需要提前进行复杂的错误情况筛选,因此无法将最常见、最正常的状况写在最前面。现在比较安心了,看来我将错误处理放在前面不是错误的做法。
私以为,这些方法的本质在于,在某种情况下(如提前计算出结果,或者发现前条件不满足)时,函数的功能已经执行完毕或者无法继续执行,因此直接返回能够简化代码、增加可读性。
17.2 递归
(我开始怀疑作者跟我脑回路非常不同……因为我也很喜欢递归……这次结对作业我还用了递归……
不过公正来讲,递归确实不算一个常用的模式。
之前用到递归的情景中,我印象最深刻的就是dfs、树(尤其二叉树)。书中又提到了快排算法。
递归方法将问题拆解成若干个该问题本身,从而调用自己求解。递归非常技巧化、优雅,适用于小问题,但对于大问题容易降低可读性,推荐使用普通迭代方法。
使用递归需要注意:
- 一定要确保递归有结束条件。这也是我想到递归的第一反应。(想起了算法的特性之一即是可结束性。)
- 使用安全计数器,防止无穷递归。私以为也可以防止爆栈(这次结对项目我还爆过栈……)。可以与上一条结合使用,不过个人感觉这个方法像是保底的措施,而且对于__规模确确实实有可能增长到很大__的问题,设定计数器似乎不合适。
- 把递归限定在一个子程序内。循环递归可读性很低,容易出错。
- 注意栈。
- 不要用递归完成完全可以用普通循环、迭代实现的功能,如计算阶乘、斐波拉契数列。这样可读性更差。这启发我,也许递归使用的是分治法,A问题可以分割成若干个小的A问题,问题规模指数型增长;而阶乘、斐波拉契数列这样的问题,实质上是A问题经过计算后转化成了另一个A问题,这用循环解决就好了。
17.3 goto
似乎大部分书籍都非常不建议使用goto,但是书中指出其实现在goto仍然很常用。
goto的反对观点
goto破坏了代码缩进格式,破坏了代码的分块,破坏了自上而下的结构,降低了代码可读性,也难以跟踪、不利于编译器优化。
对goto的征讨最先由Dijkstra发起,随后便被观察于实验证实。提这一句是因为突然看到Dijkstra大佬被震撼了!
goto的支持观点
goto应该被谨慎地使用,在一些情况下,goto非常有用。goto可以减少重复代码,可以用于过程最后清理资源,可以加快运行速度。
在实践中,我也确实遇到了这样的问题。如函数结束前需要清理资源,分散在各个地方违背DRY原则,写在一处又需要多层条件判断来保证最后在一个地方退出函数。大胆设想一下这是语言设计不够完善,对函数或者代码段可以像对异常处理一样,提供一个finally方法。
错误处理与goto
- 用goto在过程最后清理资源,就是刚才说的finally问题。
- 用goto简化错误条件判断,在条件错误时通过goto跳过一段代码,类似函数中途退出。
用非goto的方式如何完成这些工作:
- 用深层次的if-else嵌套,然而这样非常复杂,可读性、可维护性不强。
- 用状态变量代替goto。
- 用try-finally方法代替goto。不过个人认为try-finally作为异常处理,应该用于处理真正的异常,这其实也是这本书之前也强调过的一点。
- (所以我没有什么好方法……
if语句中的某一层想要调用else语句
非常奇怪的逻辑,如
if( a ){
if( b ){
Do sth1;
goto STH3;
}
} else{
Do sth2;
STH3:
Do sth3;
}
这种逻辑非常奇怪,很难想到什么时候需要用……而且很容易重构错。可以选择的方法:
- 假如Do sth3比较复杂,可以提取为一个单独的子程序。
- 如若不然,可以重新排列if-else关系。
goto使用原则
在现代高级语言中,大部分问题不需要goto,小部分需要goto的问题中,也有一大部分可以转换为其他结构。goto太危险,因此要少用。一些原则如下:
- 不要用goto完成完全可以用已有的规范控制结构完成的工作。
- 如果语言内置了等价的控制结构,不要用goto完成。
- 如果试图用goto提高性能,要审慎衡量得失。
- 每个程序内尽量只使用一个标号
- 尽量让goto向前跳转而非向后跳转
- 确认所有goto标号都被用到了,删除无用的标号
- 确认goto不会产生不可达的代码。
(讨论了这么多goto,但我几乎不用goto……
第十八章 表驱动法
18.1 表驱动法使用总则
表驱动法:为了简化逻辑,直接将输入-输出关系对应成一张表格,通过查表法获取结果,快捷又高可读性。
两个的问题:
- 用什么方法查表。可用方法包括直接访问、索引访问、阶梯访问
- 表中存放什么数据。
18.2 直接访问表
通过数组这样的直接访问方式访问表项。如:用数组获取12个月各有几天,或用多重数组获取各种情况下医保的保费。
用于表示太过复杂以至于无法用代码计算(如保费),或者完全没什么内在逻辑的数据(如各个月各有几天(反正我觉得没逻辑))。
书中给出了一个非常强大的例子。有一个信息流,有20种可能的信息,各自有对应的id;如果通过if-else判断代码会非常恐怖,就算用类封装起来内部也还是有恐怖的if-else语句。然而如果通过查表可以轻松解决,而且将各种信息体现为数据而非硬编码,更加易于维护。
对于没有数据-结果一一对应关系的数据,如0 ~ 18对应一个表项、19 ~ 25对应一个表项等等,如何构造查询表的键值:
- 复制数据,从而有一一对应关系。太暴力,而且不容易修改,只适用于超简单的情况。
- 用表达式将数据转换为键值。
- 建立单独的函数将数据转换为键值,可读性、可维护性更强。
18.3 索引访问表
就不浪费笔墨解释什么是索引访问表了。
优点:
- 表查询方法的普遍优点。
- 对于子表可以较满填充、但主表填充不满的情况,索引表能节约空间。
- 可以用多种方法进行索引。
18.4 阶梯访问表
这是一个没怎么使用的技巧。
假如对于某个值,处在不同的区间对应不同的表项,可以将不同区间端点列为一张表,通过循环与各个端点比较便可以得到所在区间。
这个方法有表驱动方法的普遍优点,它与其他表驱动方法区别在于,其他方法试图解决键值与表项数据的无规律对应,而这个方法试图解决原始数据与键值的无规律对应。
需要注意:
- 注意每一种可能的上端点,不要允许某些数据跑掉。
- 注意 < 与 <= 之间的区别,> 与 >= 同理。
- 考虑用二分查找取代顺序查找。
- 阶梯方法比较耗时(而且个人觉得容易错),在数据项不多的情况下可以考虑用索引表替代。
后记 关于读书笔记的一点抱怨
说是抱怨,其实真的是抱怨。
半个学期过去了,我发现我对于读书笔记的理解和老师、助教的理解产生了一些分歧。
在我的心目中,我其实是编程领域的入门者。我对于c, cpp, python这些语言有一些最粗浅的了解,正在学习java,对于OOP、封装之类的编程思想有一点大概的了解。然而,对于编程的方法论,尤其是如何去写一个大型的项目,我其实一无所知。
在此前的两次作业中,我的这个特点其实能够清晰地表现出来。我总是无谓地想要增加封装、增加安全性、做测试……什么都想做,但其实并没有真正地理解编程的方法和思想,所以往往要很多次重构、困扰,同一段代码写了很多很多遍,却看着他人已经开心地快要写完。
对于自己的初学者身份,我认识得非常清楚。
所以,对于阅读各种书籍,我的理解其实是阅读课本。我非常希望获取一整套编码的原则和方法论,来指导我系统地理解编程究竟是一个怎样的存在,需要我做什么。
也就是因此,我阅读的第一本书是《程序员修炼之道》,因为我从名字认为这本书能够给我一个综述的指导。事实证明,这本书也确实做到了。这本书更多是概念上的指导,它里面讨论到的许多思想、原则我都有在之后尝试。
第二本书就是现在的《代码大全》,因为我希望更加细致地学习各种编程的细节,各种技巧,以及各种原则。事实证明,这本书比我想象中还要广博。其中提到的很多技巧我都为止叹服;但也有很多技巧我并没有什么很深的感触,因为这本书提到的VB, ada, smalltalk等等语言我没有接触,其中的一些限制我在使用c系语言时并没有感觉到;另外还有一些细节我其实不太认同。
也许助教觉得我没有写个人感想,确实有的读书笔记我写的比较赶,而没有太写个人感想,但在大部分读书笔记中,我都就我自己的理解、我对于作者的方法的看法,或者我对于作者提出的问题有一些不同的解决方法,写了挺多的。不过写在各种摘录里,确实非常容易被淹没。我写上一篇读书笔记时其实已经意识到了这个问题,当时想了要不要用另一种字体将自己的看法标注出来,但是排版之后发现非常丑陋,而且觉得这样过于做作,所以没有这样做。这一次我还考虑了要不要将知识点摘要和个人感想分成两个博客,但我认为知识点摘要和个人感想是一个有机的整体,所以也没有这样做。
出现这样的问题,其实是因为我对读书笔记的理解确实和老师助教不太一样。
我理解的读书笔记,是一边阅读课本一边进行“笔记”。所谓的个人感想,是在学习的过程中联想一些过去的思想、经历和体会,或者提出一点自己的看法,但是始终还是以记录知识为要点。因此,假如一个知识点我很认同,我其实也懒得强调“是的我认为它是对的”,而就直接摘抄上去了;只有对于确实相当认同的知识,或者有一定疑问的知识,我才会加上自己的评注。
而老师助教的理解,则是读后感。因此,按照我的写法,我的个人感想显然太少。假如要写个人的想法的话,我的理性想法怎么也不可能比书上的理性想法要多;也许老师助教希望我强调我学到的感性体会,但按照我“记笔记”的方式,感性地强调“啊我觉得这本书说得有道理太有道理了”真的没什么意义,太多的感性体会只会淹没。而且我的感性体验都是在编程中获得的,所以我更倾向于在编程之后的总结中写自己的感性体验。
我能理解老师和助教希望我们思考,而我直接摘抄似乎显得我没有怎么思考过,而且像是直接复制粘贴的。其实我真的不是复制粘贴我只有pdf啊,不过说这个没什么意义。而且许多看起来是直接摘抄知识点,其实我是用我自己的理解写的,但是助教也不可能真的为了我去查书对吧所以说这个也没意义。
另外一个和老师助教之间的误会在于,我一开始不知是上课听错了还是怎么的,以为每周要阅读一本书,所以第一周直接将《程序员修炼之道》看完了,之后慢慢感受到读太多确实压力很大而且只能敷衍了事,而且《代码大全》的内容越往后走越细腻,也没法一次性读很多了。然后我看别人的读书笔记、看老师助教的评价,我开始意识到没必要阅读很多,确实只要读一定的量,能学到东西就可以了。
以上就是至今为止,我对于读书笔记这个东西的全部想法了。写到这里,突然意识到这也算是老师希望我们写的所谓反馈吧笑。
我不知道助教会不会一直看到这里来,但是假如真的看到了这里,我很想知道这样的矛盾应该如何解决。我曾经因为我写了那么多却只得两分愤愤不平,也不全是对分数锱铢必较,只是感觉别人写了一点点无用的套话得三分,我却写了这么多,十分不公平。现在我开始理解老师和助教的想法,但我们的理解不同确实摆在这里。
假如助教能够看到这里的话,我不知道我继续以现在的模式写能不能被接受,或者如何突出自己的理解和看法。假如不能接受的话,我也可以写一份知识点笔记,再另外写一份读后感。假如说助教没看到这里来,或者看到了不打算给出建议呢,那我就一直按我自己的理解写下去了。分数随缘吧,反正不可能是一分嘛。
那今天的作业就这样吧。