“Notes on Programming in C” 阅读

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

这里是我关于这篇文章的阅读笔记。除了原文 “Introduction” 部分,其他的部分的行文都将包含如下三个部分:

  • 原文
  • 简要翻译
  • 评注:针对这一主题,结合自己的工作经验,产生的一些想法和见解

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

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

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

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

0. 大纲

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

1. Issues of typography: 代码格式

1.1 原文

A program is a sort of publication. It’s meant to be read by the programmer, another programmer (perhaps yourself a few days, weeks or years later), and lastly a machine. The machine doesn’t care how pretty the program is - if the program compiles, the machine’s happy - but people do, and they should. Sometimes they care too much: pretty printers mechanically produce pretty output that accentuates irrelevant detail in the program, which is as sensible as putting all the prepositions in English text in bold font. Although many people think programs should look like the Algol­68 report (and some systems even require you to edit programs in that style), a clear program is not made any clearer by such presentation, and a bad program is only made laughable.

Typographic conventions consistently held are important to clear presentation, of course - indentation is probably the best known and most useful example - but when the ink obscures the intent, typography has taken over. So even if you stick with plain old typewriter­like output, be conscious of typographic silliness. Avoid decoration; for instance, keep comments brief and banner­free. Say what you want to say in the program, neatly and consistently. Then move on.

1.2 简要翻译

程序应该被视作是供程序员阅读的出版物。
机器不会在意程序的格式是否优雅,但程序员应该也肯定会在意这件事。

而有些时候,程序员有点 “过分关注” 代码的格式优雅性,往往造成了 “过犹不及”:阅读这种代码好像是在阅读所有虚词都加粗了的语句一样 (即注意力被无关紧要的东西吸引开了)。
尽管有人认为程序应该像 “Algol­68” 的报告一样每个细节都遵循规定好的格式 (甚至有些系统会强制这样做),但一个已经非常清晰的程序如果生硬的套用这种格式模板的话只会使得可阅读性下降

代码的格式前后一致性是比清晰而规范的格式更加重要的。虽然清晰而规范的代码格式也是非常非常重要的一件事 (一个比较好的例子就是正确的缩进),但如果 “墨水” 隐藏了 “意图”,代码格式就被过分强调了。
因此即便你坚持你的代码格式,也要有所警惕。
避免过分地装饰,例如注释一定要保持简洁并且是 “非条幅化” 的。
代码的行文要尽量简洁且前后一致

1.3 评注

首先程序需要有高可读性,代码格式规范带来高可读性,这两个观点大多数人都是赞同的。
其次关于是否要严格遵守代码格式规范的问题,Rob Pike 的观点是符合中国人的实用主义文化的。

一定要注意,这个观点不是大家拿来不遵守团队代码格式规范写 “奔放” 程序的借口,多问问自己,你觉得一看就懂的逻辑真的对于你的同事来说一看就懂吗?

2. Variable names: 变量命名

2.1 原文

Ah, variable names. Length is not a virtue in a name; clarity of expression is. A global variable rarely used may deserve a long name, maxphysaddr say. An array index used on every line of a loop needn’t be named any more elaborately than i. Saying index or elementnumber is more to type (or calls upon your text editor) and obscures the details of the computation. When the variable names are huge, it’s harder to see what’s going on. This is partly a typographic issue; consider

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

The problem gets worse fast with real examples. Indices are just notation, so treat them as such.

Pointers also require sensible notation. np is just as mnemonic as nodepointer if you consistently use a naming convention from which np means ``node pointer’’ is easily derived. More on this in the next essay.

As in all other aspects of readable programming, consistency is important in naming. If you call one variable maxphysaddr, don’t call its cousin lowestaddress.

Finally, I prefer minimum­length but maximum­information names, and then let the context fill in the rest. Globals, for instance, typically have little context when they are used, so their names need to be relatively evocative. Thus I say maxphysaddr (not MaximumPhysicalAddress) for a global variable, but np not NodePointer for a pointer locally defined and used. This is largely a matter of taste, but taste is relevant to clarity.

I eschew embedded capital letters in names; to my prose­oriented eyes, they are too awkward to read comfortably. They jangle like bad typography.

2.2 简要翻译

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

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

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

真实的代码比这个例子更加糟糕。索引就仅仅是一个表示,保持 i 这种命名风格就好。

指针也应该被合理的命名。如果从团队代码的命名传统中很容易猜到 np 的含义是 node pointer,那么使用 np 就是合理的!

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

