編寫易於理解代碼的六種方式

 我學習編寫、改善和維護代碼的過程是很艱苦的。在過去的 12 年裏,我一直在編寫計算機遊戲並通過曾紅極一時的共享軟件技術進行網絡銷售,並以此爲生。這就是說,我常常要從空白的屏幕開始從頭編碼,當代碼達到數萬行之後才能拿去銷售。

這也就是說,如果我出了錯,我必須要自己去解決問題。當我在凌晨三點還在竭力尋找 bug 的時候,看着這些不知所云的晦澀代碼,我不禁自問:“我的天啊,這些垃圾代碼究竟是哪個笨傢伙寫的啊?”,很不幸,問題的答案是 “我”。

在學習了良好、正規的編碼技巧之後,我大受其益。本文就包括了其中的一些實踐。具有豐富經驗的資深程序員大都對這些內容爛熟於心。他們可以通過本文優美的散文式的介紹再重溫一遍,並可回顧一下在採取清晰編碼的理念之前,編碼是多麼地令人頭痛。

但更多的人會如同我一樣,是無意間跌跌絆絆地闖入編程領域的,而且沒有人爲其灌輸這些編程技巧和理念。本文所要介紹的這些內容對很多人來說也許很基礎,但對於其他人來說卻是極爲寶貴的資源,因爲之前沒有人告訴過他。所以,如果您不想走彎路,那麼本文將非常適合您。

示例

爲了便於解釋,本文全篇都將使用一個示例太空遊戲程序,稱爲 Kill Bad Aliens。在這個遊戲中,您將通過鍵盤來控制一個宇宙飛船,它可以在屏幕底端水平向前或向後移動,還可以向上發射子彈。


圖 1. 我們的假想遊戲
我們的假想遊戲 

遊戲發生在稱爲 Wave 的各個時間段。在每個 wave,外星人都會一個接一個地出現在屏幕頂端。它們到處飛,還會投擲炸彈。外星人將按固定時間間隔出現。在殺死一定數量的外星人之後,一個 Wave 就告結束。

殺死一個外星人會給您加分。當結束一個 wave 時,根據您完成遊戲所需時間的長短還會有額外的分數獎勵。

如果被炸彈擊中,您的當前飛船就會炸燬,另一個飛船繼而出現。如果被炸燬超過三次以上,遊戲就結束了。如果您的得分很高,就會被晉級爲 “人”,如果分數很低,就不能。

現在,我們可以坐下來開始用 C++ 編寫這個 Kill Bad Alients 遊戲了。首先定義幾個對象來分別代表飛船、玩家的子彈、敵人和敵人的子彈。然後再編寫代碼來繪製這些對象。還需要編寫代碼來讓這些對象可以隨着時間的推移而到處移動。另外,也需要編寫遊戲邏輯、外星人 AI 以及能感知用戶擊鍵用意的代碼等等。

那麼,我們該如何實現這些以便當遊戲編制完畢後,代碼易懂、易維護,最起碼地,不會一團糟呢?

 

提示 1:經常註釋

請經常爲代碼添加註釋。假設您編寫了一個過程,但沒有爲它做註釋,幾個月後,您再回過頭來想對它進行一些修整(您絕對會這麼做),將需要花費很多時間去研讀這些代碼,原因就是因爲您之前沒有做註釋。而時間是您最爲寶貴的資源。丟失的時間是永遠也找不回來的。

但註釋和其他事情一樣也是需要技巧的。只要多練習,在這方面的技能就會不斷提高。註釋有好有壞。

最好不要將註釋寫得過長。假設爲一個函數做了註釋,而這個註釋在將來可以節省您理解代碼所需的時間,比如說 10 分鐘。這很好。現在假設所編寫的註釋過長,您花了 5 分鐘編寫這個註釋,之後還要再花 5 分鐘讀懂這個註釋。這樣一來,實際上沒有節省任何時間。這不是一種很好的做法。

