數據再抽象

首先看下面這三個結構體及其 create函數:

struct point {

double x; double y;};

struct rectangle {

double width; double height;};struct circle { struct point *center; double radius;};struct chain_node_shape { struct rectangle *body;

struct circle *holes[2] ;

};struct point *create_point(double x, double y){ struct point *ret = malloc(sizeof(struct point)); ret->x = x; ret->y = y; return ret;}

struct circle *

create_circle(struct point *center, double radius){ struct circle *ret = malloc(sizeof(struct circle)); ret->center = center; ret->radius = radius; return ret;}struct rectangle *create_rectangle(double w, double h)

{

struct rectangle *ret = malloc(sizeof(struct rectangle)); ret->width = w; ret->height = h; return ret;}struct chain_node_shape *create_chain_node_shape(struct circle *c1, struct circle *c2, struct rectangle *rect)

{

struct chain_node_shape *ret = malloc(sizeof(struct chain_node_shape)); ret->body = rect; ret->holes[0] = c1; ret->holes[1] = c2; return ret;}

顯然,這些代碼長的太像了!那四個結構體都是存儲兩個成員的結構體,而相應的 create函數也無非是將函數所接受的參數保存到結構體成員中。有沒有辦法用很少的代碼來表示它們?有!

既然每個結構體都保存 2 個成員,那麼我們就先將上述代碼刪掉,然後定義一個 pair 類型的結構體:

struct pair {

void *first;

void *second;

};

在 pair 結構體中,我們用了兩個 void * 指針,只有如此我們方能很自信的說 pair 可以存儲任意類型的兩個數據。接下來,只需修改 create_chain_node 函數的定義:

struct chain_node *

create_chain_node(void){ double *left_x = malloc(sizeof(double)); double *left_y = malloc(sizeof(double)); *left_x = 1.0; *left_y = 1.0; struct pair *left_center = malloc(sizeof(struct pair));

left_center->first = left_x;

left_center->second = left_y; double *left_radius = malloc(sizeof(double)); *left_radius = 0.5; struct pair *left_hole = malloc(sizeof(struct pair)); left_hole->first = left_center; left_hole->second = left_radius; double *right_x = malloc(sizeof(double)); double *right_y = malloc(sizeof(double)); *right_x = 9.0;

*right_y = 1.0;

struct pair *right_center = malloc(sizeof(struct pair)); right_center->first = right_x; right_center->second = right_y; double *right_radius = malloc(sizeof(double)); *right_radius = 0.5; struct pair *right_hole = malloc(sizeof(struct pair)); right_hole->first = right_center; right_hole->second = right_radius; struct pair *holes = malloc(sizeof(struct pair));

holes->first = left_hole;

holes->second = right_hole; struct pair *body = malloc(sizeof(struct pair)); double *width = malloc(sizeof(double)); *width = 10.0; double *height = malloc(sizeof(double)); *height = 2.0; body->first = width; body->second = height; struct pair *shape = malloc(sizeof(struct pair)); shape->first = body; shape->second = holes; struct chain_node *ret = malloc(sizeof(struct chain_node)); ret->prev = NULL; ret->next = NULL; ret->shape = shape; return ret;}

我勇敢的承認這個基於 struct pair 的 create_chain_node 函數太醜陋了,但是我們總算是消除了大量的結構體及其構造函數了,而且整體代碼量減少了大約 1/6。

仔細觀察上述代碼,顯然下面的三段代碼存在著高度的重複:

double *left_x = malloc(sizeof(double));

double *left_y = malloc(sizeof(double)); *left_x = 1.0; *left_y = 1.0; struct pair *left_center = malloc(sizeof(struct pair)); left_center->first = left_x; left_center->second = left_y; double *right_x = malloc(sizeof(double)); double *right_y = malloc(sizeof(double)); *right_x = 9.0; *right_y = 1.0; struct pair *right_center = malloc(sizeof(struct pair)); right_center->first = right_x; right_center->second = right_y; struct pair *body = malloc(sizeof(struct pair)); double *width = malloc(sizeof(double)); *width = 10.0; double *height = malloc(sizeof(double)); *height = 2.0; body->first = width; body->second = height;

這三段代碼都在向 pair 結構體中存入兩個 double * 類型的數據。既然如此,我們可以專門寫一個函數,讓它生成面向 double * 的 pair 結構體,即:

struct pair *