最后就我个人而言,我是更喜欢较短的名称的,那些没有在变量中体现的含义,可以从上下文中容易的推测出来。
例如全局变量,因为缺少上下文信息,所以应该命名中所包含的信息尽量全一些,这就是为何全局变量我会命名成 maxphysaddr 这种风格;而局部变量,我则简单的命名为 np。当然名称长短的问题仅仅是个人品味,但有时候个人的品味却关乎代码的清晰性。

我在命名中会尽量避免大小写混用,就我而言这种用法看起来很不舒服

2.3 评注

注意,这里虽然作者举了一些较短但变量名称比长名称更容易理解的例子,但其本意不是在讨论名称长短,而是说一个变量的命名应该从其可被理解性来考量 (虽然作者本身是更喜欢短名称的)

变量命名的清晰易懂性和前后风格一致性是不需要过多讨论的,但对于很多中国人来说想给变量起一个又短又好的名字真的很难。
我个人的工作中会为了使得变量名称信息更全,写很多比较长的变量和函数的名称,并且确实发生了代码的阅读和理解比较难的问题,不过之前并没有深入思考过
此外,关于全局变量和局部变量命名上的差别问题让我很有启发。

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

关于大小写混用影响阅读性的问题,这就纯属作者的个人喜好了

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

3.1 原文

C is unusual in that it allows pointers to point to anything. Pointers are sharp tools, and like any such tool, used well they can be delightfully productive, but used badly they can do great damage (I sunk a wood chisel into my thumb a few days before writing this). Pointers have a bad reputation in academia, because they are considered too dangerous, dirty somehow. But I think they are powerful notation, which means they can help us express ourselves clearly.

Consider: When you have a pointer to an object, it is a name for exactly that object and no other. That sounds trivial, but look at the following two expressions:

np
node[i]

The first points to a node, the second evaluates to (say) the same node. But the second form is an expression; it is not so simple. To interpret it, we must know what node is, what i is, and that i and node are related by the (probably unspecified) rules of the surrounding program. Nothing about the expression in isolation can show that i is a valid index of node, let alone the index of the element we want. If i and j and k are all indices into the node array, it’s very easy to slip up, and the compiler cannot help. It’s particularly easy to make mistakes when passing things to subroutines: a pointer is a single thing; an array and an index must be believed to belong together in the receiving subroutine.

An expression that evaluates to an object is inherently more subtle and error­prone than the address of that object. Correct use of pointers can simplify code:

parent->link[i].type

---- v.s. -----

lp->type

If we want the next element’s type, it’s

parent->link[++i].type

---- v.s. -----

(++lp)->type

i advances but the rest of the expression must stay constant; with pointers, there’s only one thing to advance.

Typographic considerations enter here, too. Stepping through structures using pointers can be much easier to read than with expressions: less ink is needed and less effort is expended by the compiler and computer. A related issue is that the type of the pointer affects how it can be used correctly, which allows some helpful compile­time error checking that array indices cannot share. Also, if the objects are structures, their tag fields are reminders of their type, so

np->left

is sufficiently evocative; if an array is being indexed the array will have some well­chosen name and the expression will end up longer:

node[i].left

Again, the extra characters become more irritating as the examples become larger.

As a rule, if you find code containing many similar, complex expressions that evaluate to elements of a data structure, judicious use of pointers can clear things up. Consider what

if(goleft)
	p->left=p->right->left;
else
	p->right=p->left->right;

would look like using a compound expression for p. Sometimes it’s worth a temporary variable (here p) or a macro to distill the calculation.

3.2 简要翻译

C 语言的指针可以指向任何东西,这使得指针是一把 “利刃”;也正如利刃一般,使用指针虽然高效,但使用不当却将造成 “伤害”。在学术界,使用指针的危险性使其声名狼藉。但我认为指针是一种强有力的 “表现”,即指针可以帮助开发者更加清晰的表现其本意。

现在假设我们有一个指向某个对象的指针,那么指针本身不会引起任何歧义,并可以准确的表示这一含义。虽然这听起来无关痛痒,但让我们看下面两个语句:

np
node[i]

其中第一个 np 是指向一个节点的指针,第二个 node[i] 代表了和指针所表示的相同的节点。

然而, node[i] 是一个表达式,这不够简洁。但当我们看到 node[i] 时,我们必须先知道 node 代表了什么,再弄清楚 i 代表了什么,并且 nodei 的关系只能通过上下文才能了解。
表达式是一种不独立的表现形式,以至于我们拿到 node[i] 这个表达式甚至无法知道 i 是否合法;如果程序中我们同时写了一堆 node[i]node[j]node[k] 的表达式,那代码肯定更容易出错,并且这种错误编译器没有办法自动的识别。
在调用子程序时,由于指针的形式只传递了一个变量,而表达式需要分别传递数组和索引,并且这两者必须在代码逻辑上是正确的,显然后者更容易出错。

