我推薦的程式設計命名風格(C&C++)

之前批評了一番從前曾經流行、現在還有很多人在用的 匈牙利命名法, 後來也介紹了一些當前常見的命名方式, 但也許有些人對如何命名這件事還是感到迷盲, 這引起了我想要寫下我自己所使用的命名風格的想法,給人參考照路的同時也為我留個筆記。

其實命名這件事博大精深,甚至命名已經不是單純的命名學問, 還與程式設計架構、模式等思路息息相關,互相影響。 不是我不願意告訴你完整命名的知識,而是全部寫出來的話會是一個蠻大的主題, 還要拉上其他相關的程式模式框架介紹,我想足夠寫成一本書了! 因此我這次只很簡單的描述說明, 在一個識別名稱已經基本思考確定的情況下,我都如何修飾並表現它們的方式。

命名風格有關的變化上,很少有絕對的對與錯,更多的因素是人的「感覺」。 正因為如此,不同的命名規則比較和細部變化往往只能靠著不斷嘗試與累積來磨合, 所以我也沒辦法說什麼樣的東西是對的,只能告訴你說這是我所喜好的命名方式,您參考看看。

一般變數(local variable)

這裡說的一般變數,就是程式設計中我們會大量定義並使用的、在函式裡面的區域變數, 這種變數我絕大部分採用愈簡短愈好的名稱,搭配 Lower Snake Case 風格, 例如:bufdatasizefilenamelisten_port 等等。

對於一般的區域變數,由於通常擁有良好的上下文關係,加上現在都喜歡寫短函式, 在內容不長、變數不多的情況下,自然的變數名稱不需要搞得太複雜,簡單清爽就是最好!

一般函式(C external function)

C 的一般函式名大概是整個命名裡面最長最囉唆的了! 沒辦法,因為從大來看,整個模組化程式設計框架下都是以函式做為基本介面單元; 從小來看,各種不管靜態還是動態程式庫之間的符號連結也幾乎都是以函式為主, 函式的重要性不言而喻,函式同時也成為最容易發生名稱問題的地方!

對於 C 的外部函式(也就是一般的函式),我採用 Lower Snake Case, 並且函式名稱應具有下列格式結構:

程式庫名或模組名_[[父類別名_]類別名_]函式主名

其中若函式不屬於某個類別(意同 C++ 之 class),則類別名可以省略,父類別亦同; 至於程式庫名與模組名在絕大多數的情況下不能省略, 只有在這個函式本身屬於最頂層應用程式的函式時,才可以省略不寫。

範例:

  • ccntr_list_get_first:
    ccntr 這個程式庫下的 list 這個類別下的 get_first 函式。

  • ccntr_man_list_iter_get_value:
    ccntr 這個程式庫下的 man_list 這個類別下的 iter 這個類別下的 get_value 函式。

  • cts2_get_description:
    cts2 這個程式庫下的 get_description 函式。

我想熟悉 C++ 的人可能就看懂了,這不就是在仿效 C++ 的 class 和 name space 嗎? 沒錯!這些修飾就是在做與 C++ 的 class 和 namespace 相同的名稱空間分割, 把 C++ 設計概念的東西搬到 C 語言上來使用。

我所推薦的這個命名方式解決了有關名稱衝突、以及從屬關係不清的問題。 但是因為 C 語言沒有 C++ 完整的 namespace 相關機制, 只能把這些東西硬塞進函式名稱裡做為函式名稱的一部份,所以函式的名稱很容易變很長, 而這也是為什麼 C 語言的程式碼裡面超級喜歡縮寫的原因之一!

另外要注意的一點是:不是只有程式庫公開的函式才應該要採用這個命名, 凡會導出符號名稱到目的檔、靜態程式庫檔、或動態程式庫檔的函式, 也就是所有非 static 的一般 C 函式,都應該要採用這個命名規則!

私有函式(C static function)