pair_for_double_type(double x, double y){ struct pair *ret = malloc(sizeof(struct pair)); double *first = malloc(sizeof(double)); double *second = malloc(sizeof(double)); *first = x; *second = y; ret->first = first; ret->second = first; return ret;}

然後再次重構 create_chain_node 函數:

struct chain_node *

create_chain_node(void){ struct pair *left_center = pair_for_double_type(1.0, 1.0); double *left_radius = malloc(sizeof(double)); *left_radius = 0.5; struct pair *left_hole = malloc(sizeof(struct pair)); left_hole->first = left_center; left_hole->second = left_radius; struct pair *right_center = pair_for_double_type(9.0, 1.0); double *right_radius = malloc(sizeof(double)); *right_radius = 0.5; struct pair *right_hole = malloc(sizeof(struct pair)); right_hole->first = right_center; right_hole->second = right_radius; struct pair *holes = malloc(sizeof(struct pair)); holes->first = left_hole; holes->second = right_hole; struct pair *body = pair_for_double_type(10.0, 1.0); struct pair *shape = malloc(sizeof(struct pair)); shape->first = body; shape->second = holes; struct chain_node *ret = malloc(sizeof(struct chain_node)); ret->prev = NULL; ret->next = NULL; ret->shape = shape; return ret;}

山重水複疑無路

經過再次重構後的 create_chain_node 看上去要好了一些,但是依然有兩段代碼存在高度重複:

struct pair *left_center = pair_for_double_type(1.0, 1.0);

double *left_radius = malloc(sizeof(double)); *left_radius = 0.5; struct pair *left_hole = malloc(sizeof(struct pair)); left_hole->first = left_center; left_hole->second = left_radius; struct pair *right_center = pair_for_double_type(9.0, 1.0); double *right_radius = malloc(sizeof(double)); *right_radius = 0.5; struct pair *right_hole = malloc(sizeof(struct pair)); right_hole->first = right_center; right_hole->second = right_radius;

但是僅從 pair 結果體層面已經無法對這兩段代碼進行簡化了,而且我又非常不想寫一個像下面這樣的輔助函數:

struct pair *

create_hole(struct pair *center, double radius){ struct pair *ret = malloc(sizeof(struct pair)); double *r = malloc(sizeof(double)); *r = radius; ret->first = center; ret->second = r; return ret;}

雖然 create_hole 能夠將上述兩段重複的代碼簡化為:

struct pair *left_center = pair_for_double_type(1.0, 1.0);

struct pair *left_hole = create_hole(left_center, 0.5); struct pair *right_center = pair_for_double_type(9.0, 1.0); struct pair *right_hole = create_hole(right_center, 0.5);

但是與 pair_for_double_type 函數相比,create_hole 這個函數的應用範圍非常狹小。由於 pair_for_double_type 函數可以將兩個 double 類型的數據存儲到 pair 結構體中,在我們的例子中創建二維點與矩形可以用到它,在科學計算中創建極坐標、複數以及所有的二次曲線方程式也都都能用到它,但是 create_hole 卻只能在創建車鏈這件事上有點用處。也就是說,正是因為 pair_for_double_type 函數所取得的成功,導致我們認為 create_hole 的品味太低。我們應該想一想還有沒有其他途徑可以消除上述代碼的重複。

仔細分析 left_hole 與 right_hole 的構造過程,不難發現 hole 的 center 與 radius 這兩種數據的類型不一致是造成我們難以對上述重複的代碼進行有效簡化的主要原因,create_hole 之所以能夠對上述重複的代碼進行大幅簡化,是因為它根據我們的問題構造了一個特殊的 pair 結構體——姑且稱之為 X,這個結構體的特殊之處在於其 first 指針存儲的是一個面向 double * 的同構類型的 pair 結構體,其 second 指針則存儲了一個 double 類型數據的基地址。正是因為 X 的結構太特殊了,所以導致 create_hole 這種抽象的應用範圍過於狹隘,以至於現實中只有圓形比較符合這種結構體。

既然是異構的 pair,而我們已經實現了一個可以創建存儲 double 類型數據的 pair 的函數 pair_for_double_type,這個函數的結果是可以直接存入異構 pair 中的。現在我們缺少只是一個可以將 double 值轉化為可直接存入異構 pair 的函數,即:

double *

malloc_double(double x){ double *ret = malloc(sizeof(double)); *ret = x; return ret;}

