本系列文章由
@yhl_leo
出品,转载请注明出处。
文章链接:
http://blog.csdn.net/yhl_leo/article/details/50545047
严肃是写作必备的两个因素之一。另一个,很不幸,是天分。 —— 欧内斯特 ⋅ 海明威
个人觉得可以把一个人编写程序所处的阶段可以分为四个:
- 识字:接触和掌握了一些基本的编程知识(变量,类型,函数,……)和语法(运算,循环,判断,……);
- 造句:可以根据基本的知识和语法模仿甚至自主实现一些小的算法等;
- 自由表达:熟悉掌握编程语言后,只要不太复杂的算法,似乎只要知道算法原理,就能实现;
- 妙笔生花:此时,你可能意识到代码实现不再是你所追求的目标,创建优秀的代码对你而言更具吸引力和挑战性。
创建优秀的代码意味着创建良好的文档化的代码。我们编写代码的原因是要表达一套清晰的指令——不仅仅是对电脑,也是对那些以后需要维护或拓展这些指令的可怜傻瓜们。所以,千万不要去膜拜把原本简单易懂的代码写得反人类的所谓大神!现实世界中的代码从来没有在编写完成后就被遗忘掉。在软件产品的生命周期内,这些代码将不断地被修改,拓展和维护。想要做到这一点,我们需要说明指导,即一个文档化的用户指南。
对于代码的文档化,人们通常的做法是编写大量的关于代码的文档或者在代码中添加大量的注释,这两种方式都是明智的。
实际上,这两种方法都是无稽之谈。大多数程序员对文字编辑器唯恐避之不及,对于编写太多注释也是相当头疼。编写代码是意见艰苦的工作,而将代码文档化更是艰苦异常。
对于支持文档的系统,常常面临以下挑战:
- 我们不需要做额外的工作。编写文档非常耗费时间,阅读文档也是这样。程序员们更愿意将时间花在编写程序上。
- 所有的独立文档都必须随着代码的更新而不断更新。在大型项目中,这将是一项可怕的工作。而不更新任何文档将会导致危险的错误和产生错误的信息。
- 大量的文档是很难管理的。在大量的文档中查找正确的文档,或者寻找在一个文档中可能多次出现的信息是不易的。就像代码一样,文档必须接受版本的控制,必须确保所阅读的文档的版本与所处理的代码的版本相对应。
- 分布在不同文档中的重要信息很容易被错过。
建议不要编写需要外部文档支持的代码。这样的代码是脆弱的。要确保你的代码本身读起来就很清晰。
对于另一种方案——使用详细的代码注释来使代码文档化——即使不是最糟的方法,也不是很好。一大堆毫无创造性的逐条注释,会妨碍好代码的产生。
避免这些,最好的方法应该就是编写自文档化的代码吧。
要知道唯一能完整并正确地描述代码的文档就是代码本身。这并不自然而然地意味着代码本身就是最佳的描述,但是在通常情况下,这是我们所能获得的唯一文档。
因此,你应该想尽办法使代码成为良好的文档,一种人人都可以读懂的文档。除了编写代码的作者外,必须还有更多的读者可以理解代码。编程语言是我们交流的媒介,清晰的交流至关重要。清晰的代码就会获得更高的质量,因为你犯错误的可能性会大大降低(错误变得显而易见),而且这样的代码维护成本也比较低。
自文档化的代码是可读性很强的代码。它本身就易于理解,而不需要依赖外部文档。我们可以通过很多方式提高代码的清晰度。其中一些技巧是非常基础的,而且在我们学习编程之初就被训练加以掌握,而其他一些技巧则是从经验中获得。
编写易于阅读的代码。人性化。简单易懂。编译器可以处理。
先看一段简单的函数例子:
int fval(int i)
{
int ret=2;
for (int n1=1, n2=1, i2=i-3; i2>=0; --i2)
{
n1=n2; n2=ret; ret=n2+n1;
}
return (i<2) ? 1 : ret;
}
这个示例非常接近现实。很多代码看上去就是这个样子的,虽然运行没有问题,结果也能输出准确,但是着实让战斗在前线的程序员们深受其害。再看一段自文档化的代码:
int fibonacci(int position)
{
if (position < 2)
{
return 1;
}
int previouseButOne = 1;
int previous = 1;
int answer = 2;
for (int n = 2; n < position; ++n)
{
previouseButOne = previous;
previous = answer;
answer = previous + previouseButOne;
}
return answer;
}
你或许只要读第一行代码,就可以知道这段程序在做什么了~它本身没有注释,却非常容易理解。一定要记住:注释只会增加人们的阅读量。不必要的注释,往往使代码变得毫无必要地烦人,而且将来会使函数的维护变得更加艰难。了解这一点非常重要,因为即便是最小、最优美的函数也需要日后进行维护。
传统的观念认为,编写自文档化的代码需要添加大量的注释。良好的注释编写当然是一项重要的技能,但是除此以外,还有很多重要的技能。事实上,我们应该通过编写不需要注释的清晰代码来主动避免注释。
优秀的代码许多特征都会重复出现,一种技巧的好处可以在代码质量的若干方面得到体现。下面罗列出一些编写自文档化代码的技术。
使用好的样式编写简单的代码
样式极大地影响着代码的清晰度。考虑周密的版面排布表达了代码的结构,它使得函数、循环和田间语句更加明确。
- 让“正常”的流程明显地贯穿你的代码。也就是说保持结构的一致性,例如,你的
if-then-else
结构的顺序应该前后一致(总是将“正常”情况放在“错误”情况之前,或者反过来)。 - 避免过多地嵌套语句。这些语句会导致复杂的代码,并且需要冗长的解释说明。人们通常认为每个函数都应该有且仅有一个出口,这杯成为“单入口单出口”(Single Entry, Single Exit, SESE)代码。但是,这要求对于可读性代码来说太严格了,并且可能会造成深层的嵌套。与下面这个SESE变体相比,我们更加喜欢前面看到的斐波拉契的例子:
int fibonacci(int position)
{
int answer = 1;
if (position >= 2)
{
int previousButOne = 1;
int previous = 1;
for (int n = 2; n < position; ++n)
{
previousButOne = previous;
previous = answer;
answer = previous + previousButOne;
}
}
return answer;
}
为了一条额外的return
语句,我宁愿避免哪个不必要的嵌套——它使得函数更难以读懂。函数逻辑深处的return
语句,其作用值得质疑,但顶端简短的循环却极大地提高了函数的可读性。
- 要谨慎地优化代码,防止它不再清晰地表达基础的算法。除非你已经非常明确某段代码是可接受的程序函数的一个瓶颈,否则不要对代码进行优化,即便优化了,也要清晰地注释这段代码发生了哪些变化。
选择有意义的名称
关于这一点,可以参考阅读:C/C++ 名正则言顺。
分解为原子函数
- 一个函数,一种操作。无需编写非常复杂的函数,在一个函数中,只需进行一种操作,选择一个毫无歧义的说明这种操作的名称,既有利于代码的利用效率,也便于移植和维护。
- 减少任何出人意料的副作用,不管他们看上去多么无害。
- 保持简短。短小的函数易于理解,如果能分解为带有描述性名称的小代码块,那么即便是一个复杂的算法你也可以轻易地弄明白。
选择有描述型的类型
尽可能使用现有语言的功能来描述约束或行为:
- 如果你要定义一个永远都不变得值,那么就强制将它定义为常量类型(C/C++中使用
const
); - 如果一个变量不应包含负值,那么就使用无符号类型;
- 使用枚举
emun
来描述一组相关的值; - 选择适当的类型。
命名常量
看到类似if(count == 76)
的代码时往往令人困惑,数字76
到底是什么意思?这句话的含义又是什么?这样的数字都隐藏了其含义,不利于阅读和理解,但是使用下面的方法:
const size_t bananas_per_cake = 76;
...
if (count == bananas_per_cake)
{
// make banana cake
}
就会清晰很多(也有很多人习惯使用宏定义的放法#define BANANAS_PER_CAKE 76
,但是高效C++的做法里,往往建议使用const
来取而代之)。
强调重要的代码
突出重要的代码,把读者的注意力吸引到正确的地方。编程时有很多机会可以做到这一点:
- 在类中按照一定的顺序进行声明。公共信息应该被放在首位,然后将私有的实现细节放在最后。
- 尽可能隐藏所有不重要的信息。不要以无用的垃圾信息塞满全局命名空间。
- 不要隐藏重要的代码。每行只写一条语句,并保持每条语句都简单。
- 限制嵌套的条件语句的数量。如果不加以限制,重要条件的处理就会被层层嵌套的
if
语句和括号隐藏起来。
提供头文件
在文件的顶部放置一个注释块,以描述文件的内容以及该文件所属的项目(甚至提供代码创建和编辑人的名单及其邮箱等)。这并不需要花费太多精力,但是却有明显地效果。当有人维护这个文件时,他们就能马上掌握关于该文件的信息。
这个头文件非常重要,大多数公司处于法律方面的考虑,要求在每个源文件都必须包含一个可见的版权声明。以cv.h
(OpenCV 2.4.10版本的)为例:
/*M/// // // IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. // // By downloading, copying, installing or using the software you agree to this license. // If you do not agree to this license, do not download, install, // copy or use the software. // // // License Agreement // For Open Source Computer Vision Library // // Copyright (C) 2000-2008, Intel Corporation, all rights reserved. // Copyright (C) 2009, Willow Garage Inc., all rights reserved. // Third party copyrights are property of their respective owners. // // Redistribution and use in source and binary forms, with or without modification, // are permitted provided that the following conditions are met: // // * Redistribution's of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // // * Redistribution's in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // // * The name of the copyright holders may not be used to endorse or promote products // derived from this software without specific prior written permission. // // This software is provided by the copyright holders and contributors "as is" and // any express or implied warranties, including, but not limited to, the implied // warranties of merchantability and fitness for a particular purpose are disclaimed. // In no event shall the Intel Corporation or contributors be liable for any direct, // indirect, incidental, special, exemplary, or consequential damages // (including, but not limited to, procurement of substitute goods or services; // loss of use, data, or profits; or business interruption) however caused // and on any theory of liability, whether in contract, strict liability, // or tort (including negligence or otherwise) arising in any way out of // the use of this software, even if advised of the possibility of such damage. // //M*/
或许你写不了这么清晰,详尽的头文件,但是也应该提供一些最基本的描述:
/********************************************************* * File: Foo.h * Purpose: Foo class implementation * Notice: (c) 1066 Foo industries. All rights reserved. *********************************************************/
恰当地处理错误
在最恰当的上下文中处理错误。如果磁盘I/O有问题,你应该在访问磁盘的代码中处理这个问题。处理这个错误可能会引起另一个更高层的错误(如无法加载文件异常)。这意味着在程序的每一层上,一个错误就是此上下文中的问题是什么的一个精确描述。自文档化代码能帮助读者了解错误的起因,错误意味着什么,以及对程序在这一点的暗示。
编写有意义的注释
前面已经讲述了如何尝试使用隐代码文档化技术来避免编写注释。不过,在你编写出所能编写的最清晰的代码之后,其余信息仍然需要使用注释来说明。
只有在你无法以任何其他方式来提高代码清晰度的情况下,再添加注释。
最后,总结一下。关于写作技巧的提高,有一个简单的原则:读的书越多,写得就越好。用批判的眼光阅读著名作者的作品,可以是你学会如何分辨好坏。你会从中学到新的技巧和习惯用法,为你的知识库添砖加瓦。类似地,如果你阅读大量的代码,那么你将会成为更出色的程序员。如果你将自己沉浸在优秀的代码中,你很快就会具备在千里之外就能闻到糟糕代码的能力。有了这些经验,你自然而然地会发现自己在编写代码时能运用好的技巧,当你编写出糟糕代码时,你会马上有所警觉,让你浑身不自在。