与指针比起来,表达式更容易出现潜在的错误;将表达式改为指针可以使得程序更加清晰,例如:

parent->link[i].type

---- v.s. -----

lp->type

如果我们想获得下一个元素,两种表示方法的写法如下:

parent->link[++i].type

---- v.s. -----

(++lp)->type

表达式中,i 变化了而其他的部分却保持不变;而指针只有一个东西,并且这个东西变化了。

这也是出于代码格式风格的考量。使用指针来遍历一个结构体要比使用表达式简单多了:更少的代码,同时编译器也需要解释更少的东西 (注:编译器在处理 ++lp 这种表达式的时候只是移动了指针位置,而处理 link[++i] 需要进行更多的编译步骤)。
此外,指针由于指定了所代表的类型,编译器可以方便的检测相对应的编码错误;而这一点是只用索引所不能的 (不是很理解 ???)。
并且,如果指针所指的对象是结构体,结构体的成员变量也可以正确的被指示类型,

np->left

上面的例子中一切刚刚好,我们通过 left 可以知道 np 的含义。而如果使用数组和索引的方式,首先数组就需要有一个精挑细选的名字,那么整个表达式就会变得更长,例如

node[i].left

另一个准则是,如果你发现你的代码中有很多相似的、复杂的、且用来获取一个结构的元素的表达式时,需要谨慎地使用指针才能使代码更清晰,比如下面的例子:

if(goleft)
	p->left=p->right->left;
else
	p->right=p->left->right;

例子中看起来就像是在使用 p 的某种复合的表达式。在这种情况下,定义一个临时变量或者一个宏来将相同的运算抽取出来,是个不错的选择。

3.3 评注

坦白地说,这一篇的后半部分我基本没有 get 到作者的点,我猜想这可能和我们现在用惯了 IDE,而作者当时的代码编译和构建环境都比较原始有关,我有点理解不了其中的一些点。

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

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

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

4. Procedure names: 函数命名

4.1 原文

Procedure names should reflect what they do; function names should reflect what they return. Functions are used in expressions, often in things like if’s, so they need to read appropriately.

if(checksize(x))

is unhelpful because we can’t deduce whether checksize returns true on error or non­error; instead

if(validsize(x))

makes the point clear and makes a future mistake in using the routine less likely.

4.2 简要翻译

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

if(checksize(x))

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

if(validsize(x))

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

4.3 评注

是关于函数命名的非常好的建议,受到启发

5. Comments: 注释

5.1 原文

A delicate matter, requiring taste and judgement. I tend to err on the side of eliminating comments, for several reasons. First, if the code is clear, and uses good type names and variable names, it should explain itself. Second, comments aren’t checked by the compiler, so there is no guarantee they’re right, especially after the code is modified. A misleading comment can be very confusing. Third, the issue of typography: comments clutter code.

But I do comment sometimes. Almost exclusively, I use them as an introduction to what follows. Examples: explaining the use of global variables and types (the one thing I always comment in large programs); as an introduction to an unusual or critical procedure; or to mark off sections of a large computation.

There is a famously bad comment style:

i = i + 1;  /* Add one to i */

and there are worse ways to do it:

/**********************************
*                                *
*          Add one to i          *
*                                *
**********************************/

              i=i+1;

Don’t laugh now, wait until you see it in real life.

Avoid cute typography in comments, avoid big blocks of comments except perhaps before vital sections like the declaration of the central data structure (comments on data are usually much more helpful than on algorithms); basically, avoid comments. If your code needs a comment to be understood, it would be better to rewrite it so it’s easier to understand. Which brings us to

5.2 简要翻译

恰当的注释需要品味和判断力。我个人由于以下原因倾向于不去写注释:

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

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

  • 对全局变量的解释 (我总是会写这种注释);
  • 对一个不是很合乎常理或者一个关键函数的简介;
  • 一大段计算结尾的标志

下面是一个典型的糟糕注释

i = i + 1;  /* Add one to i */

下面这个绝对是更糟糕的注释

/**********************************
*                                *
*          Add one to i          *
*                                *
**********************************/

              i=i+1;

你可能觉得很好笑,直到你真的看到有人这么写注释

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

5.3 评注

努力提高代码的自解释性是重要的

另外,我在实际开发中经常发现代码中存在 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 提示高亮影响代码的阅读!
IDE提示

6. Complexity: 代码复杂度