C 的 static function 比照一般函式使用 Lower Snake Case 命名, 但因為其只能在所屬的程式碼檔案內被使用,也不會匯出符號, 所以基本上無需採用一般函式那樣囉唆的命名,直接將主名稱做為整個函式的名稱即可! 除非你的一份 C 程式檔案裡面含有兩個以上的類別或模組的程式碼!

函式參數

我的函式參數命名風格比照一般區域變數,採用 Lower Snake Case。 並且如果這個函式是所屬一個類別的函式的話, 做為物件實體參考的指標參數應該為函式的第一個參數,以與 C++ 的慣例相同, 最好這個參數的名稱要被命名為 self,以類比 C++ 的 this。 範例如:

void ccntr_list_link(ccntr_list_t *self, ccntr_list_node_t *pos, ccntr_list_node_t *node);

結構(structure/union)

由於聯合(union)和結構(structure)除了程式上的效果不一樣以外,在語法上的表現是相同的, 因此共用同一套命名規則,以下將以結構為主進行說明。

結構成員

結構成員與一般區域變數相似,通常結構體定義都擁有良好的群組分類關係, 所以成員名稱並不需要太複雜, 命名採簡潔、易懂用途為中心思想,搭配 Lower Snake Case 風格。

結構本身

結構本身命名採用 Lower Snake Case 風格,並需符合下列敘述的規則。

對定義在公開標頭檔的結構話,其名稱需符合下列格式結構:

程式庫名或模組名_結構主名

例如:

  • cts2_conf:
    表示它是 cts2 程式庫下的 conf 資料類型, 裡面可能放的都是與 cts2 程式庫有關的配置參數。

程式庫名前綴和模組名前綴可在下列幾種情況下省略:

  1. 這個結構被定義在私有標頭檔、或單一模組的程式碼檔裡。
  2. 這個結構被定義在函式內、或 C++ 的類別內、或 C++ 的 namespace 內時。

若這個結構被用作為某個類別型態使用的話,其結構主名即同類別名, 名稱表示與類別函式相似,但最後需加上 _t 結尾,整體名稱格式如下:

程式庫名或模組名_[父類別名_]類別名_t

例如:

  • ccntr_man_list_iter_t:
    該結構實際表達這是 ccntr 程式庫下的 man_list 類別下的 iter 類別。

結構別名(type define)

有時我們會喜歡用 typedef 給結構取別名,這通常是源自於 C 那煩瑣的變數宣告要求。 例如說我有一個結構叫 mytype,則宣告變數時我需要這樣寫:

struct mytype value;

此時,有些人會使用 typedef 為結構取別名:

typedef struct mytype mytype;

這樣,在宣告變數時就可以省略那個 struct

mytype value;

對於要不要給結構取別名這件事我還沒有確定的判斷。 目前的建議是: 如果你設計這個結構是希望用戶把他當一個黑盒子來用的話, 那就應該用別名的方式讓使用者省去那個 struct 字樣, 畢竟這時候,這個東西對於用戶來講就是一個不知是什麼東西的型態而已; 而另一方面,設計者應該也不希望用戶花太多注意力來關注這個型態到底內部是個什麼東西! 通常絕大部份的類別定義屬於這種條件,設計者通常不希望用戶直接操作其成員變數, 因此對於絕大部份被當類別使用的結構,我們應該為其設定別名

相反的, 如果這個結構的設計就是為了要叫使用者直接操作成員變數的話, 何不就大方的告訴大家這是個結構! 因此建議就比較傾向不設定別名的做法, 例如 C 標準函式庫、以及 POSIX API 定義的許多結構就採用這種做法。

巨集(macro)

所有的巨集定義都應在名稱前面加上程式庫或模組名稱, 並且應採用 Upper Snake Case 風格, 例如:

#define CCNTR_MAN_MAP_ENABLED

只有在單一程式碼檔案內被定義並被使用的巨集可以省略程式庫名稱前綴。

熟悉 C/C++ 的人都應該了解巨集的威力,不論是好的方面還是壞的方面, 因此長久以來程式設計圈就有一個不成文的共識,要求使用全大寫來命名所有的巨集。 對於熟悉西方語言的人會知道,大寫在文字中具有強調的效果, 而全大寫表示的文字一定是很重要或很特別的部份。

