“Notes on Programming in C” 阅读 (精简版)

写在前面: 之前有一个完整的版本,包含了原文、翻译和评注的版本。但感觉这样很影响阅读,所以又写了一个精简版,只包含部分翻译和个人评注。

“Notes on Programming in C” 一文是 罗布·派克 (Rob Pike) 于 1989 年写的一份关于 C 语言编程的编程实践建议,包含 9 个主题的简要说明,涵盖了代码风格、程序优化、设计模式等内容。

该文虽然是针对 C 语言所写,并且年代久远,但其中的很多想法对编写高质量的代码现在看来仍然具有非常好的指导意义。

此外,正如文中 “Introduction” 部分所说,

… 我并不希望每个人都赞同文中所述的内容,因为这只是一些 “见解”,而 “见解” 将随着时间而变 … (如果你反对我的想法) 如果这里内容使你思考你为何而反对,那么 (比起全盘照收) 更好。绝不要我说应该如何做你就怎样编程,应该按照你认为的完成该程序的最好的方法去进行编程

这也是我想和阅读本文的读者想说的。

0. 大纲

  1. Issues of typography: 代码格式 (推荐阅读指数 2)
  2. Variable names: 变量命名 (推荐阅读指数 2)
  3. The use of pointers: 指针的使用 (推荐阅读指数 1)
  4. Procedure names: 函数命名 (推荐阅读指数 2)
  5. Comments: 注释 (推荐阅读指数 3)
  6. Complexity: 代码复杂度 (这应该是这篇文章最有名的一部分) (推荐阅读指数 4)
  7. Programming with data: 面向数据编程 (推荐阅读指数 2.5)
  8. Function pointers: 函数指针 (推荐阅读指数 1.5)
  9. Include files: include 文件 (推荐阅读指数 0.5)

之所以没有五星推荐,是因为即便是最有名的章节,其中的观点放在现在也都是非常广为人知的了

1. Issues of typography: 代码格式

程序应该被视作是供程序员阅读的出版物(但却不应该墨守成规),通俗地讲就是让其他人理解起来不费劲儿。

同时,整个团队的代码风格的一致性是比清晰而规范的格式更加重要的。(本质上这也是为了让代码阅读起来更加容易)

2. Variable names: 变量命名

对于变量命名,真正起决定性的应该是表达的清晰性,而非名称的长短。

如果是一个很少被用到的全局变量,叫一个如 maxphysaddr 这种较长的名字还没有什么问题。但对于一个循环中每一行都会被用到的数组的索引 (index),没有什么比 i 更加合适的了。给这种变量命名为 index 或者 elementnumber 只会让你输入更多字符而没有其他的意义,并且使得代码变得更加晦涩。一般情况下,变量名越长,代码逻辑阅读与梳理就变得越费劲,例如:

for(i=0 to 100)
    array[i]=0
---- v.s. -----
for(elementnumber=0 to 100)
    array[elementnumber]=0

真实的代码比这个例子更加糟糕(我就曾经因为过分追求变量名称的意义将一个简单的变量弄得读起来非常费劲)。索引就仅仅是一个表示,保持 i 这种命名风格就好。

变量命名除了清晰可理解外,剩下的准则就只有代码前后的风格一致性了。例如,如果你的一个变量被命名为 maxphysaddr,不要将另一个变量命名为 lowestaddress (正确的应该是 minphyaddr)

就我个人 (原作者) 而言,我是更喜欢较短的名称的,那些没有在变量中体现的含义,可以从上下文中容易地推测出来。

然而全局变量,因为缺少上下文信息,所以应该命名中所包含的信息尽量全一些,这就是为何全局变量我会命名成 maxphysaddr 这种风格;而局部变量,我则简单地命名为 np。当然名称长短的问题仅仅是个人品味,但有时候个人的品味却关乎代码的清晰性。

