这是我读 “the art of readable code” 一书做的笔记

简化循环和逻辑

making control flow easy to read

下面的代码:

if (length >= 10)

要比:

if (10 <= length)

更容易看懂。这是很显然的。而在C语言中,有的人为了避免=与==的错误,常常把 代码写成:

if (10 == length)

这种做法是为了避免错误的。其实在我看来,这明显是在掩饰自己区分不了=与==的 弱点,如果你真的理解赋值与相等的含义,就从来不会在比较相等的时候写成=,以 我使用C语言这么多年来,我从来没有犯过这种错误。上面的代码对于一个正常人来 说,很难理解,因为程序员首先是一个人,自然的语言是“长度等于10”,而不是 “10等于长度”,所以为了使代码更可读,我建议使用下面这种:

if (lenght == 10)

上面的是有数字,有常量的比较,下面这个:

while (bytes_received < bytes_expected)

要比:

while (bytes_expected > bytes_received)

更容易看懂,因为第一个的阅读顺序符合人类的自然语言。

  • 对于三目运算符?:,当表达式很长时不要使用。
  • 不要使用do/while。当你阅读do/while的时候,因为你刚开始不知道条件,你会 把循环的主体阅读两次。我几乎不写do/while,而且我非常讨厌看do/while代码。
  • 尽量不要使用嵌套的if/else,想尽办法使得只有一个层次。

breaking down giant expressions

利用宏来简化代码。看下面的例子:

void AddStats(const Stats &add_from, Stats *add_to)
{
    add_to->set_total_memory(add_from.total_memory() + 
        add_to->total_memory());
    add_to->set_free_memory(add_from.free_memory() + 
        add_to->free_memory());
    add_to->set_swap_memory(add_from.swap_memory() + 
        add_to->swap_memory());
}

不管是谁,看到这样的代码都会头晕,但是你会发现其实它们都在做同一件事:

add_to->set_XXX(add_from.XXX + add_to->XXX);

于是,通过定义带参数的宏,可以简化成这样:

void AddStats(const Stats &add_from, Stats *add_to)
{
    #define ADD_FIELD(field) \
        add_to->set_#field(add_from.#field() + add_to->#field())
    ADD_FIELD(total_memory);
    ADD_FIELD(free_memory);
    ADD_FIELD(swap_memory);
}

这样不仅视觉上看起来舒服,而且理解起来非常容易。

variables and readability

  • 可去除一些多余的变量

  • 尽量缩小变量的范围,即使是全局变量,这样才能让程序更清晰。

  • 在C++中,有这样一个例子:

    int size = list.size();
    if (size > 0) {
        cout << size << endl;
    }
    

假设后面再也没有用到size了,但是阅读代码的人会一直把这个变量记住,因为 他以为后面还会用到这个变量。我们可以把它改成这样:

if ((int size = list.size()) > 0) {
    cout << size << endl;
}

当然,在C语言中,这需要C99的支持,当读者看完这段代码时,就会忘记这个变量, 因为后面已经用不上了。 作者是这样说的,但是我觉得这不太可能,因为写代码的人不可能预知未来,它总是 喜欢先把变量缓存起来,说不定以后还会用到,所以我不太赞成这种写法,除非是非常 简单,很明显后面用不上的变量。

  • 尽量使变量只能改变一次(prefer write-once varaibles)

改善外层代码

packing informations into names

choose specific words

书上说到getPage(url);这个函数的名字,根据我以前的看法,这绝对是一个好名字, 但是作者却批评这种命名方式。作者说,get太模糊了,我们看不出来它是从缓存中get还是 从数据库中get,还是从互联网上get。如果是从互联网上get,则应该使用fetchPage或者 downloadPage()。我觉得这个说法非常好,我以后给变量或者函数起名字的时候也要注意 这方面的东西了。

avoid generic names like tmp and retval

作者批评了tmp,retval,foo这种词语,虽然我没用过retval和foo这种奇怪的名字,但是 我却用过tmp这东西,后来想了一下,tmp这种东西的确看不出来任何含义,就算时间紧迫,我 也不会用tmp这种变量了。但是,就像作者所说的一样,对于下面的代码:

void swap(int *a,int *b)
{
    int tmp = *a;
    *a = b;
    *b = tmp;
}

这时候tmp则用得恰到好处,tmp在这里生命周期非常短,而且它的作用刚好是作为临时来用的。

对于计数器变量,我们经常使用i,j,k,x,y,z,a,b,c什么的,但是,正如作者所说,当有 多个计数器变量时,这种东西就经常会让人很难看懂了。看下面的例子:

for(int i = 0; i < N; i++){
    for(int j = 0; j < M; j++){
        for(int k = 0; k < C; k++){
            if(school[i].teacher[k] == user[j]){
                ...
            }
        }
    }
}