列舉(enumerate)

所有的列舉值都應在名稱前面加上程式庫或模組名稱, 並且應採用 Upper Snake Case 風格。 在模組名稱後面、最好再加上列舉用途名稱, 好在程式庫內定義了兩個以上的列舉型態時,可以方便的區分某個列舉值是屬於哪一種用途使用。 至於列舉型態本身,則比照結構(structure)的命名規則。 例如:

enum mylib_result_code
{
    MYLIB_RC_SUCCESS,
    MYLIB_RC_UNKNOWN_FAILURE,
    MYLIB_RC_CANNOT_ALLOC_MEMORY,
    MYLIB_RC_FILE_NOT_EXIST,
};

列舉值的程式庫名稱前綴、以及用途前綴只有在以下幾種情況下可以被省略:

  1. 該列舉在私有標頭檔、或單一程式碼檔案內被定義並被使用。
  2. 該列舉被定義在 C++ 的類別內、或 C++ 的 namespace 內。
  3. 該列舉是使用 C++11 的 enum class 來定義的。

對列舉值的命名要求比較嚴格有幾個原因:

  1. 傳統上的列舉值與結構或聯合等型態的成員不同,其可見域並不受列舉型態的約束。
  2. 列舉常被用來定義常數,而巨集也常被用來定義常數, 兩者間的用途有所重疊,其可見範圍不受約束的特性也有所相似,因此命名也就互相比照。

C++ 函式

C++ 函式我一般使用 Upper Camel Case 命名風格。 當名稱中出現一些平常會寫成大寫的縮寫字時,請把首字母以外的部份寫成小寫, 例如一個函式應被命名為 AesEncrypt 而不應被命名為 AESEncrypt

邏輯上的理由我自己也沒有整理出來,可能這就是一種喜好吧! 不知怎地,C++ 函式用 camel case 看起來就是比較搭配!

C 函式必須把很多從屬關係寫進名稱中,導致名稱較長, 如果不使用一些區隔性比較強烈的分隔符號來隔離單字的話,可能視覺上容易把他們混淆在一起吧。 C++ 因為擁有 namespace 和真正的 class 來做這些事情,所以不需要這些囉唆的前綴, 直接把函式主名當作全名即可。 至於 C++ 中不是類別成員的一般函式亦比照辦理, C++ 中的一般函式多半是私有函式,就算要放到標頭檔裡,也有 namespace 可以使用, 因此比較沒有名稱衝突和從屬關係不清的問題; 而且,若在同一個 C++ 程式碼檔案內,使用不同命名風格來命名函式的話,不會很不統一嗎?

C++ 函式參數

C++ 函式參數比照區域變數以及 C 函式參數,使用 Lower Snake Case 命名風格

C++ 類別(class)

C++ 類別採用 Upper Camel Case 命名風格,例如:StringFileList 等等。 一般情況下不需要額外的前綴後綴,需要的話通常使用 namespace 來處理即可。 當名稱中出現一些平常會寫成大寫的縮寫字時,請把首字母以外的部份寫成小寫, 例如一個類別應被命名為 AviReader 而不應被命名為 AVIReader

通常我無需考慮 C++ 類在程式庫間的相容性問題, 因為 C++ 那複雜又不統一的函式名稱變換(mangling)、類別生命週期操作、和例外傳遞機制的差異性, 已經使得一般情況的 C++ 類別(這裡不是指那些特別被設計來做為程式庫介面使用的純虛類別) 不可能被用作程式庫的介面。 我也不擔心類別名和變數名的衝突或混淆,因為所有的變數我都已經使用 lower case 命名了!

名稱空間(namespace)

老實說,這是我個人目前命名方案裡最後一塊未拼上的拼圖! 除了我不會使用帶底線的 Snake Case 風格外, 目前都有嘗試把 Lower Case 和 Upper Camel Case 用在 namespace 上, 尚未探索出最適合的命名方式。