在最近關於 Linus Torvalds 的一個採訪中,這位 Linux 的創始人,在採訪過程中大約 14:20 的時候,提及了關於程式碼的 “good taste”。good taste?採訪者請他展示更多的細節,於是,Linus Torvalds 展示了一張提前準備好的插圖。
他展示的是一個程式碼片段。但這段程式碼並沒有 “good taste”。這是一個具有 “poor taste” 的程式碼片段,把它作為例子,以提供一些初步的比較。
這是一個用 C 寫的函式,作用是刪除連結串列中的一個物件,它包含有 10 行程式碼。
他把注意力集中在底部的 if
語句。正是這個 if
語句受到他的批判。
我暫停了這段視訊,開始研究幻燈片。我發現我最近有寫過和這很像的程式碼。Linus 不就是在說我的程式碼品味很差嗎?我放下自傲,繼續觀看視訊。
隨後, Linus 向觀眾解釋,正如我們所知道的,當從連結串列中刪除一個物件時,需要考慮兩種可能的情況。當所需刪除的物件位於連結串列的表頭時,刪除過程和位於連結串列中間的情況不同。這就是這個 if
語句具有 “poor taste” 的原因。
但既然他承認考慮這兩種不同的情況是必要的,那為什麼像上面那樣寫如此糟糕呢?
接下來,他又向觀眾展示了第二張幻燈片。這個幻燈片展示的是實現同樣功能的一個函式,但這段程式碼具有 “goog taste” 。
原先的 10 行程式碼現在減少為 4 行。
但程式碼的行數並不重要,關鍵是 if
語句,它不見了,因為不再需要了。程式碼已經被重構,所以,不用管物件在列表中的位置,都可以運用同樣的操作把它刪除。
Linus 解釋了一下新的程式碼,它消除了邊緣情況,就是這樣。然後採訪轉入了下一個話題。
我琢磨了一會這段程式碼。 Linus 是對的,的確,第二個函式更好。如果這是一個確定程式碼具有 “good taste” 還是 “bad taste” 的測試,那麼很遺憾,我失敗了。我從未想到過有可能能夠去除條件語句。我寫過不止一次這樣的 if
語句,因為我經常使用連結串列。
這個例子的意義,不僅僅是教給了我們一個從連結串列中刪除物件的更好方法,而是啟發了我們去思考自己寫的程式碼。你通過程式實現的一個簡單演算法,可能還有改進的空間,只是你從來沒有考慮過。
以這種方式,我回去審查最近正在做的專案的程式碼。也許是一個巧合,剛好也是用 C 寫的。
我盡最大的能力去審查程式碼,“good taste” 的一個基本要求是關於邊緣情況的消除方法,通常我們會使用條件語句來消除邊緣情況。你的測試使用的條件語句越少,你的程式碼就會有更好的 “taste” 。
下面,我將分享一個通過審查程式碼進行了改進的一個特殊例子。
這是一個關於初始化網格邊緣的演算法。
下面所寫的是一個用來初始化網格邊緣的演算法,網格 grid 以一個二維陣列表示:grid[行][列] 。
再次說明,這段程式碼的目的只是用來初始化位於 grid 邊緣的點的值,所以,只需要給最上方一行、最下方一行、最左邊一列以及最右邊一列賦值即可。
為了完成這件事,我通過迴圈遍歷 grid 中的每一個點,然後使用條件語句來測試該點是否位於邊緣。程式碼看起來就是下面這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
for (r = 0; r < GRID_SIZE; ++r) { for (c = 0; c < GRID_SIZE; ++c) { // Top Edge if (r == 0) grid[r][c] = 0; // Left Edge if (c == 0) grid[r][c] = 0; // Right Edge if (c == GRID_SIZE - 1) grid[r][c] = 0; // Bottom Edge if (r == GRID_SIZE - 1) grid[r][c] = 0; } } |
雖然這樣做是對的,但回過頭來看,這個結構存在一些問題。
- 複雜性 — 在雙層迴圈裡面使用 4 個條件語句似乎過於複雜。
- 高效性 — 假設
GRID_SIZE
的值為 64,那麼這個迴圈需要執行 4096 次,但需要進行賦值的只有位於邊緣的 256 個點。
用 Linus 的眼光來看,將會認為這段程式碼沒有 “good taste” 。
所以,我對上面的問題進行了一下思考。經過一番思考,我把複雜度減少為包含四個條件語句的單層 for
迴圈。雖然只是稍微改進了一下複雜性,但在效能上也有了極大的提高,因為它只是沿著邊緣的點進行了 256 次迴圈。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
for (i = 0; i < GRID_SIZE * 4; ++i) { // Top Edge if (i < GRID_SIZE) grid[0][i] = 0; // Right Edge else if (i < GRID_SIZE * 2) grid[i - GRID_SIZE][GRID_SIZE - 1] = 0; // Left Edge else if (i < GRID_SIZE * 3) grid[i - (GRID_SIZE * 2)][0] = 0; // Bottom Edge else grid[GRID_SIZE - 1][i - (GRID_SIZE * 3)] = 0; } |
的確是一個很大的提高。但是它看起來很醜,並不是易於閱讀理解的程式碼。基於這一點,我並不滿意。
我繼續思考,是否可以進一步改進呢?事實上,答案是 YES!最後,我想出了一個非常簡單且優雅的演算法,老實說,我不敢相信我會花了那麼長時間才發現這個演算法。
下面是這段程式碼的最後版本。它只有一層 for
迴圈並且沒有條件語句。另外。迴圈只執行了 64 次迭代,極大的改善了複雜性和高效性。
1 2 3 4 5 6 7 8 9 10 |
for (i = 0; i < GRID_SIZE; ++i) { // Top Edge grid[0][i] = 0; // Bottom Edge grid[GRID_SIZE - 1][i] = 0; // Left Edge grid[i][0] = 0; // Right Edge grid[i][GRID_SIZE - 1] = 0; } |
這段程式碼通過每次迴圈迭代來初始化四條邊緣上的點。它並不複雜,而且非常高效,易於閱讀。和原始的版本,甚至是第二個版本相比,都有天壤之別。
至此,我已經非常滿意了。
那麼,我是一個有 “good taste” 的開發者麼?
我覺得我是,但是這並不是因為我上面提供的這個例子,也不是因為我在這篇文章中沒有提到的其它程式碼……而是因為具有 “good taste” 的編碼工作遠非一段程式碼所能代表。Linus 自己也說他所提供的這段程式碼不足以表達他的觀點。
我明白 Linus 的意思,也明白那些具有 “good taste” 的程式設計師雖各有不同,但是他們都是會將他們之前開發的程式碼花費時間重構的人。他們明確界定了所開發的元件的邊界,以及是如何與其它元件之間的互動。他們試著確保每一樣工作都完美、優雅。
其結果就是類似於 Linus 的 “good taste” 的例子,或者像我的例子一樣,不過是千千萬萬個 “good taste”。
你會讓你的下個專案也具有這種 “good taste” 嗎?