微软资深软件工程师:阅读代码真的很难

编者按:原文作者 Eric Lippert 是微软一名资深软件设计工程师,从1996年起一直在微软开发部门任职,协助设计并实现VBScript、JScript、JScript .NET、Windows Script Host、Visual Studio Tools for Office 和 C#。

Escalation的工程师JeremyK在他博客中问到:

你是怎么教人们快速深入挖掘不熟悉的代码(不是自己所写的)?我学习如何编程的方法很传统 —— 自己动手编码。但我现在很纠结:到底是集中精神阅读源码,还是自己编写。对我而言,似乎唯一有效的方法就是自己写过。

不是和Jeremy开玩笑,写代码的确没有读代码难。

首先,我同意你的看法,几乎很少有人能读代码但不会写代码。这不像自然书面语或口语,理解他人的意思并不需要去理解他们为什么要那样说。比如,如果我说:

“写代码有两种方式:一种严格且详细,另一种模糊且草率。前者生成简洁分层的婚礼蛋糕,后者却是意大利面条。”

上面这句话产生一个平衡且幽默的效果,但即使听众和读者不理解我使用“零照应”和“并列句”这样的文字技巧,也会理解我要说的意思。但是说到代码,既要从代码本身中理解代码作者的意图,又要理解代码产生的预计效果,这两者都极为重要。

微软资深软件工程师:阅读代码真的很难_第1张图片
Eric Lippert

因此,我又回到那个问题了,有些人需要快速切入代码,但不需要动手写代码,那我们如何编写适合这些人的代码?

下面是我在编写代码时,尽力去做的事,目的就是使其他人能轻松阅读:

1. 使代码遵从工具。Object Browsers和Intellisense虽然很好,但我告诉你,我是守旧派。如果找不到我想要的,我会不高兴。什么使得代码成为可查询的呢?

  • 像”i”这样的变量名不好。如果没有明确的错误提示,你就无法轻易查找代码。
  • 避免使用是其他名字前缀的名字。比如,在代码中有个“perfExecuteManifest”,如果再有一个“perfExecuteManifestInitialize”,这就会让我抓狂,因为每次在源码中查询前者时,我不得不费力地过滤掉后者所有的实例。
  • “临时传递数据”(tramp data)应使用相同的名字。所谓“临时传递数据”(tramp data),就是指那些传递给方法A的变量,还要传给方法B的变量。这两类变量实际上是相同的,所以如果它们有着相同的名字,则更好。
  • 别用宏来重命名东西。如果有个方法叫get_MousePosition,那别这样GETTER(MousePosition)来声明该方法。因为我会找不到实际的方法名。
  • Shadowing会引起很多问题,请不要用它。

2. 坚持使用一种命名模式。如果你打算用匈牙利命名法,那就坚持并广泛使用,否则将适得其反。使用匈牙利命名法来记录数据,而不是存储类型;记录普遍事实,而不是临时条件。

3. 使用断言来记录先决条件(preconditions)和后置条件(postconditions)。

4. 别缩写英文单词。确切来说,别缩写成稀奇古怪的形式。在脚本引擎中,有个变量名叫“NME”,这让我非常抓狂!它应当叫“VariableName”。

5. C语言标准运行时库的设计不是很优秀。别去效仿它。

6. 别写“聪明”的代码;当代码出现问题,维护代码的程序员没时间去理解你的聪慧。(编注:这条建议即为:编写可维护的代码,详情可参见《明星软件工程师的10种特质》中的第8点。)

7. 理解编程语言特性的设计初衷,使用这些特性去做它们适合完成的工作,而不是它们能做到的工作。例如:别把异常当做一般的流控制机制来使用(即便你能做到),而应该用它们来报告错误。别强制把接口指针转换成类指针,即便你知道这样没问题。

8. 按功能单元划分源码树,而不是按组织结构。比如:我目前所在团队中,有2个根子目录的名字是“Frameworks”和“Integration”,这是两个团队的名字。不巧的是,Frameworks团队名下有一个叫“Adaptor”的子目录,而“Adaptor”却是Integration的子目录,这就非常令人迷惑。同理,(如果)有着相同子目录的不同的子树,有些是客户端的组件,有些是服务端的组件;有些是管理组件,有些是非管理组件;有些是处理型组件,有些是非处理型组件;有些是零售组件,有些内部测试工具。这就会乱七八糟的。

当然,我实际上根本没有回答Jeremy的问题——如何调试不是我写的代码?