有了這個函數,就可以對 create_chain_node 繼續進行簡化了:

struct chain_node *

create_chain_node(void){ struct pair *left_hole = malloc(sizeof(struct pair)); left_hole->first = pair_for_double_type(1.0, 1.0);; left_hole->second = malloc_double(0.5); struct pair *right_hole = malloc(sizeof(struct pair)); right_hole->first = pair_for_double_type(9.0, 1.0);; right_hole->second = malloc_double(0.5); struct pair *holes = malloc(sizeof(struct pair)); holes->first = left_hole; holes->second = right_hole; struct pair *body = pair_for_double_type(10.0, 1.0); struct pair *shape = malloc(sizeof(struct pair)); shape->first = body; shape->second = holes; struct chain_node *ret = malloc(sizeof(struct chain_node)); ret->prev = NULL; ret->next = NULL; ret->shape = shape; return ret;}

而且,基於 malloc_double 函數,還能對 pair_for_double_type 函數進行簡化:

struct pair *

pair_for_double_type(double x, double y){ struct pair *ret = malloc(sizeof(struct pair)); ret->first = malloc_double(x); ret->second = malloc_double(y); return ret;}

事實上,如果我們再有一個這樣的函數:

struct pair *

pair(void *x, void *y){ struct pair *ret = malloc(sizeof(struct pair)); ret->first = x; ret->second = y; return ret;}

還能對 reate_chain_node 再做一步簡化:

struct chain_node *

create_chain_node(void){ struct pair *left_hole = pair(pair_for_double_type(1.0, 1.0), malloc_double(0.5)); struct pair *right_hole = pair(pair_for_double_type(9.0, 1.0), malloc_double(0.5)); struct pair *holes = pair(left_hole, right_hole); struct pair *body = pair_for_double_type(10.0, 1.0); struct pair *shape = pair(body, holes); struct chain_node *ret = malloc(sizeof(struct chain_node)); ret->prev = NULL; ret->next = NULL; ret->shape = shape; return ret;}

看到了吧,只要略微換個角度,很多看似難以簡化的代碼都能得以簡化。這個簡化的過程一直是在指針的幫助下進行的,但事實上,當你的注意力一直集中在怎麼對代碼進行簡化時,指針的使用簡直就是本能一樣的存在,以至於你覺得你並沒有藉助指針的任何力量,完全是你自己的邏輯在指導著你的行為。在這個過程中,無論是面向對象還是面向模板,都很難將你從冗長的代碼中拯救出來……

面向什麼,可能就會失去未面向的那些

在上文中模擬車鏈的程序中,我一開始是用面向對象的方式來寫的,所以我造出了 5 個結構體,分別描述了二維點、矩形、圓形、鏈節形狀以及鏈節等對象,結果卻出現了一大堆繁瑣的代碼。雖然面向對象編程,在思維上是非常簡單的,那就是現實中有什麼,我們就模擬什麼。但是你認真思考一下,現實中其實很多東西都有共性,如果你傻乎乎的去逐個模擬,而忽略它們的共性,那麼你的代碼絕對會非常臃腫。

當然,面向對象編程也提倡從所模擬的事物中提取共性,然後藉助繼承的方式來簡化代碼。但是一旦信仰了類與繼承,你能做的最好的抽象就是對某一類事物進行抽象,比如你能夠對『車』類的事物進行抽象,但是你卻無法將對『飛機』和『車』這兩類中的事物進行抽象。顯然,飛機與車是有共性的,例如它們都能載客,都有儀錶盤,都有窗戶,都有座位,都有服務員……

當我發現基於面向對象創造的那些結構體存在著一個共性——它們都包含著兩個成員,很自然的就會想到我應該製造一個包含著兩個任意類型的結構體 pair,然後用 pair 來容納我需要的數據。當面向對象編程範式在你的思想中根深蒂固,這種簡單的現象往往會被忽略的,特別是你已經滿足於你寫的程序已經能夠成功的運行之時。

接下來,當我試圖用 pair 結構體取代二維點、矩形、圓形、鏈節形狀等結構體的時候,我就開始走上了『泛型』的道路。C 語言里沒有 C++ 模板這種工具可以用,所以我只能依賴 void *,而且為了簡化 double 類型的數據向 void * 的轉化,所以定義了:

double *