有的小伙伴要问了,索引都用 i 命名,最后不就是 i, j, l, m, n… 了么。我们思考一个问题,如果代码已经多层循环到了索引命名都很困难了,是不是循环过深了呢?是不是函数承载的功能过多呢?是不是应该考虑重构现有代码呢?

3. The use of pointers: 指针的使用

原作者例举了指针的一些好处,但无论如何,我个人倾向于指针是邪恶的
指针之所以容易出错,本质上是因为指针相比于其他代码的语法在逻辑上是更加抽象的,人类的大脑没有办法一直准确地处理这种抽象。
如果一个程序中充满了复杂的指针运算,调试和阅读起来会非常艰难。
这也是为什么大多数更高级的编程语言都抛弃了指针。

另外,编写程序时如果能使用易于理解的代码特性实现一些功能,不要使用那些理解起来很困难的语法,这种困难很可能是因为这种语法比较抽象,需要更多的大脑负荷;而增加大脑负荷,只会使开发者无法聚焦到应该聚焦的事情上。

不过有些人天生觉得指针这一类东西很好理解也很好用,这些人真的是很幸运。

4. Procedure names: 函数命名

函数的命名应该反映其功能,同时也应该体现出其返回值的形式。因为函数是使用在表达式中的,经常被用于 if 语句中,所以函数的名字需要被合理的命名,例如

if(checksize(x))

checksize 没有办法帮我们推断出当 x 不合理时,这个函数到底是返回 true 还是 false,取而代之的,

if(validsize(x))

validsize 就可以让我们明确的知道其返回逻辑,使用起来也将更少出错。

5. Comments: 注释

原作者由于以下原因倾向于尽量少地去写注释:

  • 第一,好的代码结构清晰,命名规范,本身就是自解释的,不需要额外的注释
  • 第二,注释无法被编译器检测,所以无法保证你含义的正确性,尤其是代码被修改了以后。误导的注释会让人对代码的逻辑感到困惑
  • 第三,注释使代码变的凌乱不堪

但也不是从来不写任何注释。最多的情况是,对接下来的一段代码的作用作出简介。例如,

  • 对全局变量的解释;
  • 对一个不是很合乎常理或者一个关键函数的简介;
  • 一大段计算结尾的标志

除非是核心数据结构的前面,不要出现大团大团的注释 (对于数据的注释绝对比对算法的注释重要得多)。基本而言,避免写注释就对了;如果你觉得你的代码非要写注释才能被理解,那最好重构成更容易理解的代码,见下一节 “Complexity: 代码复杂度”

努力提高代码的自解释性比大段的注释重要得多

另外,我在实际开发中经常发现代码中存在 IDE 自动生成的注释,例如:

/**
 * 从一个JSON数组得到一个java对象集合,其中对象中包含有集合属性
 * @param object
 * @param clazz
 * @param map
 * 
 * @return
 */
public static <T,K> List<?> getDTOList(String jsonString, Class<T> clazz, Map<String,K> map) {
...
}

因为代码迭代,上面的例子中第一个 @param object 和实际签名无法对应,而且即便可以对应,这种把函数签名重写一遍的操作有什么意义?只会让 IDE 提示高亮影响代码的阅读!
“Notes on Programming in C” 阅读 (精简版)_第1张图片

6. Complexity: 代码复杂度

绝大多数的代码都过于复杂了,或者说,比高效地解决需求所需要的代码复杂度要复杂。

为何会这样?大多数情况下这是因为一个糟糕的代码设计 (架构),但这里我不想讨论关于代码设计 (架构) 这一话题,这个话题太大太广了。