當然,也不要將註釋寫得過短。如果在一兩頁之長的代碼中找不到任何註釋,那麼這段代碼最好清晰得 “晶瑩剔透”,否則將來研讀所需的時間將會很長。

再有,註釋的方式不能太死板。當剛剛開始編寫註釋時,人們往往會頭腦一熱,寫下這樣的註釋:

// Now we increase Number_Aliens_on_screen by one.
Number_Aliens_on_screen = Number_Aliens_on_screen + 1;

 

這麼明顯的東西顯然不需要註釋。如果代碼非常混亂以致於需要逐行註釋,那麼更有利的方式是首先簡化代碼。在這種情況下,註釋並不能節省時間,反倒會消耗時間。因爲註釋需要時間去研讀,而且它們分佈於屏幕上的實際代碼中的不同位置,所以在顯示器上一次只能看少許的註釋。

此外,千萬不要這麼寫註釋:

Short get_current_score()
{
	[insert a whole bunch of code here.]

	return [some value];

	// Now we're done.
}

 

“We're done” 這樣的註釋有何用處呢?真是感謝您讓我知曉。註釋下面的這個大括號以及其後跟隨的大片空白難道還不足以讓我明白這是一段代碼的結束麼?同樣,在返回語句之前也不需要使用類似 “Now we return a value” 這樣的註釋。

那麼,如果您正在編寫代碼,而又沒有上司或公司的規定可以做指導,這時,又該如何註釋呢?我的做法是:對於由我自己維護的代碼,我會寫一個簡介。這樣一來,當我返回一個我很久以前編寫的過程時,我就可以查看對它的解釋。一旦我瞭解了其工作原理之後,我就可以很容易地理解實際的編碼了。這通常會涉及:

  1. 過程/函數之前寫幾句話,說明其功能。
  2. 對傳遞給它的數值的一個描述。
  3. 如果是函數,對其返回結果的一個描述。
  4. 在過程/函數內部,能將代碼分解爲更短小的任務的註釋。
  5. 對於看起來有些難懂的大塊代碼,對其成因給與簡短的解釋。

總之,我們需要在開始時給出一個描述,然後再在整個代碼內部的幾個位置加以註釋。這種做法需時不多,但卻可在將來節省大量的時間。

如下所示是另一個取自假想的 Kill Bad Alients 遊戲的例子。考慮代表玩家子彈的那個對象。需要頻繁地調用函數來將其向上移動以便檢查該子彈是否會擊中任何目標。我可能會按如下所示編寫實現這個功能的代碼:

// This procedure moves the bullet upwards. It's called
//NUM_BULLET_MOVES_PER_SECOND times per second. It returns TRUE if the
//bullet is to be erased (because it hit a target or the top of the screen) and FALSE
//otherwise.
Boolean player_bullet::move_it()
{
	Boolean is_destroyed = FALSE;

	// Calculate the bullet's new position.

	[Small chunk of code.]

	// See if an enemy is in the new position. If so, call enemy destruction call and
	// set is_destroyed to TRUE

	[small chunk of code]

	// See if bullet hits top of screen. If so, set is_destroyed to TRUE

	[Small chunk of code.]

	// Change bullet's position.

	[Small chunk of code.]

	Return is_destroyed;
}

 

如果代碼足夠清晰,如上所示的註釋應該就已經足夠。對像我這樣需要不時地返回這個函數來修復錯誤的人來說,這將能夠節省大量時間。

 

提示 2:大量使用 #define。沒錯,是要大量使用。

假設,在我們這個假想的遊戲中,希望玩家在射中一個外星人時即可獲得 10 分。有兩種方法可以實現這個目的。如下所示的是其中一個比較糟糕的做法:

// We shot an alien.
Give_player_some_points(10);

	This is the good way: In some global file, do this:

#define		POINT_VALUE_FOR_ALIEN	10

 

之後,當我們需要給出一些分數時,我們很自然地會這麼寫:

// We shot an alien.
Give_player_some_points(POINT_VALUE_FOR_ALIEN);

 