malloc_double(double x){ double *ret = malloc(sizeof(double)); *ret = x; return ret;}struct pair *pair_for_double_type(double x, double y){ struct pair *ret = malloc(sizeof(struct pair)); ret->first = malloc_double(x); ret->second = malloc_double(y); return ret;}

如果你對 C++ 的泛型編程有所了解,一定會覺得 pair_for_double_type 函數其實就是對 pair 進行特化。因為本來我是希望 pair 能存儲任意類型的數據的,但是現在我需要頻繁的用它來存儲一對 double 類型的數據,那麼我就應該去製造一個專用的 pair 結構。

當我發現我需要頻繁的產生 pair 實例,並向它的 first 與 second 指針中存儲某些類型的數據存儲空間的基地址,所以我就將這種共性抽象為:

struct pair *

pair(void *x, void *y){ struct pair *ret = malloc(sizeof(struct pair)); ret->first = x; ret->second = y; return ret;}

最終使得 create_chain_node 函數的定義即簡潔又清晰:

struct chain_node *

create_chain_node(void){ struct pair *left_hole = pair(pair_for_double_type(1.0, 1.0), malloc_double(0.5)); struct pair *right_hole = pair(pair_for_double_type(9.0, 1.0), malloc_double(0.5)); struct pair *holes = pair(left_hole, right_hole); struct pair *body = pair_for_double_type(10.0, 1.0); struct pair *shape = pair(body, holes); struct chain_node *ret = malloc(sizeof(struct chain_node)); ret->prev = NULL; ret->next = NULL; ret->shape = shape; return ret;}

原來我用面向對象編程範式所寫的代碼是 104 行,換成泛型編程範式所寫的代碼是 75 行。那麼我可以斷定,是泛型編程拯救了面向對象嗎?當然不能!因為我們的程序還沒有寫完,我們還需要面向對象。

對象的回歸

先擺出 create_chain_node 函數:

struct chain_node *

create_chain_node(void){ struct pair *left_hole = pair(pair_for_double_type(1.0, 1.0), malloc_double(0.5)); struct pair *right_hole = pair(pair_for_double_type(9.0, 1.0), malloc_double(0.5)); struct pair *holes = pair(left_hole, right_hole); struct pair *body = pair_for_double_type(10.0, 1.0); struct pair *shape = pair(body, holes); struct chain_node *ret = malloc(sizeof(struct chain_node)); ret->prev = NULL; ret->next = NULL; ret->shape = shape; return ret;}

create_chain_node 函數可以創建鏈節,它是藉助很抽象的 pair 結構體將很多種類型的數據層層封裝到了 chain+node 結構體中,那麼我們如何從 chain_node 結構體中提取這些數據,並使之重現它們所模擬的現實事物?

例如,我們怎樣從 chain_node 結構體中獲取一個 left_hole 的信息?顯然,下面的代碼

struct *t = create_chain_node();

struct pair *shape = t->shape;struct pair *holes = shape->second;struct pair *left_hole = holes->first;

並不能解決我們的問題,因為 left_hole 中只是兩個 void * 指針,而我們需要知道的是 left_hole 的中心與半徑。那麼我們繼續:

struct pair *center = left_hole->first;

double radius = *((double *)(left_hole->second));

依然沒有解決我們的問題,因為我們想要的是 left_hole 的中心,而不是一個包含著兩個 void * 指針的 center,所以需要繼續:

double center_x = *((double *)(center->first));

double center_y = *((double *)(center->second));

最後我們得到了三個 double 類型的數據,即 center_x, center_y, radius,於是似乎我們的任務完成了,但是你如何將上述過程寫成一個函數 get_left_hole? C 語言中的函數只能有一個返回值。如果通過函數的參數來返回一些值,那麼 get_left_hole 是能寫出來的,例如:

void get_left_hole(struct chain_node *t, double *x, double *y, double *r)

{ struct pair *shape = t->shape; struct pair *holes = shape->second; struct pair *left_hole = holes->first; struct pair *center = left_hole->first; *x = *((double *)(center->first)); *y = *((double *)(center->second)); *r = *((double *)(left_hole->second));}

但是,如果你真的這麼寫了,那隻能說明再好的編程語言也無法挽救你的品味。

我們應該繼續挖掘指針的功能,像下面這樣定義 get_left_hole會更好一些:

