“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 這種)是不是要仔細想一想,這些東西真的有實際作用嗎?還是隻是程序員的一個不斷複製、粘貼的錯誤

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