The Art of Readable Code
作为程序员,日常工作的大部分时间都是花在一些“基本”的事情上,像是给变量、函数或类命名,写循环以及在函数级别解决问题。并且这其中很大的一部分就是阅读和编辑(修改)已有的代码。因此,代码是否易于理解就显得尤为重要。《编写可读代码的艺术》(The Art of Readable Code)这本书从命名,排版,注释,循环以及如何拆分长表达式等方面阐述了编写易于理解代码的技巧。这本书除了教会你这些技巧之外,更重要的是它让你对“代码的可读性“另眼相看。也许你从来都未曾意识到“代码的可读性”会如此的重要!
第一章、代码应当易于理解当我们面对两段相同功能而编写方式各异的代码时,如果判断那种方式更合适?或者说那种编写方式更易于理解?那就是第二个关键思想,即可读性基本定理:“代码的写法应当使别人理解它所需要的时间最小化。” 当然,也许不同的人,理解不同的代码所需要的时间也不同。因此对这个观点或许是一个仁者见仁智者见智的问题。但是,我们依然需要通过“大多数”这个方向来判断代码应该如何编写。
2、总是越小越好吗?
代码要精炼简洁。不错!但有一个前提就是,不能影响可读性。那么,可读性又如何判断?答案在上面已经给出:时间最小定理!// Fast version of "hash = (65599 * hash) + c" hash = (hash << 6) + (hash << 16) - hash + c;
一条注释虽然增加了代码的长度,但是它可以让读者更快的理解代码。如果没有代码上面那条注释的话,估计这条代码足够你研究好一阵子。
3、理解代码所需要的时间是否与其它目标有冲突?
不会!经验表明,提高代码可读性的同时往往会把代码引向好的架构其也更容易测试。而代码的可测试性是高质量代码的另一个重要属性。真可谓意外得到的收获!
4、编写可读的代码——难在哪里?
要想编写出高可读性的代码,就需要我们经常的问一问自己:其他人阅读我的代码会遇到困难吗?这需要我们在编码时花费额外的时间,更需要我们打开大脑中从前在编码时可能没有打开的那部分功能。但如果你接受了这个目标,那么可以肯定,你将成为一个更好的程序员。你的代码缺陷会更少,周围的人也爱用,你将因它而自豪。好了,让我们开始吧!
第一部分 表面层次的改进
不要忽视了不重要的表面,“表面”的东西能让你的代码更“体面”。这一部分的内容十分通俗易懂,你简直可以不费吹灰之力就广泛的应用起来,值得我们认真学习并立即实践。你将发现:这些知识和技巧会影响你代码库中的每一行代码。
第二章 把信息装到名字里
无论是变量、函数还是类(包),它们的名字都是一个小小的注释。因此,选择一个好的名字不是无所谓的事情,而是一件非常重要的任务。俗话说,万事开头难!长期来看,你会从这个好的开始受益良多。
1、具体的说,有哪些技巧可以提高命名的贴切度或者说正确性?
关于这个问题,作者给出了以下几条提示:选择专业的词:有经验的程序员都知道,命名并非一件容易的事,尤其是取一个见名知义的名字。遇到困难时,我们要勇于寻求帮助,从字典、同事、朋友或者是专业领域人士那儿获得帮助。
找到更有表现力的词:英语是一门丰富的语言,有很多词(近义词)可以选择。作为非英语母语过度的程序员来说,英文水平也是影响我们命名水平的障碍之一。对于程序员来说,英语和你的编程语言一样重要。学习吧!英语能让你走得更远。
避免像tmp和retVal这样泛泛的名字:用具体的名字代替抽象的名字:
1) ServerCanStart() --> CanListenOnPort();
2) DISALLOW_EVIL_CONSTRUCTORS --> DISALLOW_COPY_AND_ASSIGN
为名字附带更多信息:对于一些需要特别说明的东西,我喜欢在变量名的后面用下划线附带一些更多的信息。例如单位等等。形如”变量名_附加信息;“有目的的使用大小写和下划线等(即利用名字的格式来传递含义)这里有一些示例:delay_s; fileSize_mb; max_kbps; degrees_cw; password_plaintext; comment_unescaped; html_utf8; data_urlencode;
3)丢掉没用的词。例如convertToString(), ToString()一样易于理解,且更精炼简洁。
第三章 不会误解的名字
对于这个主题的一个关键思想是:要多问自己几遍:“这个名字会被别人解读成其他的含义吗?”要自己审视这个名字。length,limit 都存在多义性。
2、推荐用min和max来表示(包含)极限:[min, max] 闭区间。
3、推荐用first和last来表示包含的范围: [first, last] 闭区间 或 [first, last) 半闭半开区间。
4、推荐用begin和end来表示包含/排除范围: [begin, end) 半闭半开区间。
5、给布尔值命名
这个一个非常好的主题。通常来讲,加上is, has, can, should, test这样的词,可以把布尔值变得更明确。但有的观点建议,布尔型变量名不加这些前缀词,而返回布尔值的函数名则应该加上这些前缀词。决定权在你手里。另外,作者建议最好避免使用反义名字。
6、名字应该与使用者的期望相匹配
get***(): 使用者通常将其看成一个“轻量级访问器”。关于这一点,有一个原则比较重要,即“如果有一个人用错了你的接口,那么肯定会有更多的人用错这个接口”。
7、如何权衡多个备选名字?
要吹毛求疵一点,多想一想这个名字是否会被别人误解为别的名字?还有更贴切更好的名字吗?
第四章 审美
好的源代码应当“看上去养眼”。本章告诉我们如何使用好的留白、换行、对齐和顺序来让代码变得更易读。作者提出了三个原则:1、保持代码的美观重要吗?
要说服程序员写代码像写文书报告一样,注意排版的美观和代码的整洁,在当下可能还有一些难度。这可能是因为代码主要是有机器解析编译执行,也从来都不需要打印,所以大家都不太重视美观度,觉得花这些必要的时间是一种浪费。时下大部分人都停留在”代码只要能跑起来就OK“的状态。不过没有关系,真金不怕火炼。写代码像写文书报告一样讲究排版的日子不久就会到来。
2、以下是作者给出的一些建议:
1)、重新安排换行来保持一致和紧凑。
2)、使用函数整理不规则的东西7)、个人风格与一致性。在某些时候“一致的风格比正确的风格更重要”。
第5章 该写什么样的注释
不知道从什么时候开始,我开始变得不喜欢写注释了。对于写注释,我经历了“喜欢写注释到不喜欢写注释”这样一个过程。受一些程序设计教程的影响,我刚开始编码时,代码中充斥着注释,由于有了注释,我认为代码已经变得足以易懂,因此放松了对代码本身可读性的要求。渐渐的,我明白了“好代码>坏代码+好注释”的道理,于是我开始关注代码本书的可读性,但随之感觉注释是多余的。特别是看到注释和代码本身不一致时,我更是深恶痛觉。很幸运,我读到了《编写可读代码的艺术》这本书,她让我在这两个极端中找到了方向。本书作者在第5章和第6章阐述了该写什么样的注释和怎么写好的注释。
关于注释,这里有一个关键思想:“注释的目的是尽量帮助读者了解得和作者一样多”。
1、什么不需要注释?
1)不要为那些从代码本身就能快速推断的事实写注释。
2)不要为了注释而注释。
3)不要给不好的名字加注释——而应该把名字改好。
2、那么,什么需要注释?
1)记录作者编码时的思想(加入“导演评论”)。
2)为代码中的瑕疵写注释。
标记 | 通常的意义 |
TODO | 我还没有完成的事情 |
FIXME | 已知的无法运行的代码 |
HACK | 对一个问题不得不采用的一个粗糙的解决方案 |
XXX | 危险!这里有重要的问题 |
// users thought 0.72 gave the best size/quality tradeoff const double image_quality = 0.72;
4)意料之中的提问(或者一些生僻的用法),例如:
struct Recorder { std::vector<float> m_data; // .... void clear() { // Force vector to relinquish its memory (look up "STL swap trick") std::vector<float>().swap(m_data); } };
5)公布可能的陷阱
6)另外,“全局观”,“总结性”注释对代码的阅读者理解整个代码的关联和逻辑是很有帮助的。
因此,关于注释,这里有一个通用的技术是“想象你的代码对于外人来讲看起来是什么样子的”。
最后,送给自己一句话:你需要注释,当然也许不是每个地方都需要注释。努力把代码写好的同时通过注释给代码注入更多的信息。
第6章 写出言简意赅的注释
写好的注释绝不比写好的代码容易。写出好的注释很难,但我们仍然需要尽量做到。就像作者在上一章末提到的一样“克服作者心里阻滞”。只要坚持不懈,精益求精,总有一天我们能写出言简意赅的注释。在这一章,作者就如何写出“言简意赅”的注释给出了他的建议:如让注释保持紧凑,避免使用不明确的代词,精确的描述函数的行为。特别的提到了“用输入/输出的例子来说明特别的情况”,要声明代码的高层次意图而非具体的细节。另外,“具名函数参数”的注释是一个非常实用的实践。
下面是一些例子:
1、让注释保持紧凑
// CategoryType -> (score, weight) typedef map<int, pair<float, float> > ScoreMap;
或者也有这样做的:
typedef map<int/*CategoryType*/, pair<float/*score*/ , float/*weight*/> > ScoreMap;
2、精确的描述函数的行为
// Count how many newline bytes ('\n') are in the file. int CountLines(const std::string &fileName) const { /*...*/ }
3、用输入/输出的例子来说明特别的情况
// Example: Partition([8 5 9 8 2], 8) might result in [5 2 | 8 9 8] and return 1. int Partition(std::vector<int>* v, int pivot);
4、声明代码的意图
for (std::list<Product>::reverse_iterator it=products.rbegin(); ; ) { }
5、用嵌入的注释“具名函数参数的意义”
Connect(/*timeout_ms=*/ 25, /*use_encryption=*/ false);