struct point {

double *x; double *y;};struct hole { struct point *center; double *radius;};struct hole *get_left_hole(struct chain_node *t){ struct pair *shape = t->shape; struct pair *holes = shape->second; return holes->first;}

好在哪?我們充分利用了 C 編譯器對數據類型的隱式轉換,這實際上就是 C 編譯器的一種編譯期計算。這樣做可以避免在代碼中出現 *((double *)(...)) 這樣的代碼。void * 指針總是能通過賦值語句自動轉換為左值,前提是你需要保證左值的類型就是 void * 的原有類型。這是 C 語言的一條清規戒律,不能遵守這條戒律的程序猿,也許再好的編程語言也無法挽救他。

C++ 這個叛徒,所以無論它有多麼強大,也無法拯救那些無法保證左值的類型就是 void *原有類型的程序猿。用 C++ 編譯器迫使程序猿必須將

struct pair *shape = t->shape;

struct pair *holes = shape->second;

寫成:

struct pair *shape = (struct pair *)(t->shape);

struct pair *holes = (struct pair *)(shape->second);

否則代碼就無法通過編譯。這樣做,除了讓代碼更加混亂之外,依然無法挽救那些無法保證左值的類型就是 void * 原有類型的程序猿,只會讓他們對裸指針以及類型轉換這些事非常畏懼,逐漸就走上了惟類型安全的形而上學的道路。C++ 11 帶來了新的智能指針以及右值引用,希望他們能得到這些新 C++ 式的拯救吧。

當我們用面向對象的思路實現了 get_left_hole 之後,就可以像下面這樣使用它:

struct *t = create_chain_node();

struct hole *left_hole = get_left_hole(t);printf("%lf, %lf, %lfn", *(left_hole->center->x), *(left_hole->center->y), *(left_hole->radius));

一切都建立在指針上了,只是在最後要輸出數據的需用 * 對指針進行解引用。

上述代碼中有個特點,left_hole 並不佔用內存,它僅僅是對 t 所引用的內存空間的再度引用。可能有人會擔心 left_hole 具有直接訪問 t 所引用的內存空間的能力是非常危險的……有什麼危險呢?你只需要清楚 left_hole 只是對其他空間的引用,而這種直覺很容易在使用一段時間的指針之後就能夠建立。有了指針,你想修改 left_hole 所引用的內存空間中的數據,就可以 do it,不想修改就不去 do it,這有何難?如果自己並不打算去修改 left_hole所引用的內存空間中的數據,但是又擔心自己或他人會因為失誤而修改了這些數據……你應該將這些擔心寫到有關 get_left_hole 函數的文檔里。試圖從語言自身的層面來確保內存空間的讀寫許可權,結果必然會讓代碼充滿了與所解決的問題毫無關係的雜碎代碼,你變成了裝在套子里的人,你的程序變成了裝在套子里的程序。

對於只需要稍加註意就可以很大程度上避免掉的事,非要從編程語言的語法層面來避免,這真的是小題大作了。如果我們在編程中對於 void * 指針的隱式類型正確轉換率高達 99%,為何要為 1% 的失誤而修改編程語言,使之充滿各種巧妙迂迴的技巧並使得代碼愈加晦澀難懂呢?

《C 陷阱與缺陷》的作者給出了一個很好的比喻,在烹飪時,你用菜刀的時候是否失手切傷過自己的手?怎樣改進菜刀讓它在使用中更安全?你是否願意使用這樣一把經過改良的菜刀?作者給出的答案是:我們很容易想到辦法讓一個工具更安全,代價是原來簡單的工具現在要變得複雜一些。食品加工機一般有連鎖裝置,可以保護使用者的手指不會受傷。然而菜刀卻不同,如果給菜刀這種簡單、靈活的工具安裝可以保護手指的裝置,只能讓它失去簡單性與靈活性。實際上,這樣做得到的結果也許是一台食品加工機,而不再是一把菜刀。

我成功的將本節的題目歪到了指針上。現在再歪回來,我們來談談對象。其實已經沒什麼好談的了,get_left_hole 返回的是泛型指針的類型具化,藉助這種類型具化的指針我們可以有效避免對 pair 中的 void * 指針進行類型轉換的繁瑣過程。

將函數變成數據

再來看一下經過大幅簡化的 create_chain_node 函數:

struct chain_node *