这取决于我的目的。如果我只是因为一个bug,而深挖一段具体的代码,我会在调试器中逐步跟踪所有代码,写下我“走过”的调用分支,记录下哪些方法是特定数据结构的“生产者”,哪些方法是“消费者”;我也会仔细盯着输出窗口,查看出现的有用信息;还要打开异常捕捉器,因为异常通常是问题所在。设置断点;我会记录所有和我上面建议相反的地方,因为这些东西很可能误导了我。

如果我想在理解一段代码后修改它,我通常是从代码头部开始,或者先查找公共方法。我要知道类是如何实现的,它是如何扩展的,它的作用,它是如何嵌入整个代码中的?我会尽力理解这些东西后,才去了解这些特定部分(代码)是如何实现的。这耗时虽更长些,但如果你准备改动复杂代码,你应当那样做。

昨天,我对于这个问题思考了好一会,还跟Larry Osterman进行了讨论,终于想出了一些也许会在阅读和调试别人的代码时有用的建议。

首先,你正在调试的机器代码,很有可能会与源代码不匹配,或者只是因为你得到的源代码不是最新的,或者是因为代码被过度优化以至于源代码和二进制文件之间的关系变得不够明显——像瑞士乳酪那样松散,又或者是因为你遇到符号表错误等等。我当然不是个汇编程序员,但我了解必要的汇编知识和调用规则,这样我可以试着调试那些源代码和二进制之间不是那么同步的代码。即使知道“‘this”指针很有可能在ECX寄存器中”可以对正在进行的调试会话大有帮助。

第二,你还记得那部卡通片Calvin and Hobbes中Calvin偷偷溜到Hobbes的身后吓他,然后学到了吓一只正在睡觉的老虎并不明智吗?Calvin喃喃自语道:“我得开始聆听那没有声音却不停冒出的疑问才行。”

那部卡通片改变了我的生活。我意识到自己常常会犯一个错误,在回顾之前我应该能够提前意识到,但不知因为什么,我却不顾没有声音却不停冒出的疑问而执意地冒失地继续下去。我决定绝不让自己的墓碑上写着“此人死于愚蠢的但本可以完全避免的事情上。”

相信你的头脑。是的,预感有可能是错的,但当它真的是错误的时候,发现它是错的并不需要很大的代价。而如果它是对的,你可以省很多时间。当我在调试我不知道的代码时,我试着去聆听那些没有声音却不停冒出的疑问。在内存显示窗口有一个字节瞬间变成了红色,表明它已经改变了。嗯。在这次函数调用中我是否期望内存发生改变?其自身拥有的内存是改变了,还是只是堆栈错误?嗯,这个局部变量突然变值—这是不是个符号错误?这个变量是否是shadow变量而我则刚刚离开了内部作用域?顺着这类的线索追踪下去。

译者注:shadow变量:当一个变量所在的作用域之外还有一个同名的变量,称为shadow变量;shadow变量只在自己的作用域有效,详细解释和示例参见variable shadowing.

最近我凭直觉在Excel中找到了一个极不起眼的漏洞—我有发现有一个数位的堆分配的缓冲器通过了一个函数但其长度没有通过。嗯。可疑!果真,只要你追踪所有的逻辑,你就会发现更深十级的函数对于缓冲范围作了错误的假设并且造成了一个错误。幸好这个假设十分保守,受到了许多限制,所以没有造成缓冲溢出的后果。还有,幸好这个漏洞极不起眼—一般用户都不会马上就陷入其中。

那么我的第三个建议又会是什么呢?昨天我说过,你需要知道代码是干什么的和开发者为什么要让它干这些。但你还需要知道更多信息—你还要知道代码是怎样随着时间的流逝而在发生改变的。显然在这个漏洞中,程序一开始占用一小块固定大小的缓冲。有人修改了缓冲区分配代码以分配可变范围的缓冲区,但却忘记了这代码所表示的程序是写来假定一个固定的小范围缓冲的。漏洞的发生通常由于在其它合理的代码重整中发生的极小的错误。明显地,没有人会把这样的漏洞写进从一开始就已经有了可变范围的缓冲区代码的代码。当你在阅读一片段的代码时,试着了解不变量将会是什么,然后想想那些情况会违反常量的假定。

当然,要想知道那些没有声音却不停冒出的疑问来自哪里是很困难的,而且可能教也教不来。不管你怎样试图克服它,它都会很困难。

本文来自: http://blog.jobbole.com/438/  
英文原文: http://blogs.msdn.com/b/ericlippert/archive/2004/06/14/reading-code-is-hard.aspx

你可能感兴趣的:(微软资深软件工程师:阅读代码真的很难)