在某種程度上,大多數程序員都知道該這麼做,但是需要遵守一定之規,才能將其做好。比如,每次在定義常數時,都需要考慮在某個中心位置對其進行定義。假設,要將玩遊戲的區域設置成 800 * 600 像素,請務必這麼做:

#define PIXEL_WIDTH_OF_PLAY_AREA	800
#define PIXEL_HEIGHT_OF_PLAY_AREA	600

 

如果,在某個日期,又想更改遊戲窗口的大小了(您很可能需要這麼做),若在此處就能更改數值將會節省您雙倍的時間。這是因爲:第一,無需在全部代碼中查找所有提到遊戲窗口是 800 像素寬的地方(800!我當時是怎麼想的?)第二,無需總要修復那些由於漏掉了引用而引起的無法避免的 bug。

當我製作 Kill Bad Aliens 遊戲時,我要決定需要殺掉多少外星人一個 wave 纔算結束、屏幕上一次能有多少外星人、這些外星人又以多快的速度出現。例如,如果我想讓每個 wave 中的外星人的人數相同,並且他們都以相同的速度出現,我可能會編寫如下所示的代碼:

#define		NUM_Aliens_TO_KILL_TO_END_WAVE	20
#define		MAX_Aliens_ON_SCREEN_AT_ONCE		5
#define		SECONDS_BETWEEN_NEW_Aliens_APPEARING	3

 

這段代碼很清晰。此後,若我覺得這個 wave 太短或外星人相繼出現的時間間隔過短,我就可以立即調整相應的值並立即讓遊戲重新生效。

如此設置遊戲值的一個妙處是能快速地做出更改,這種立竿見影的施控感覺實在是很好。比如,如果將上述代碼改寫成如下所示:

#define		NUM_Aliens_TO_KILL_TO_END_WAVE	20
#define		MAX_Aliens_ON_SCREEN_AT_ONCE		100
#define		SECONDS_BETWEEN_NEW_Aliens_APPEARING	1

 

那麼,您就無法享受上述的快感和興奮了。


圖 2. 處理常量之前的 Kill Bad Aliens
處理常量之前的 Kill Bad Aliens 

圖 3. 處理常量之後的 Kill Bad Aliens(這樣的遊戲可能不夠好,但卻十分有趣,可供參考) 
處理常量之後的 Kill Bad Aliens,這樣的遊戲可能不夠好,但卻十分有趣,可供參考。 

順便說一下,您可能已經注意到,我沒有爲上述值做任何註釋,這是因爲從變量名可以很明顯地看出這些值的意義。這正是接下來我要討論的內容。

 

提示 3:不要使用弄巧成拙的變量名。

總的目標很簡單:編寫代碼以便讓那些不知道其用意的人能讀懂,讓知道其用意的人能儘快地理解。

實現這一目標最好的策略是爲變量、過程等賦以含義鮮明的名字。當他人看到這個變量名時,就會立刻清楚其意義,您也不必搜索整個程序來尋找 incremeter_side_blerfm 的用意何在,這大約會節省五分鐘左右的時間。

這裏需要進行一些均衡。所給出的命名應該儘量長且足夠清晰以便您能理解其含義,但也不能過長或太過怪異,如果這樣,代碼的可讀性就會受到影響。

例如,在實際中,我可能不會像上一節所示的那樣給常量命名。我之前之所以這麼做是爲了讓讀者在沒有任何上下文的情況下也能充分理解這些常量的含義。在程序本身的上下文中,與如下所示的相比:

#define		MAX_Aliens_ON_SCREEN_AT_ONCE		5

 

我會毫不猶豫地這樣編碼:

#define		MAX_NUM_Aliens		5

 

這個簡短的名字所引起的疑惑很快就會迎刃而解,而簡短的命名還會增加代碼的可讀性。

現在來看看在本文中我經常要調用的那個用來將外星人在屏幕上到處移動的代碼片段,我會毫不猶豫地這樣編碼:

