■本文写作动机
经常听到“请读一下代码”或“读代码”等诸如此类的话,却没有见过有人写出怎样能读好代码的文章。是不是既然是程序员就理所当然知道程序的阅读技巧了呢?
但谁都不能否认读懂别人写的代码绝对不是件轻松的事。其实读代码不但是必要的,跟写代码一样也有技术或模式可言。那么,代码应该怎么读呢?以C语言为前提,不知不觉总结了些明确的东西。
■方针的决定
确定了要读的程序,应该怎么做呢?什么方针也没有只是一味地从main开始读,因该很难读懂程序的意思吧。首先要明确读程序的目的,专注于此。所有程序都必须读时,要分解成一部分一部分来读。
■解析的手法
解析手法大概可以分为静态手法和动态手法两类。所谓静态手法就是指只读程序本身。而动态手法是通过使用调试等分析程序运行时的动的一面。
一般的解析应从动态解析开始为宜。静态解析或多或少是想象程序的动作的。与此相对的动态解析,看到的即是事实的东西。首先从事实出发很容易找到方向,同时也减少了错误。这与在最优化前要取得特性的描述有相似之处吧。或者也可以说成,事件的解决最好从现场开始。
■静态解析
◇使用对象程序
没有程序就无从下手。首先必须清楚是什么程序,程序是怎么动作的。
◇读文档
这与上一项比较相似,必须先知道规格。如果有内部结构的说明材料,也一定要读一下。要检查一下是否有“HACKING”及“TOUR”等为名字的文件。
◇读路径结构
看看路径是以怎样的方针构成的。程序是怎么作的,都有哪些部分,要把握这样的概要。确定一下各个模块之间都有哪些关系。
◇读文件构成
看看文件是以怎样的方针组成的,同时要分析一下文件中的函数。文件名就像是注释的内容一样,要引起重视。
试着找出函数名的命名规则。如果是C程序,对extern函数应该使用了前缀,这种前缀用来区分函数的种类。如果是面向对象的程序,前缀通常表明函数的所属信息,属于重要信息。(例:rb_str_push)
◇省略语的调查
要列出很难理解的省略语,及早调查。比如“GC”的意思是garbage collection呢还是graphic context呢,意思差的很大。使用英语单词的场合经常有只取字头,或省略母音的情况。特别是要是调查好在对象程序的特定应用领域中常用的省略语。
笔者仅举一个记着的例子。某个Lisp应用中,程序中使用了很多“blt”这个前缀,究竟是什么意思一直困惑不解。这实际上是built-in function (嵌入式函数)的意思。明白的话可能觉得没什么,可是不理解的话对程序的把握就增加了相当的难度。
◇弄清楚数据结构
因为是程序,了解了数据结构的话就基本可以说读懂了一半。写程序时也是一样,对数据结构的说明远比对程序逐行注释更具价值。(这句话好像在那本书里写的,哪本书呢。)
※补充: 是“程序书法”一书。以下摘自p.168。“对程序的注释的最有效的方法之一是:对数据的使用的解释。对主要的变量若能做出可能是什么样的值以及值是怎样变化的解释的话,但这一点就可认为是很不错的注释了。”
这本书是1974年出版的。
※补充2:“C程序诊断室”一书中也有类似的论述。以下摘自p.78。“放弃使用流程图吧。流程图是反映控制流程的,所以它不好。程序是处理数据的,数据不同控制的流程就不同。要记住,数据永远是主体。对变量及参数等数据的有效定义与使用能大幅度地改善程序的组织。所以数据构造图比流程图更具价值。一定要写清楚数据是什么意思!”
休闲话题:C语言中的数据结构通常是指使用struct或union。这些重要的数据结构通常定义在头文件中。当然内部结构定义在.c中以及也有动态构建数据结构的情况,不最终读函数就不清楚。即使这样也应该从头文件读起。读头文件时文件名就显得重要了,比如frame.h这样的文件,应该是staff frame的定义。
猜想数据结构时要注意结构体成员。结构体的定义中若有next指针就应该判断为链表。同样,存在parent・children・sibling这样的要素的话,十之八九是树结构。
◇把握函数之间的调用关系
除了函数名这也是一种重要的信息。特别是函数数目多的时候就更显得重要了。这里应该使用工具,能有做成图的工具当然好,没有的话把最重要的部分自己画一画。没有必要在画图上花费太长时间,在纸的背面简单画些草图足以。
用图来显示函数之间的调用关系叫做调用图(call graph)。仅反映程序中函数之间调用关系的图称为静态调用图(static call graph),反映实际运行时函数之间的调用关系的图称为动态调用图(dynamic call graph)。
但是,经过检索发现,日语的文章中“call graph”通常指dynamic call graph,而static call graph通常指“函数间调用关系”。但是因为static和dynamic对应起来容易理解,笔者就称为动态调用图和静态调用图。
◇读函数
读函数的动作。最好一边读函数关联图,一边一部分一部分地理解。传统的自上而下式的程序应该从main开始,与GUI相似的在主循环中不停循环的程序以回调(CallBack)为顺序来把握。这主要取决于函数之间的关系。
这里,要使用调查关键词的定义位置的工具(见后述)。如前所述,定义函数及变量的函数名本身就包含了分类等重要信息。
同时,读程序之前要考虑“不读什么”。不管以何种手段,全读的话会花费大量时间。错误处理,不要的分支,基本达不到的情况要略过。防御处理作的很好的话,这部分工作相对比较容易。反之,要让这部分很轻松就要使用异常等防御处理。
◇随心所欲来替换
这并不是一个解析的阶段,而是一种手法。人类的头脑真是不可思议,为了加深记忆充分利用了身体的各个部分。很多人喜欢用纸而非电脑的键盘,不能单单理解为一种怀旧情怀。就是因为单单通过显示器来读的话,印象肤浅,所以一边写一边读。这样的话相对容易读懂代码。不是很好的名字要替换,晦涩难懂的深略语不单只记录下来,也可替换。
但是,一定要将原来的程序备份以下,以免途中发现不合适时可以跟原来的程序对比一下。不这样的话,很容易被自己的一个小小失误而陷入困境。
◇替换后运行
跟前一项类似,只不过这里要将程序运行一下看看。比如,对动作很难理解的部分改变一下参数或程序试着运行一下。这样的话从运行的变化中可以推测出代码的意思。
还有就是,应该与原来的代码的运行情况作一下对比。
■名字的重要性
写到这里好像程序的读解就是名字的调查。文件名,函数名,变量名,类型名,成员名等,似乎程序就是一堆名字。名字是程序的抽象化的最强大的武器,这可能是理所当然的,但当读程序的时候意思到这一点,能大大地提高效率。
■读变更记录
程序一般都附有变更的记录这样的材料。比如GNU的软件必然有ChangeLog这个文件。这对理解“程序这么变更的理由”最有帮助。
还有,若使用了CVS或SCCS这样的版本管理系统的话比起ChangeLog更具价值。以CVS为例,表示特定行的最后变更的cvs annotate,显示与指定版本差异的cvs diff等都很方便。
再者就是,开发时用到邮件组或新闻组时很容易对对过去的Log进行检索。记载了很多变更理由。当然也可以用Web检索。
译者注:由于水平有限且时间仓促翻译错误在所难免,遇到读不懂之处敬请联系。
[@more@]来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/16163/viewspace-985321/,如需转载,请注明出处,否则将追究法律责任。
转载于:http://blog.itpub.net/16163/viewspace-985321/