create_chain_node(void){ struct pair *left_hole = pair(pair_for_double_type(1.0, 1.0), malloc_double(0.5)); struct pair *right_hole = pair(pair_for_double_type(9.0, 1.0), malloc_double(0.5)); struct pair *holes = pair(left_hole, right_hole); struct pair *body = pair_for_double_type(10.0, 1.0); struct pair *shape = pair(body, holes); struct chain_node *ret = malloc(sizeof(struct chain_node)); ret->prev = NULL; ret->next = NULL; ret->shape = shape; return ret;}

這個函數對於我們的示例而言,沒有什麼問題,但是它只能產生特定形狀的鏈節,這顯然不夠通用。如果我們想更換一下鏈節的形狀,例如將原來的帶兩個小孔的矩形鐵片換成帶兩個小孔的橢圓形鐵片,那麼我們將不得不重寫一個 create_elliptic_chain_node 函數。當我們這樣做的時候,很容易發現 create_elliptic_chain_node 函數中同樣需要下面這段代碼:

struct chain_node *ret = malloc(sizeof(struct chain_node));

ret->prev = NULL; ret->next = NULL; ret->shape = shape; return ret;

如果我們要生產 100 種形狀的鏈節,那麼上述代碼在不同的鏈節構造函數的實現中要重複出現 100 次,這樣肯定不夠好,因為會出現 500 行重複的代碼。太多的重複的代碼,這是對程序猿的最大的羞辱。

面向對象的程序猿可能會想到,我們可以為 chain_node 做一個基類,然後將上述共同的代碼封裝到基類的構造函數,然後在各個 chain_node 各個派生類的構造函數中製造不同形狀的鏈節……在你要將事情搞複雜之前,建議先看一下這樣的代碼:

void *

rectangle_shape(void){ struct pair *left_hole = pair(pair_for_double_type(1.0, 1.0), malloc_double(0.5)); struct pair *right_hole = pair(pair_for_double_type(9.0, 1.0), malloc_double(0.5)); struct pair *holes = pair(left_hole, right_hole); struct pair *body = pair_for_double_type(10.0, 1.0); return pair(body, holes);}struct chain_node *create_chain_node(void *(*fp)(void)){ struct chain_node *ret = malloc(sizeof(struct chain_node)); ret->prev = NULL; ret->next = NULL; ret->shape = fp(); return ret;}

看到了吧,我將 create_chain_node 函數原定義中負責創建鏈節形狀的代碼全部的抽離了出去,將它們封裝到 rectangle_shape 函數中,然後再讓 create_chain_node 函數接受一個函數指針形式的參數。這樣,當我們需要創建帶兩個小孔的矩形形狀的鏈節時,只需:

struct chain_node *rect_chain_node = create_chain_node(rectangle_shape);

如果我們像創建帶兩個小孔的橢圓形狀的鏈節,可以先定義一個 elliptic_shape 函數,然後將其作為參數傳給 create_chain_node,即:

struct chain_node *elliptic_chain_node = create_chain_node(elliptic_shape);

這樣做,豈不是要比弄出一大堆類與繼承的代碼更簡潔有效嗎?

在 C 語言中,函數名也是一種指針,它引用了函數代碼所在內存空間的基地址。所以,我們可以將 rectangle_shape 這樣函數作為參數傳遞給 create_chain_node 函數,然後在後者中調用前者。

由於我們已經將 chain_node 結構體中的 shape 指針定義為 void * 指針了,因此對於 create_chain_node 函數所接受的函數,其返回值是 void * 沒什麼問題。不僅沒問題,更重要的是 void *(*fp)(void) 對所有不接受參數且返回指針類型數據的函數的一種抽象。這意味著對於鏈節的形狀,無論它的形狀有多麼特殊,我們總是能夠定義一個不接受參數且返回指針的函數來產生這種形狀,於是 create_chain_node 函數就因此具備了無限的擴展能力。

如果阿基米的德還活著,也許他會豪放的說,給我一個函數指針與一個 void *,我就能描述宇宙!

代碼簡化的基本原則

當你採用一切都是對象的世界觀編寫代碼時,一旦發現一些類之間存在著共性的數據抽象,這往往意味著你需要創造一種泛型的數據容器,然後用這種容器與具體類型的數據的組合來消除那些類。

當你打算從泛型的數據容器中取數據,並希望所取的數據能夠直觀的模擬現實中的事物時,這往往意味著你要創造一些數據結構,然後讓泛型的數據容器中存儲的數據流入這些數據結構中,從而轉化為有類型且具名的數據。這些數據結構就類似於各種各樣的觀察器或 Parser,我們通過它們解讀或修改泛型容器中的數據。