// move all the Aliens
for (short i  = 0; I < MAX_NUM_Aliens; i++)
	if (Aliens[i].exists()) // this alien currently exist?
		Aliens[i].move_it();

 

請注意,包含所有外星人的這個數組的名稱很簡單,叫做 Aliens。這很棒。它恰好就是我想要的那種描述性名稱,這個名稱又很簡短,即使鍵入千遍之多,我也不會感到煩悶。此數組將會經常用到。如果將其命名爲類似 all_Aliens_currently_on_screen 這樣的名稱,那麼所編寫的最終代碼將會長出很多,而且代碼還會因此變得不怎麼清晰。

同樣,我還將循環變量直接命名爲 i,無任何額外的說明。若是初次接觸描述性變量名這個概念,您很可能會忍不住將此循環變量命名爲 "counter" 之類的名字。實際上,沒有必要這麼做。命名變量的意義在於讓讀者能夠立即理解該變量的用意。人人都知道 "i"、"j" 這類名稱常常用於循環變量,所以將循環變量如此命名是完全可以的,無需多加解釋和說明。

當然,有關變量命名還是需要多加註意。比如,有一種稱爲 Hungarian Notation 的東西。其種類很多,但基本的理念是在變量名的開始添加一個標記以表示其類型(例如,所有無符號長型變量都以 ul 開頭)。這比我希望的要多少麻煩一些,但這個概念必須要了解。爲了弄清楚事情可能需要花費太多時間,但還是值得的。

 

提示 4:進行錯誤檢查。

一個正常大小的程序往往都會有大量的函數和過程。而且更爲麻煩的是,其中的每一個都需要進行稍許錯誤檢查。

當創建過程/函數時,應該總要考慮這樣的一個問題:“假如一些懷有惡意的人故意向函數或過程傳遞進各種怪異的值,這段剛剛創建的代碼如何能自保並且讓計算機也能免受破壞呢?”然後,編寫代碼來檢查這些惡意數據以保護自身免受這些數據的破壞。

舉個例子。我們的這個太空遊戲的主要目標是殺掉外星人並積分,所以我們需要一個過程來更改分數。而且,當加分時,我們需要調用一個例程來實現分數上星光閃爍的效果。如下所示的是第一個過程:

Void change_score(short num_points)
{
	score += num_points;

make_sparkles_on_score();
}

 

到目前爲止還不錯。現在請思考一下:這裏可能出現的錯誤是什麼呢?

首先,一個很明顯的問題是:如果 num_points 是負值該如何呢?我們能讓玩家的分數降低麼?就算我們能降低分數,但在我之前給出的關於該遊戲的描述中,沒有提到過失分。而且,遊戲應該有趣,但失分無論如何不能算是一個有趣的事情。所以,我們將分數負值視爲一個錯誤並必須要捕獲。

上述錯誤相對容易,但這裏有一個很微妙的問題(也是我在遊戲中經常要處理的)。如果 num_points 爲零又會怎麼樣呢?

這是一個很似是而非的情景。還記得麼,我們會在每個 wave 結束時根據玩家完成速度的快慢給一個獎勵分數。如果玩家速度極慢,我們是否應該給他一個值爲零的獎勵分數呢?在凌晨三點,調用 change_score 並傳遞值 0,這完全可行。

現在的問題是我們可能不想讓計分板在顯示的數值沒有變化時仍舊五顏六色地閃個不停。所以我們要先捕獲這個問題。讓我們嘗試如下代碼:

Void change_score(short num_points)
{
	if (num_points < 0)
{
// maybe some error message
		return;
}

	score += num_points;

	if (num_points > 0)
make_sparkles_on_score();
}

 

好了,情況好多了。

請注意這是很簡單的一個函數。裏面並沒有用到任何極受新手推崇的新奇指針。如果要傳遞數組或指針,那麼最好小心錯誤和壞數據的出現。

