《编写可读代码的艺术》读书笔记(一)

      The Art of Readable Code

      作为程序员,日常工作的大部分时间都是花在一些“基本”的事情上,像是给变量、函数或类命名,写循环以及在函数级别解决问题。并且这其中很大的一部分就是阅读和编辑(修改)已有的代码。因此,代码是否易于理解就显得尤为重要。《编写可读代码的艺术》(The Art of Readable Code)这本书从命名,排版,注释,循环以及如何拆分长表达式等方面阐述了编写易于理解代码的技巧。这本书除了教会你这些技巧之外,更重要的是它让你对“代码的可读性“另眼相看。也许你从来都未曾意识到“代码的可读性”会如此的重要!

第一章、代码应当易于理解
1、是什么让代码变得“更好”?

      当我们面对两段相同功能而编写方式各异的代码时,如果判断那种方式更合适?或者说那种编写方式更易于理解?那就是第二个关键思想,即可读性基本定理:“代码的写法应当使别人理解它所需要的时间最小化。” 当然,也许不同的人,理解不同的代码所需要的时间也不同。因此对这个观点或许是一个仁者见仁智者见智的问题。但是,我们依然需要通过“大多数”这个方向来判断代码应该如何编写。

2、总是越小越好吗?

      代码要精炼简洁。不错!但有一个前提就是,不能影响可读性。那么,可读性又如何判断?答案在上面已经给出:时间最小定理!
例如,
// Fast version of "hash = (65599 * hash) + c"
hash = (hash << 6) + (hash << 16) - hash + c;

一条注释虽然增加了代码的长度,但是它可以让读者更快的理解代码。如果没有代码上面那条注释的话,估计这条代码足够你研究好一阵子。

3、理解代码所需要的时间是否与其它目标有冲突?

       不会!经验表明,提高代码可读性的同时往往会把代码引向好的架构其也更容易测试。而代码的可测试性是高质量代码的另一个重要属性。真可谓意外得到的收获!

4、编写可读的代码——难在哪里?

       要想编写出高可读性的代码,就需要我们经常的问一问自己:其他人阅读我的代码会遇到困难吗?这需要我们在编码时花费额外的时间,更需要我们打开大脑中从前在编码时可能没有打开的那部分功能。但如果你接受了这个目标,那么可以肯定,你将成为一个更好的程序员。你的代码缺陷会更少,周围的人也爱用,你将因它而自豪。好了,让我们开始吧!

第一部分 表面层次的改进

       不要忽视了不重要的表面,“表面”的东西能让你的代码更“体面”。这一部分的内容十分通俗易懂,你简直可以不费吹灰之力就广泛的应用起来,值得我们认真学习并立即实践。你将发现:这些知识和技巧会影响你代码库中的每一行代码。

第二章 把信息装到名字里

       无论是变量、函数还是类(包),它们的名字都是一个小小的注释。因此,选择一个好的名字不是无所谓的事情,而是一件非常重要的任务。俗话说,万事开头难!长期来看,你会从这个好的开始受益良多。

1、具体的说,有哪些技巧可以提高命名的贴切度或者说正确性?

      关于这个问题,作者给出了以下几条提示:

选择专业的词:有经验的程序员都知道,命名并非一件容易的事,尤其是取一个见名知义的名字。遇到困难时,我们要勇于寻求帮助,从字典、同事、朋友或者是专业领域人士那儿获得帮助。

找到更有表现力的词:英语是一门丰富的语言,有很多词(近义词)可以选择。作为非英语母语过度的程序员来说,英文水平也是影响我们命名水平的障碍之一。对于程序员来说,英语和你的编程语言一样重要。学习吧!英语能让你走得更远。

避免像tmp和retVal这样泛泛的名字:
当然,对于空泛的名字,这里也有一些需要我们根据实际取舍的情况,例如:
1)某些情况下,像交换两个变量的值的函数中,tmp 是合适的。
2)在小的函数内部,作为返回值的局部变量retVal也是可以接受的。
3)作为循环变量的i、j, k以及循环迭代器的iter, it, 这些名字虽然和空泛,但已得到大家的认可:它是一个循环变量或迭代器。因此,切记不要把这些名字表示别的含义。
4)当有多层循环时,我们通常会需要更贴切具体的名字。例如,ri/i_row, ci/i_col等。
5)在使用这些空泛的名字时,我们最好问一下自己:“还有别的更好的选择吗?”“我这样用的理由是什么?”。