當某個函數 f 中有一部分代碼是與具體的問題息息相關,而另一部分代碼則與具體的問題無關。為了讓這個函數具備強大的擴展性,你需要將那些與具體問題息息相關的代碼抽離到專用的函數中,然後再將這些專用函數傳遞給 f。

迴避 C 指針是要付出代價的

在 C 語言中,在執行上述的代碼簡化基本原則時,指針是最簡單明快的工具,像是著名廚師庖丁手裡的刀。在靜態類型語言中,任何企圖迴避指針的行為,必然會導致編程語言的語法複雜化或者削弱語言的表達能力。

在 C++ 中為了迴避指針,發明了引用——本質上一種被弱化了的指針,結果導致 C++ 初學者經常要問『什麼時候用指針,什麼時候用引用』這樣的問題。在智能指針未問世之前,STL 提供的泛型容器無法存儲引用,為了避免在容器中存儲對象時發生過多的內存複製,往往需要將指針存到容器中。當某個函數在內部創建了一個比較大的對象時,這個函數想將這個對象傳遞給其他對象時,這時如果不藉助指針,那隻能是將這個大對象作為返回值,然後引發了對象數據不止一次被複制的過程。如果在函數中 new 一個大對象,然後以指針的形式將其返回,這又與 C++ 一直想向用戶掩蓋指針的理想發生了矛盾……為了解決這個問題,終於在 C++ 11 里搞出來一個挺複雜挺扭曲的右值引用的辦法,解決了在類的複製構造函數中偷偷的使用指針,但是類的用戶卻看不到指針這樣的問題……

Java 迴避指針的策略比 C++ 要高明一些。在 Java 中,即沒有指針也沒有引用。只要是類的實例(對象),無論是將其作為參數傳遞給函數,還是作為函數的返回值,還是將其複製給同類的其他對象,都是在傳地址,而不是在傳值。也就是說,Java 將所有的類實例都潛在的作為指針來用的,只有那些基本類型才是作為值來傳遞的。這種對數據類型進行了明確的區分的態度是值得點贊的,但是當 Java 想將一個函數(方法)傳遞給另一個函數(方法)時,代碼就出現了扭曲,完全不能做到像 C 語言以指針的形式傳遞函數那樣簡潔直觀。

C# 在指針的處理上似乎要比 Java 好得多,但是將那些使用指針的代碼標定為 unsafe,這是一種歧視,類似於『嗟,來食!』。另外 C# 的指針只能用於操作值類型,也不能在泛型代碼中使用。

在動態類型語言中,例如 Python,據說是一切皆引用,這樣很好。也可以直接將一個函數作為參數傳遞給另一個函數,甚至還能在一個函數中返回一個函數,這樣更好。動態類型語言在語法、抽象能力、類型安全以及資源管理方面很大程度上超越了 C、C++、Java 這些靜態類型語言,但是用前者編寫的程序的計算速度卻往往比後者慢上一倍。

沒有完美的指針,也不會有完美的編程語言,這一切皆因我們是在機器上編程,而不是在我們的大腦編程,更不是在教科書里編程。

C 程序猿的指針信條

這是我的指針。雖有很多相似的,但這個是我的。我的指針是我的摯友,如同我的生命。我將運用它如同運用我的生命。指針沒了我便是廢物,我沒了指針便成為廢人。我將準確無誤的使用我的指針,我將比敵人用的更好,我將在他的程序速度超過我之前超過他,我會超過他的。

我與我的指針知道,編程中不論動用多麼優雅的語言,動用多麼強大的標準庫,面向多麼強大的編程範式,都是沒意義的。只有解決問題才有意義。我們會解決的。

我的指針是人性的,就像我一樣,因為它如同我的生命。因此我將像對兄弟一樣地了解它。我將了解它的弱點,它的強項,它的構成,它所指的和指向它的。我將對指針持續建立完善的知識與技藝,使它們就如同我一般整裝待發。我們會成為彼此的一部分。

在上帝面前我對這信條宣誓。我與我的指針是計算機的守衛者,我們是問題的剋星,我們將拯救我的程序。但願如此,直到不需要編程,沒有問題,只有休息。


推薦閱讀:
相关文章