6.1 原文

Most programs are too complicated - that is, more complex than they need to be to solve their problems efficiently. Why? Mostly it’s because of bad design, but I will skip that issue here because it’s a big one. But programs are often complicated at the microscopic level, and that is something I can address here.

Rule 1. You can’t tell where a program is going to spend its time. Bottlenecks occur in surprising places, so don’t try to second guess and put in a speed hack until you’ve proven that’s where the bottleneck is.

Rule 2. Measure. Don’t tune for speed until you’ve measured, and even then don’t unless one part of the code overwhelms the rest.

Rule 3. Fancy algorithms are slow when n is small, and n is usually small. Fancy algorithms have big constants. Until you know that n is frequently going to be big, don’t get fancy. (Even if n does get big, use Rule 2 first.) For example, binary trees are always faster than splay trees for workaday problems.

Rule 4. Fancy algorithms are buggier than simple ones, and they’re much harder to implement. Use simple algorithms as well as simple data structures.

The following data structures are a complete list for almost all practical programs:

array
linked list
hash table
binary tree

Of course, you must also be prepared to collect these into compound data structures. For instance, a symbol table might be implemented as a hash table containing linked lists of arrays of characters.

Rule 5. Data dominates. If you’ve chosen the right data structures and organized things well, the algorithms will almost always be self­evident. Data structures, not algorithms, are central to programming. (See Brooks p. 102.)

Rule 6. There is no Rule 6.

6.2 简要翻译

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

为何会这样?大多数情况下这是因为一个糟糕的代码设计 (架构),但这里我不想讨论关于代码设计 (架构) 这一话题,这个话题太大太广了。即便是从一些微小的方面,大多数代码还是过于复杂,这也是我这里想剖析的一些点:

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

6.3 评注

总结起来就是

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

这三条原则真的真的是实践出真知,我猜大多数一线程序员都会表示赞同

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

7.1 原文

Algorithms, or details of algorithms, can often be encoded compactly, efficiently and expressively as data rather than, say, as lots of if statements. The reason is that the complexity of the job at hand, if it is due to a combination of independent details, can be encoded. A classic example of this is parsing tables, which encode the grammar of a programming language in a form interpretable by a fixed, fairly simple piece of code. Finite state machines are particularly amenable to this form of attack, but almost any program that involves the ‘parsing’ of some abstract sort of input into a sequence of some independent `actions’ can be constructed profitably as a data­driven algorithm.

Perhaps the most intriguing aspect of this kind of design is that the tables can sometimes be generated by another program - a parser generator, in the classical case. As a more earthy example, if an operating system is driven by a set of tables that connect I/O requests to the appropriate device drivers, the system may be `configured’ by a program that reads a description of the particular devices connected to the machine in question and prints the corresponding tables.

One of the reasons data­driven programs are not common, at least among beginners, is the tyranny of Pascal. Pascal, like its creator, believes firmly in the separation of code and data. It therefore (at least in its original form) has no ability to create initialized data. This flies in the face of the theories of Turing and von Neumann, which define the basic principles of the stored­program computer. Code and data are the same, or at least they can be. How else can you explain how a compiler works? (Functional languages have a similar problem with I/O.)

7.2 简要翻译

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

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

数据驱动编程不是很普遍的原因之一 (至少在新手中) 是 Pascal 语言的盛行 (国外很多人使用 Pascal 语言作为学习编程的入门语言)。 Pascal 语言,如同其作者一样,坚信数据应该和算法分离。因此 Pascal 语言 (至少其原始版本) 无法初始化数据。这实际是和图灵以及冯诺伊曼的理论相违背的。代码和数据是相同的,如果不是这样,计算机又该如何工作呢?(函数编程在处理 I/O 时也存在相同的问题)

7.3 评注

没啥可说的,实践中你会发现:数据驱动编程确实可以简化代码逻辑,而且更容易理解

8. Function pointers: 函数指针

8.1 原文

Another result of the tyranny of Pascal is that beginners don’t use function pointers. (You can’t have function­valued variables in Pascal.) Using function pointers to encode complexity has some interesting properties.

Some of the complexity is passed to the routine pointed to. The routine must obey some standard protocol - it’s one of a set of routines invoked identically - but beyond that, what it does is its business alone. The complexity is distributed.

There is this idea of a protocol, in that all functions used similarly must behave similarly. This makes for easy documentation, testing, growth and even making the program run distributed over a network - the protocol can be encoded as remote procedure calls.