用具体的名字代替抽象的名字:

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;

2、名字应该有多长?

       关于这个问题,有一个基本的准则:“用最短的名字表达最多的信息”。除此之外,作者还给出了一些指导原则:
1)在小的作用域里可以使用短的名字。但类的成员名字不能过于短小,尤其是全局变量或全局函数的名称一定要足够表达它们的意图。
2)首字母缩略词和缩写通常不会是一个好主意——当然,行业通常的缩略词除外。例如,cur, mgr等名字是可以接受的。

3)丢掉没用的词。例如convertToString(), ToString()一样易于理解,且更精炼简洁。

第三章 不会误解的名字

        对于这个主题的一个关键思想是:要多问自己几遍:“这个名字会被别人解读成其他的含义吗?”要自己审视这个名字。 
1、两个例子
filter();
clip();

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***(): 使用者通常将其看成一个“轻量级访问器”。
  getMean() --> computeMean();
  size() --> countSize() 或者 countElements();

关于这一点,有一个原则比较重要,即“如果有一个人用错了你的接口,那么肯定会有更多的人用错这个接口”。

7、如何权衡多个备选名字?

要吹毛求疵一点,多想一想这个名字是否会被别人误解为别的名字?还有更贴切更好的名字吗?

第四章 审美

       好的源代码应当“看上去养眼”。本章告诉我们如何使用好的留白、换行、对齐和顺序来让代码变得更易读。作者提出了三个原则:
  • 使用一致的布局,让代码阅读者很快就习惯这种风格
  • 让相似的代码看上去相似
  • 把相关的代码行分割,形成代码块

1、保持代码的美观重要吗?

要说服程序员写代码像写文书报告一样,注意排版的美观和代码的整洁,在当下可能还有一些难度。这可能是因为代码主要是有机器解析编译执行,也从来都不需要打印,所以大家都不太重视美观度,觉得花这些必要的时间是一种浪费。时下大部分人都停留在”代码只要能跑起来就OK“的状态。不过没有关系,真金不怕火炼。写代码像写文书报告一样讲究排版的日子不久就会到来。

2、以下是作者给出的一些建议:

1)、重新安排换行来保持一致和紧凑。

2)、使用函数整理不规则的东西
3)、在需要的时候使用列对齐
4)、选择一个有意义的顺序,并始终一致的使用它
5)、把声明按块组织起来
6)、把代码分成段落。

7)、个人风格与一致性。在某些时候“一致的风格比正确的风格更重要”。

第5章 该写什么样的注释

      不知道从什么时候开始,我开始变得不喜欢写注释了。对于写注释,我经历了“喜欢写注释到不喜欢写注释”这样一个过程。受一些程序设计教程的影响,我刚开始编码时,代码中充斥着注释,由于有了注释,我认为代码已经变得足以易懂,因此放松了对代码本身可读性的要求。渐渐的,我明白了“好代码>坏代码+好注释”的道理,于是我开始关注代码本书的可读性,但随之感觉注释是多余的。特别是看到注释和代码本身不一致时,我更是深恶痛觉。很幸运,我读到了《编写可读代码的艺术》这本书,她让我在这两个极端中找到了方向。本书作者在第5章和第6章阐述了该写什么样的注释和怎么写好的注释。

关于注释,这里有一个关键思想:“注释的目的是尽量帮助读者了解得和作者一样多”。

1、什么不需要注释?

1)不要为那些从代码本身就能快速推断的事实写注释。

2)不要为了注释而注释。

3)不要给不好的名字加注释——而应该把名字改好。

2、那么,什么需要注释?

1)记录作者编码时的思想(加入“导演评论”)。

2)为代码中的瑕疵写注释。

标记 通常的意义
TODO 我还没有完成的事情
FIXME 已知的无法运行的代码
HACK 对一个问题不得不采用的一个粗糙的解决方案
XXX 危险!这里有重要的问题
3)给常量添加注释:有些时候,常量不需要注释,常量的名字足够表达它本身的意图。但是很多时候,常量的背后通常有一个只有代码编写者知晓的“故事”。
    // 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);

 

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页