排除架构不谈,即便是一些微小的方面,大多数代码还是过于复杂,遵循下面的原则可以在一定程度上避免复杂代码的出现:

  • 规则 1: 你无法真正地知道一个程序真正把资源花费在哪里,真正的性能瓶颈总是在一些意想不到的地方。所以不要一而再再而三地瞎猜然后修改代码,除非你十分确定性能瓶颈真正发生在什么地方
  • 规则 2: 测量。在你能准确测量代码各部分性能之前不要操之过急;即便你已经测量准确了,如果一部分代码和其他部分比起来性能不是特别的差,最好也不要进行优化
  • 规则 3: 当 n 很小的时候,那些所谓 “高效” 的算法通常性能比较差,而现实中 n 通常是小的。除非你知道在你的实际场景中 n 经常会是很大的,不要使用那些看起来非常吸引人的复杂算法 (在优化前要先看一眼上面的规则 2)。例如,在日常工作中,二叉树 (binary trees) 往往比伸展树 (splay trees) 性能更好
  • 规则 4: 与简单的算法比起来,吸引人的 “高效” 算法要花费更多精力才能实现,然而却往往更容易产生 bug。所以尽量使用简单的算法和简单的数据结构。
    在日常的使用中,下面的数据结构绝对够用了
    text 数组: array 链表: linked list 哈希表: hash table 二叉树: binary tree
    当然你要把这些数据合理的组织成复合数据。例如,一个符号表 (Symbol Table,应该就是字典) 需要由哈希表以及数组的链表复合而成。
  • 规则 5: 数据驱动。如果你选择了正确的数据结构并且数据之间组织得足够良好,对应的算法几乎是不言自明的。程序的核心是数据结构而非算法 (参考 Brooks 人月神话 102 页,数据的表现形式是编程的根本)
  • 规则 6: 没有规则 6 (我理解是所有的东西都要尽量保持简洁)

个人感觉作者想表达的总结起来就是

  • 不要凭空地优化,不要贸然地优化
  • 尽量简化你的程序:用一句时髦的话叫简单可依赖
  • 数据先于算法:有助于提高代码质量和开发、维护效率

7. Programming with data: 面向数据编程

与一大堆 if else 相比,算法经常可以通过数据被更加紧凑、高效、清晰的表达出来。这是因为,如果我们要处理的复杂性,是由于一系列相互独立细节的排列组合引起的,这个排列组合本身就是可以被编码简化的,对应的复杂性也是可以被简化的。
一个典型的例子是分析表 (parsing tables,编译原理相关,见 wiki),即将编程语言复杂的语法规则转化为固定的、相对简单的语法,从而使其可以被运行。有限状态机特别适用于这个艰巨的工作。几乎任何输入是抽象类型、输出是独立行为的解析类的程序,都可以通过数据驱动来进行编程。

这种程序设计思路最让人觉得神奇的地方是,一个程序的驱动表有时是由另一个程序动态生成的,在编译器的例子中,后者称为解析生成器 (parser generator)。
一个比较接地气的例子是,某个操作系统通过一系列的表来进行驱动,这些表正确将 I/O 请求连接到合适的设备驱动,而这些表的配置正是由一个数据驱动的程序来完成的:该程序读入连接到该系统的一系列特定设备的描述信息,并生成了这些驱动表。

8. Function pointers: 函数指针

这一部分主要是对 C 语言而言。函数指针也是公认的 C 语言比较难以掌握的功能,不合理的使用将使得代码可读性非常差,引发各种潜在的、难以调试的 BUG。

不过作者的核心论点是,将函数作为变量一样操作,可以方便的进行面向对象抽象,进而使得 C 语言更容易开发和维护。虽然以上这些功能面向对象语言都可以方便地支持。

脱离 C 语言这一情景,作者这里提到的 “协议” (protocol) 的概念在各种语言中都得到了广泛的应用,例如 Java 语言可以将接口作为参数进行传递,python 中函数和类都是 “first class” ,以及鸭子类型等。

函数指针这一节说的实际上是面向 “协议” (protocol) 的编程,常见的说法是面向接口而非面向实现编程。这使得程序的抽象能力得到了增强;而抽象意味着固定、不变,即面对新的需求,代码将不需要修改,只需要进行扩展就行了。

这也是为什么作者一直在强调 “使用函数指针来对代码的复杂性进行封装”。

9. Include files: include 文件

你可能感兴趣的:(程序设计)