I argue that clear use of function pointers is the heart of object­oriented programming. Given a set of operations you want to perform on data, and a set of data types you want to respond to those operations, the easiest way to put the program together is with a group of function pointers for each type. This, in a nutshell, defines class and method. The O­O languages give you more of course - prettier syntax, derived types and so on - but conceptually they provide little extra.

Combining data­driven programs with function pointers leads to an astonishingly expressive way of working, a way that, in my experience, has often led to pleasant surprises. Even without a special O­O language, you can get 90% of the benefit for no extra work and be more in control of the result. I cannot recommend an implementation style more highly. All the programs I have organized this way have survived comfortably after much development - far better than with less disciplined approaches. Maybe that’s it: the discipline it forces pays off handsomely in the long run.

8.2 简要翻译

Pascal 语言盛行的后果是许多 C 语言的初学者都不会使用函数指针 ( Pascal 语言是无法把函数作为一个变量的)。使用函数指针来对代码的复杂性进行封装,会带来许多有趣的特性。

使用函数指针,调用者的逻辑的复杂性就会被传递到被调用者。这是因为被调用的函数需要遵循某种 “协议” (protocol) ,除了这一要求之外,其他具体的逻辑都将由被调用者来实现。即复杂性被分散了

上面提到的 “协议” (protocol) 的概念是,所有用起来类似的功能,其行为也应该是一致的。实践了这一思想,将更容易的书写文档、测试、扩展,甚至可以使得程序在网络上分布式地运行 (因为协议可以作为远程过程被调用)

我认为 (使用 C 语言进行) 面向对象编程的核心就是清晰的对函数指针的运用:你有一堆想对数据进行的操作,并且有一堆想和这些操作作出交互的数据类型,想达到这一目的的最简洁的办法就是将一系列操作不同数据类型的函数指针进行合理的组织。就好像在一个壳中定义了类和方法 (译注:因为 C 语言结构体是没有办法添加成员函数的,所以将数据和函数指针组织在结构体里,就达成了面向对象编程的基本要素)。虽然面向对象语言可以提供更多的特性,例如更优雅的语法、推导类型等等,但是就面向对象这一概念的核心而言,面向对象语言提供的这些特性是乏善可陈的。

在编程工作中,数据驱动思想与函数指针工具的结合,将引发令人震惊的表达能力;以我的个人经验,这种方式经常会让开发变得愉悦而轻松。用这种方法,即便不使用任何面向对象语言,仍然可以通过节省额外的工作以及对结果的强有力的掌控,提升 90% 的效率!和这种方法比起来,没有什么其他实现方式是我更加推崇的了。我用这种方式开发的所有程序,即便一再被修改,现在还都仍旧健康地运行着 — 比那些没有任何原则指导的程序要运行得好多了。为了遵循这一设计原则的所有努力,日后都会成倍回报给你。

8.3 评注

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

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

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

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

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

9. Include files: include 文件

9.1 原文

Simple rule: include files should never include include files. If instead they state (in comments or implicitly) what files they need to have included first, the problem of deciding which files to include is pushed to the user (programmer) but in a way that’s easy to handle and that, by construction, avoids multiple inclusions. Multiple inclusions are a bane of systems programming. It’s not rare to have files included five or more times to compile a single C source file. The Unix /usr/include/sys stuff is terrible this way.

There’s a little dance involving #ifdef’s that can prevent a file being read twice, but it’s usually done wrong in practice - the #ifdef’s are in the file itself, not the file that includes it. The result is often thousands of needless lines of code passing through the lexical analyzer, which is (in good compilers) the most expensive phase.

Just follow the simple rule.

9.2 简要翻译

一个简单的原则是: include 文件不要包含其他的 include 文件。

如果 include 文件陈述 (例如在注释或其他的隐式方法) 其运行必须依赖哪些其他文件,那么决定要如何引入这些文件的决定权就交给了使用者 (即程序员)。这种情况下,程序员可以更容易的掌控引入顺序、避免多次引入的问题等。多次引入相同头文件的问题是一个非常常见的问题。

好像使用 #ifdef这种语法可以避免多次引入的问题,然而实践中改方案常常被错误使用,即将 #ifdef 放在了自身,而不是要引入它的那个文件中。这尝尝导致编译过程中的语法分析器额外的处理成千上万条的额外代码,使得编译成为开发中最耗时的一个环节。

确保: include 文件不要包含其他的 include 文件

9.3 评注

对于 include 文件没有什么特别想说的

然而,当我们看别人的代码中有大量固定的写法时(就像作者所说的 #ifdef 这种)是不是要仔细想一想,这些东西真的有实际作用吗?还是只是程序员的一个不断复制、粘贴的错误

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章