school,teacher,user这种变量命名是很好的,但是i,j,k这种东西就很难懂了。我们怎么 知道i,j,k分别对应哪个数组的下标呢?我以前也经常写这种代码,也经常看到其他人写这种代码, 每当我看到这种代码时(就算是我自己的代码),我都觉得非常头疼,现在看了这本书,马上 醒悟过来,以后再也不写这种代码了!转而使用si,ti,ui,这样 school[si].teacher[ti] == user[ui] 就非常清楚了。

prefer concrete names over abstract names

意思是不要抽象的名字,而是要具体的名字。以书上的一个例子说, serverCanStart是抽象的名字,而canListenOnPort则是一个具体的名字

attaching extra information to name,by using a suffix or prefix

如果我们有一个变量:

string id;

而这个id必须是十六进制,这时,直接使用id就很不好,因此你看不出来它必须使用十六进制, 使用hex_id代替就很明确了。

单位数值, 使用书上的例子,看下面的js代码:

var start = (new Date()).getTime();
var elapsed = (new Date()).getTime() - start;
document.writeln("time is:" + elapsed + " seconds");

如果对js比较熟悉的,会知道,getTime()返回的是ms,而不是s,因此这样的命名会很容易 产生bug,不是每个程序员都会记得那么清楚getTime()返回的单位是什么。把这2个变量改成 start_ms和elapsed_ms就很清楚了! 看到这里,我决定,以后遇到这种有单位的变量,都要带个单位的后缀,以写出可读的代码。

deciding how long a name should be

在较短的域里面可以使用较短的变量名,比如:

if(i != k){
    int t = a[i];
    a[i] = a[k];
    a[k] = t;
}

现在,打一个长变量名已经不是问题了。

我以前很恶心java的类名方法名很长,看起来不简洁,现在看多了也习惯了,而且现在打长的变量 名的确很简单了,因此现在很多编辑器或IDE都有自动补全的功能。比如我现在使用的Emacs就自带 有补全的功能(Alt + /),我居然还不知道,因此我都是用auto-complete的,看来当它不起作用 时,我就可以手动地补全了。

有关缩略词, 作者说,对于工程项目的代码,最好不要写缩略词,因为新加进来的成员可能看不懂缩略词的意思, 而一些最常见的缩写,比如evaluation写成eval,string写成str,document写成doc,则写 成缩写比较好。其实,我觉得,像linux系统这么大的工程都使用了很多缩写,有的时候缩写还是 非常必要的,可能是我的个人原因,我不喜欢又臭又长的代码,我喜欢简洁的代码,对于unix哲学 中的缩写规则,我很感兴趣,在程序中还是要尽量使用缩写。

using name formating to pack extra information

不同类型的实体命名特点, 在google c++的规范中,类名首字母大写,使用驼峰式;宏常量名全部大写,使用下划线分隔; const变量则首字母大写,驼峰式,区分宏常量;类的方法首字母大写,驼峰式;类的变量全小写, 最后要跟一个下划线;其它局部变量则全部小写,后面不跟下划线。 在html/css中,id一般使用下划线分隔,而class使用中线(dash)来分隔。

names that can’t be misconstructed

  • 关键思想是:不断地问自己,这个名字别人会不会认为是其他意思?
  • 很多情况对变量的命名是有包含与不包含的意思的。这里列举出几种很常用的用法。
    • 包含的情况,使用min和max,比如说min_items和max_students
    • 两端包含,first和last。
  • 包含前面,不包含后面,使用begin和end。
  • 在给布尔型变量命名时,注意不要带负面的意思,如不要使用:

    bool disable_ssl = false;
    

而要使用:

bool use_ssl = true;
  • 一般会使用is,can,has前缀命名布尔型变量。

  • 函数的名字一定要和里面的操作相符,书上举了一个例子,比如函数getSum,这个函数的 实现是计算一大堆数据的和,但是一般程序员第一眼看过去的时候就会以为仅仅是返回和,并 没有想到里面会有代价很多的计算,很有可能会经常调用这个函数,这样就会使程序变得很慢。 使用computeSum会使人更容易明白。

  • 作者还举了STL里面的list::size()方法来批评。这个方法会一个节点一个节点地计算链 表的长度,O(n)的速度很慢。看下面的例子:

    while (list.size() > max_size) {
        ...
    }
    

连写STL的程序员也有不规范的时候,一般的程序员会以为size()是O(1)的速度,直接返回 链表的长度,这样就会使得程序的速度非常慢了。如果改成countSize()会好很多, 但是幸运的是,作者说了,最新版的STL已经把size()变成了O(1)速度。

aesthetics

举的第一个例子让我震惊!看下面的代码:

public class PerformanceTester {
        public static final TcpConnectionSimulator wifi =
                new TcpConnectionSimulator(
                    500,  /* Kbps */
                    80,   /* millisecs */
                    200,  /* jitter */
                    1     /* packet loss % */);
        public static final TcpConnectionSimulator t3_fiber =
                new TcpConnectionSimulator(
                    4500,  /* Kbps */
                    10,    /* millisecs */
                    0,     /* jitter */
                    0      /* packet loss % */);
        public static final TcpConnectionSimulator cell =
                new TcpConnectionSimulator(
                    100,  /* Kbps */
                    400,  /* millisecs */
                    250,  /* jitter */
                    5     /* packet loss % */);
}

