接下来,有关ruby特点的介绍告一段落,终于要进入源代码的阅读了,不过,请稍等! 阅读源代码,是每个程序员必做的一件事。不过,对于“如何阅读源代码”,程序员们却没有很具体方法。为什么会这样?很简单,能写自然会读。 不过,我却也认为阅读别人的代码并不简单。同写代码一样,读代码肯定也有一些必需的技术或定式。因此阅读ruby之前,让我们整理一下思路,看看阅读代码有什么通用规则。 原则 先来谈谈原则。 确定目的 “阅读代码的最高境界是带着目的去读。” 这句话来自Ruby的作者Matz。的确是这样,这句话非常能够得到大家的肯定。 “是时候读读内核代码了”,于是打开一大堆代码,买来一大堆参考书,结果却是不知从何下手,相信有此经验的人不在少数。另一方面,“这个程序有bug,不尽快修复就赶不上交付日期……”的时候,即便是别人的程序也能在转瞬之间将问题搞定,这种事是不是也曾发生过? 这两种情况的差异就在于,人的主观意识不同。如果自己不知道自己要弄懂什么,肯定什么都不会弄懂。所以,首先搞清楚自己想要了解什么,明确目标是所有行动的第一步。 当然,仅仅这样不足以称之为“技术”。所谓技术,是指无论是谁只要有意识的去了解,就肯定能够了解的东西。接下来的讨论,就从“迈出第一步到最终达成目的”所用到的方法开始。 目的具体化 现在,我们最终的目标是“理解ruby的一切”。即便这勉强算确定了目标,但到底怎样实际的阅读代码还是没有明确。因为没有与具体的工作联系到一起。因此,必须先把这个暧昧的目标明确化。 具体该怎么做呢?首先,假想自己就是编写这个程序的人。这个时候,编写一个这样的程序所用到的知识自然就会运用到阅读程序上来。比如,阅读传统的结构化程序,自己也要用结构化的方法进行思考。即逐步分割目标。又如,类似于GUI的程序总要进入事件循环,首先从一个恰当的事件循环入手,调查活动处理器的作用。或者,先从MVC(Model View Controller)中的M着手调查。 再者就是有意识的选择分析方法。无论是谁都会有一套自己的分析方法,不过,自己的方法通常是建立在经验和直觉的基础上。怎样做才能更好的阅读源码呢?自己的思考和意识非常重要。 都有哪些分析方法呢?下面就来说明一下。 分析方法 阅读代码的手法大体上可以分为静态方法和动态方法两种。静态方法是指不运行程序对代码进行阅读和分析。动态方法是指运用调试器等工具观察程序的实际运行过程。 研究代码最好从动态分析入手。因为它就是“事实”。静态分析并不运行程序,所以或多或少带有“预测”的成分。如果希望了解真相,那么我们应该从事实起步。 当然,动态分析的结果是否真的是事实也未可知。也许调试器有错误,也许CPU过热死机,也许自己设错了条件…… 即便如此,动态分析还是比静态解析更接近事实。 动态分析 使用目标程序 缺少这一步便无法开始。这个程序是个什么样的东西,它可以完成哪些操作,这些都是我们应该预先知道的。 使用调试器追踪程序运行 比如,“实际的代码经过了哪里,采用了怎样的数据结构”之类的问题,与其在脑中思考,不如实际运行程序之后看结果来得更快。调试器会让这个工作变得简单。 如果能将程序运行时的数据结构画成图,那就太棒了。不过这样的工具实在难找(特别是自由的很少)。比较简单的数据结构快照可以用文本表示,也可以使用一个叫graphviz的工具,不过,如果以通用和实时为目标,那就比较难了。 Tracer 如果想了解代码经过了哪些过程,那就应该使用tracer。如果是C话,有一个叫ctrace的工具。此外,系统提供的tracer还有strace, truss, ktrace等工具。 到处打印 有一种说法叫“打印调试”。即便算不上什么调试技术,这种方法依然很有用。观察特定变量的变化时,与其一点点的用调试器追踪,不如嵌入打印语句,这样,只要把结果搜集起来就可以了。 改写后运行 对于程序不易理解的地方,可以稍微修改参数和代码,尝试运行。修改可以运行的话,就可以推测出代码的行为。不用说,应该预留原来的二进制文件,让二者去做同样的事情。 静态分析 名称的重要性 静态分析就是通过阅读代码进行分析,对源码的分析就是对名称的调查:文件名、函数名、变量名、类型名、参数名等等,程序本身就是一个名称的集合。名称是程序抽象化的最大武器,如果认识到这一点,那么阅读的效率就会有很大的不同。 另外,还要注意编码规则。比如,如果是C的函数名,为了区分函数种类,会给extern函数加上许多前缀。如果程序是面向对象的风格,函数的归属信息都会放在前缀中,使之成为重要的信息(比如:rb_str_length)。 阅读文档 也许会有解释内部构造的文档。特别要注意名为“HACKING”的文件。 阅读目录结构 通常目录都是根据某些策略进行划分的。了解程序可以分为哪些部分,把握各部分的概要。 阅读文件构成 结合文件中的函数(名),了解文件划分的策略。类似于程序注释,文件名是一种保质期很长的东西,应该得到特别的重视。 另外,如果文件中还有模块(module),构成这个模块的函数在文件中应该放到接近的地方。也就是说,根据函数的排列顺序就可以了解模块的组成。 调查缩略语 若有晦涩难懂的缩略语,应预先列出,提早调查。比如,“GC”究竟是Garbage Collection,还是Graphic Context,二者的含义相去甚远。 程序相关的缩略语大多是通过“取单词的首字母,省略元音字母”的方法形成的。特别要注意的是,目标程序的领域中一些已广为接受的缩略语应该预先弄清楚。 了解数据结构 如果数据和代码放在一起,应该首先从数据构造看起。也就是说,如果是C的话,从头文件入手是个不错的选择。这时,可以最大限度的发挥想象力,根据文件名进行推测。比如,在语言处理系统中有一个叫frame.h的头文件,它可能就定义了栈帧(stack frame)。 此外,结构体的类型和成员的名称也会给人许多启示。比如,如果结构体中有一个指向结构体自身的指针next,会联想到这可能是一个链表。同样,如果存在诸如parent/children/sibling等元素,该结构体十有八九是树(tree)。如果有prev,可能就是堆栈。 把握函数间的调用关系 函数之间的关系是仅次于名称的重要信息。有一种表现函数间调用关系的图,称为调用图(call graph),它的确很方便。 对于这个工具,基于文本的方式已经够用了,但如果能用图的话,那就更没得说了。只是这么方便的工具很少(自由的尤其少)。我为本书分析ruby时,用Ruby写了一个小命令语言解析器,然后,将结果传给那个叫做graphviz的工具,进行半自动生成。 阅读函数 阅读函数的动作,用一句话来概括它的行为。看着函数关系图来阅读函数的各个部分还是不错的。 阅读函数时候,重要的不是“读什么”,而是“不读什么”。可以说,削减了多少代码决定了阅读的难易程度。具体如何进行正确的削减,很难在这里演示,因此,这部分会放在正文中解释。 编码风格不符合自己的习惯时,可以用indent之类的工具进行转换。 按个人喜好改写代码 人体很奇妙,尽可能使用身体的各个部分去做的事情,很容易留下记忆。认为“草稿纸好于PC键盘”的大有人在,我想,如果不是单纯的怀古,与此还是有些关系的。 所以,仅仅对着屏幕阅读是无法记忆到身体中的,应该尝试一下边读边改,代码就会比较快的融入身体之中。 遇到不顺眼的名字和代码就要毫不犹豫的改写。将那些晦涩难懂的缩略语以它未省略的形式替换掉。 当然,改写时应该单独预留一个原有代码的备份。修改途中遇到问题,可以通过对比原有代码进行确认。为一个简单的错误而陷入几小时苦恼的困境中,得不偿失。改写是为了熟悉代码,改写本身不是我们的目的,希望不会投入太多热情。 阅读历史 程序一般都会有一个文档,记载着变更的历史。比如,GNU的软件必定有一个名为Changlog的文件,对于了解“程序演变的原因”很有帮助。 如果使用了诸如CVS和SCCS这样的版本管理系统,并可以直接进行访问的话,会比Changelog更具价值。以CVS为例,了解特定行最近的修改用cvs annotate,比较特定版本的差异用cvs diff,等等。 此外,最好能够直接从开发用的邮件列表和新闻组中检索过往的记录,其中常常记载了变更的理由。当然,如果能直接从Web上搜索就更好了。 用于静态分析的工具 不同的目的有不同的工具对应,不能一概而论。如果只让我选一个的话,我会推荐global。 这么选择是因为它很容易用于其它的用途。比如,其中包含的gctags原本是为了制作tag文件,不过,也可以用它取出“文件所包含的函数”的名称列表。 C代码 ~/src/ruby % gctags class .c | awk '{print $1}' SPECIAL_SINGLETON SPECIAL_SINGLETON clone_method include_class_new ins_methods_i ins_methods_priv_i ins_methods_prot_i method_list : : ~/src/ruby % gctags class.c | awk '{print $1}' SPECIAL_SINGLETON SPECIAL_SINGLETON clone_method include_class_new ins_methods_i ins_methods_priv_i ins_methods_prot_i method_list : : 虽说如此,这只不过是我的推荐,读者使用何种工具取决于个人喜好。不过,选择的工具至少应该具备以下功能: * 能够列出“文件所包含的函数”的名称 * 能够搜索函数名、变量的位置(能够直接跳转到那的更好) * 函数的交叉索引