這樣做的好處並不僅僅限於讓程序免遭破壞。好的錯誤檢查還能讓調試更爲迅速。假設,您知道寫入的數據超出了數組的範圍,爲了發現可能出現的錯誤,您需要詳細檢查代碼。若所查看的這個過程中的錯誤檢查均已就緒,那麼就無需花很多時間去專門通查它來尋找錯誤。

這種做法將節省大量時間,而且還能重複。還是那句話,時間是我們所擁有的寶貴資源。

 

提示 5:“不成熟的優化是麻煩的根源” —— Donald Knuth

上述格言非我個人所造,它可以在 Wikipedia 中找到,所以必定是十分睿智的。

除非是想找別人麻煩,否則編寫代碼的首要目標就是簡明性。簡單的代碼更易於編寫、易於日後理解,也更易於調試。

優化與簡明性是相悖的。但有時,卻必須要進行優化,在遊戲中尤其如此。這個問題至關重要,您可能直到用解析器實際對工作代碼進行測試時纔會意識到需要進行優化。(解析器 是一種程序,用來監視其他程序並找出該程序使用不同的調用所花費的時間。這些都是很棒的程序。您可以找一個來試試。)

每次當我優化遊戲時,常常都禁不住會大出所料。我十分擔心的那些代碼總是問題不大,相反,我覺得萬無一失的代碼反倒會運行得十分緩慢。由於我對運行速度的快慢並沒有什麼概念,在獲得實際數據之前我所進行的優化根本就是浪費時間。比浪費時間更糟糕的是它還讓代碼變得有些混亂。

這個規則看來很難遵守。但,如果規則很容易,它也就稱不上規則了。好的程序員大都更痛恨將原本可以運行迅速的代碼弄得臃腫笨拙。

但好消息是,在我不斷 “該這樣不該那樣的” 佈道式的介紹中, 這是惟一的一個您可以稍微懈怠一些的地方!

請讓自己編寫的代碼儘量整潔和有效一些吧。在後面的優化階段,可能需要將其變得面目全非。所以如非必要,請慎重。

說到傷害,接下來,就來看看最後的這條建議。

 

提示 6:不要一知半解、自作聰明。

您可能聽說過 IOCCC 吧,即 International Obfuscated C Code Contest。大家都知道,C 和 C++,不管其優勢如何卓越,都會最終導致編寫的代碼噩夢般地複雜。這個比賽就是要通過評選出最離譜的代碼來展示簡明代碼的價值,真是別具匠心。

讓我們來看看在您自認爲具有了編程的全部知識並甘願冒險的情況下,您能製造什麼樣的麻煩。足夠的知識讓您信心百倍地將十行代碼壓縮進一行代碼內。付出的代價就是您絕對無法快速修復其中可能存在的 bug。

這裏所需吸取的教訓就是如果您所編寫的代碼要求您必須具有有關複雜優先規則的詳細知識或讓您不得不翻看某些書的後面章節才能弄清來龍去脈,那麼您在編寫這段代碼時就犯了一知半解、自作聰明的毛病了。

每個人對代碼的複雜性都有自己的容忍程度。就我個人而言,我編寫的程序往往呈比較典型的保守風格。我個人認爲,如果一段 C 代碼需要您必須知道 i++ 和 ++i 之間的差別,那麼這段代碼就過於複雜了。

您儘可以把我想象成一個循規蹈矩的人。沒錯,我的確如此。但循規蹈矩卻可以讓我花很少的時間就可以讀懂我的代碼。

 

結束語

至此,您可能會想:“哇哦,真是浪費時間。您介紹的所有這些東西都是顯而易見,盡人皆知的。爲何還多此一舉,寫這樣的文章呢?” 實際上,我很希望您會這麼想,因爲這意味着您已經進步了,變得明智了。這很好。

但不要錯認爲所有這些內容對每個人都是不言自明的。事實並非如此。糟糕的代碼隨處可見,但實際上這些代碼本不應如此。

如果您正在努力編寫大量代碼並想讓自己不受其所累。那麼就請讓代碼儘量簡單明瞭一些,這樣,您就可以節省大量時間和免受很多挫折。

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