这里,缩进是对齐了,注释也对齐了,但是占用的行数太多,而且注释重复了3遍。 改成这样就好看多了:

public class PerformanceTester {
        // TcpConnectionSimulator(throughput, lantency, jitter, packet_loss)
        //                          [Kbps]     [ms]      [ms]    [percent]

        public static final TcpConnectionSimulator wifi =
                new TcpConnectionSimulator(500, 80, 200, 1);
        public static final TcpConnectionSimulator t3_fiber =
                new TcpConnectionSimulator(4500, 10, 0, 0);
        public static final TcpConnectionSimulator cell =
                new TcpConnectionSimulator(100, 400, 250, 5);
}

作者建议代码要列对齐,我之前在programming windows一书中看到过这种 漂亮的代码,但是一直做不到,因为要打很多空格。后来发现emacs有通过正则表达式 对齐的功能align-regexp,爽极了!

knowing what to comment

when should not comment?

看注释会浪费阅读代码的人的时间,注释还会占用屏幕的地方,导致读者时常要 翻页,所以,没有价值的注释不要写。下面的代码的注释都没有价值:

// The class definition for Account
class Account {
public:
        // Constructor
        Account();

        // Set the profit member to a new value
        void SetProfit(double  profit);

        // Return the profit from this Account
        double GetProfit();
};

因为,注释的含义从代码中已经可以看出来了,注释没有提供额外的信息,其实是和 代码重复了,不仅浪费地方,而且浪费写代码的人的时间和读代码的人的时间。 记住:

good code > bad code + good comment

what should comment be?

  • 写自己的想法

  • 写自己的代码的缺点,比如:

    // TODO:use a faster algorithm
    

有一些和TODO一样的很流行的标签

  • 解释常量为什么是那个值

making comments precise and compact

  • 函数注释中,可使用举例说明,如:

    //Example:strip("abba/a/ba", "ab") returns "/a/"
    
  • 要写明你的代码的意图。看下面例子:

    // Iterate through the list in reverse order
    for (i = SIZE; i >= 0; i--) {
        ...
    }
    

上面的注释写了和没写差不多,改成这样就非常好了:

// display each price, from highest to lowest
for (i = SIZE; i >= 0; i--) {
    ...
}
  • 调用函数的时候也可以注释,按照书上的例子,这样的代码:

    Connect(/* timeout_ms = */ 10, /* use_encryption = */ false);
    

要比这样的代码:

Connect(10, false);

更容易看懂。

重构代码

extracting unrelated subproblems

  • 要封装一些和本功能无关的子问题到另外一个函数上。
  • 对于一些工具的类或函数(字符串操作,哈希表等),就分离出来。

one task at a time

每个函数应该只实现一个功能,不要实现多个功能。

turning thoughts into code

记住爱因斯坦的这句话:

You do not really understanding something unless you can explain it to
your grandmother.               
                                            --Albert Einstein
代码应该用易懂的英语来写。

writing less code

  • 一定要熟悉现存的库,这样可以减少代码量,多重用代码,少写代码。
  • 使用Unix工具(shell命令),而不是自己写代码

写出易懂的选择排序

以选择排序为例,我以前是直接把它记住的(当然是在理解的前提下),其中的一些 下标i,j,k,直接记住人家的代码的,如果要我重新写,我还是会用i,j,k,造成了 硬性的思维,现在看来,只要记住算法,用自己的方式来写出的代码才是好的代码。 下面的选择排序是经典的课本上的代码:

void select_sort(int *arr,int len)
{
    int i,j,k,temp;

    for(i = 0; i < len - 1; i++){
            k = i;
            for(j = i + 1; j < len; j++){
                    if(arr[j] < arr[k]){
                            k = j;
                    }
            }
            if(i != k){
                    temp = arr[k];
                    arr[k] = arr[i];
                    arr[i] = temp;
            }
    }
}

我觉得,有经验的程序员看上面这份代码当然是没问题,但是给初学者来看,i,j,k分别 代表什么意思,他就会摸不着头脑了,所以,我们需要给变量的名字赋予意义,更容易 理解,于是我写了下面的代码:

void select_sort(int *arr,int len)
{
    int current,next,smallest,temp;

    for(current = 0; current < len - 1; current++){
            smallest = current;
            for(next = current + 1; next < len; next++){
                    if(arr[next] < arr[smallest]){
                            smallest = next;
                    }
            }
            if(current != smallest){
                    temp = arr[smallest];
                    arr[smallest] = arr[current];
                    arr[current] = temp;
            }
    }
}

这样的话,单从变量名字就可以理解整个算法的思想了。虽然变量的名字是长了一点, 但是现在的编辑器或者IDE的自动补全功能这么强大,变量名太长这个已经不是问题了。