如果談到C++與C有什麼不同,必然會談到C++提供了C沒有的名稱空間(Name space),而對現今許多語言來說,名稱空間會是基本語法元素之一,以便開發者避免名稱衝突的相關問題。既然名稱空間的需求存在如此之久,C語言標準上卻始終沒有名稱空間上的規範,因此,實際遇上問題時的解決方案,會是什麼呢?
既有的名稱空間
談到沒有名稱空間的語言,JavaScript其實也是其中之一,然而,聰明的開發者已經會使用各種方式,實現了各種不同的名稱空間風格,以符合各式各樣的需求,而在ECMAScript 6中,也為名稱空間的實現進行了標準化,最近在回顧C語言,腦袋中想的是,對於同樣沒有名稱空間語法的C語言來說,必然也有著其他實現名稱空間的方式吧!
何謂名稱空間?維基百科上寫到:「表示識別字(identifier)的可見範圍。」從這個觀點出發,實際上C當然具備名稱空間,開發者可以輕易地在C99規格書中,搜尋到name space字眼。
而在〈6.2.3 Name spaces of identifiers〉的說明中,也特別指出:識別字可以歸類到四個不同的名稱空間:(1)標籤(Label);(2)struct、union、enum定義;(3)struct、union成員;(4)其他識別字(像是變數、函數、typedef名稱的宣告等)。
有個不錯的例子,可以一次展現這幾個名稱空間,在P.J Plauger的《The Standard C Library》練習題中,要求寫出一個包含x: ((struct x*)x)->x=x(5);的陳述句,而且必須是能成功編譯且正確執行的C語言,如果試著將程式實現出來的話,五個x名稱從左而右,分別代表的是標籤(Label)、struct定義、變數、struct成員與巨集定義(巨集是前置處理指令,因而不在C99規格書6.2.3的分類之中)。
不過,這與提供有名稱空間語法的語言中,提到的名稱空間機制還是不同。在那些語言中,名稱空間是可以讓名稱進一步擁有全名(Fully qualified name),以避免程式碼中名稱衝突的方式。
在這類語言中,名稱空間通常也會是專案組織的一部份,不同的名稱空間,可能代表著不同程式碼檔案位於對應名稱的資料夾,以便避免實體檔案上的名稱衝突,而且還可以管理某個名稱空間下的識別字能見度,限定某些識別字只有在同一個名稱空間之程式可以使用,而哪些可以開放給其他模組使用。
對C語言來說,後者並不是問題,在稍微進階的C語言文件或書籍中,都會談及如何將程式進行模組化,這可在不同資料夾中,利用不同的標頭檔(header file)與C原始碼檔案實現,以便將相關的功能組織為各個模組,也可以進一步利用static來限定識別字的能見度。
讓一個名稱擁有全名
問題在於,如何能夠讓一個名稱擁有全名?在JavaScript這類動態性較高,但本身缺乏名稱空間的語言,是藉由將名稱「附著」在某個物件上;就算是在Java中,類別也常會被當成某些作為static成員的名稱空間,因此在C語言中可以對照聯想到的是,有沒有辦法利用C語言既有的名稱空間之一,讓struct名稱作為全名的前置,而相關名稱成為struct成員。
這確實是C語言中,名稱空間常見的實現方式之一。在〈Namespaces in C〉這篇文件中,就示範了如何運用struct與函式指標,讓create、add_star等函式成為struct成員:
static const struct { GALAXY *(* create)(void); void (* destroy)(GALAXY *galaxy); void (* add_star)(GALAXY *galaxy, STAR *star); } galaxy = { galaxy__create, galaxy__destroy, galaxy__add_star };
這麼一來,就可以透過galaxy,達到GALAXY *g = galaxy.create()、galaxy.add_star(g, s)這類的呼叫方式,就像是函式擁有了全名一樣,然而,這樣的方式會使得程式實作上變得複雜,為了定義充當名稱空間的struct實例,必須寫一堆標頭檔,而且顯然地,GALAXY這個型態無法置入galaxy這個名稱空間,而不是有個型態叫做galaxy.GALAXY。
另一個讓名稱擁有全名的方式,是直接在名稱上使用前置(Prefix),例如,GTK的函式都會使用gtk_前置名稱作為開頭,像是gtk_application_new、gtk_window_set_title這樣的名稱,而型態本身,也會使用Gtk作為前置,像是GtkApplication、GtkWidget這類名稱。前置名稱透過適當的慣例規範,也可以表明能見度,例如在Subversion中,svn_fs_initialize表示一個公開API,如果名稱上有兩個底線,例如svn_fs_base__dag_get_node,就表示這是一個模組內部使用的名稱。
上述這樣的方式,更廣為許多C語言開發者接受。而基於C的Objective-C也沒有名稱空間,取而代之的是一堆名稱規範,例如NSWindow、CAAnimation這樣的名稱,NS通常作為基礎程式庫的名稱前置(據稱是Steve Jobs當年離開Apple自立門戶時,自創的NextStep之縮寫),CA作為核心動畫(Core Animation)的前置等。
為什麼C語言不加入名稱空間?
C語言從ANSI C以來,一直都沒有太多變化,然而名稱空間的需求一直存在,令人不禁思考,為何在後來的標準中,始終未能加入名稱空間的規範?
在一些文件中指出,許多語言與作業系統依賴在現有的C標準上,相容性會是一大考量。就算基於C語言建立的Objective-C,也曾多次考慮是否加入名稱空間機制,然而,想要保持與C(以及C++)的相容性的話,將會因此困難重重而作罷。
不過,另一個可能是最主要的原因是,名稱空間會使得C語言複雜化,也沒有辦法完全解決名稱衝突的問題。就像有些語言雖然具有名稱空間的機制,然而,許多開發者的運用方式,卻經常與名稱空間之目的背道而馳,而且,就算是在具有namespace的C++中,也經常看到的告誡就是:儘量不要使用using namespace std。
在〈How to avoid namespace collision in C and C++〉中有個回應:「處理這個問題的正確方式是,設定好建構環境,讓名稱衝突不容易發生。」這讓我想起最近重溫C語言時自我設定的目標之一:「適當地規畫.h、.c,讓程式碼更易於理解與閱讀」,而不是讓程式碼在標頭檔與實現之間,毫無章法地隨意置放。
名稱空間管理是一種態度
仔細想想,有名稱空間的語言,確實,開發者不見得就能完善地處理名稱規畫,避免衝突發生;類似地,就算語言在語法上支援物件導向,開發者也不見得能確切落實物件導向,然而只要有心,C語言也可以進行物件導向,就像〈你所不知道的C語言:物件導向程式設計篇〉中,一開始寫到的:「物件導向是一種態度」。
在〈Deep C〉這份簡報的第246頁中指出,C語言的精神有許多面向,然而,整個社群的氛圍中有幾個原則,其中與名稱空間(或者是模組化、物件導向)相關的幾條應該是:信任程式設計者、保持語言小而簡單、維持概念的簡單性、不阻止程式設計者做必要之事。
眾所皆知地,C語言中有許多其他語言不想讓開發者碰觸的東西,因此相對地,使用C語言的態度,就是要成為一個承擔得起信任的開發者。因此,適當地規畫.h、.c,讓程式碼更易於理解與閱讀,讓模組化的概念得以落實是一種態度,而該採取struct或者是名稱前置,使用的程式庫採用了何種命名慣例,以實現相關的名稱空間機制,也應當以認真的態度來進行思考與面對。