GP(Generic Programming,泛型編程)號稱編程思想的又一次革命。但是,在論述GP的資料中,一般都是以C++語言為基礎來討論。那麼,GP是否可以在其它的編程語言中實現呢?這是作者一直在思考的一個問題,因為水平有限和資料匮乏,收獲甚微。現將一些不成熟的想法整理出來,請方家不吝指教。
本文以Delphi為例(Java的情況與此類似,可參照),討論GP的另一種實現思路。代碼是隨手寫出的,未經驗證。
根據作者的理解,實現GP的關鍵之處,在於實現ADT(Abstract Data Type,抽象數據類型)。只有實現了ADT,才能夠將具體的數據類型與通用的算法分離開來。
在C++中,ADT的存儲是通過模板來實現的。舉一個最簡單的棧的例子(沒有給出實現部分):
template
class Stack{
public:
void Push(const Type &item);
Type Pop;
...
}
棧的應用:
Stack s;
int data;
data = 1;
s.Push(data); //入棧
int out;
out = s.Pop; //出棧
通過建立一個int類型的Stack對象,實現了對int類型數據的存儲。
但是,在Delphi/Java中,並沒有模板這種機制。那麼如何實現ADT呢?與C++不同的是,Delphi/Java的類繼承體系是單根結構的,也就是說,在Delphi中,所有的class都通過編譯器強制而保證成為TObject的子類(Java中是Object)。這個TObject可以看成是一切類的最高層次的抽象。那麼,是否可以認為Delphi/Java中已經先天地提供了對ADT的支持呢?
試著用這種思路建立一個棧:
TStack = class
public
procedure Push(item:TObject);
function Pop:TObject;
...
end;
這個TStack類針對TObject類型的對象進行操作。但是,這個類還不能立即應用,因為在Delphi中,簡單數據類型並不是對象。所以必須再建立一個自定義的數據類型。下面建立了只有一個integer成員的自定義數據類型:
TADT = class
public
data:integer;
end;
再來看棧的應用:
var
stack:TStack;
adt1,adt2:TADT;
begin
stack := TStack.create;
adt1 := TADT.create;
stack.Push(adt1); //入棧
adt2 := stack.Pop as TADT; //出棧
stack.free;
這樣就完成了對ADT對象的存儲。必須注意到,在入棧時,是將adt對象直接入棧的,因為TStack類是對TObject進行操作,由於Delphi的單根結構,可以將任何類型的對象賦給TObject類型的變量。但是,出棧時,返回的也是一個TObject的變量,因此必須用as完成一次向下映射。同樣由於單根結構,Delphi/Java提供了對RTTI的強大支持,因此這種向下映射是輕而易舉的事情。但是,毫無疑問,RTTI在效率上有所損失。
在實現了ADT的存儲之後,如何才能實現對ADT的操作呢?
在C++中,通過運算符重載來解決這個問題。看一個例子:
在一個List類中,執行查找操作:
template class List{
Type* find(Type &value); //查找指定數據項,找到則返回其地址,否則返回NULL
...
}
template Type* List::find(Type &value){
Type* p = first->link;
where(p!=NULL&&!(p->data==value)){
p=p->link;
}
return p;
}
在List類的find函數的實現中,代碼抄自鏈表結構的實現,不需要關心其細節。需要注意的地方只有一個,即判斷條件中的p->data==value。由於p->data和value都是ADT,在建立一個List類的對象時,它們可能是簡單數據,也可以是任何的自定義數據類型。但是,仍然可以統一使用==這樣的運算符,對它們進行操作。為什麼呢?原因在於運算符重載。
下面用一個Point類來說明這一點:
class Point{
private:
int x,y;
public:
int Operator ==(const Point &point); //判斷兩個Point對象是否相等
...
}
int Point::Operator ==(const Point &point){
if(x==point.x&&y==poing.y){
return 1;
}else{
return 0;
};
}
可以看到,由於重載了==運算符,兩個Point對象之間可以進行比較。同理,任何數據類型,只要重載了相應的運算符,就可以被容器類進行統一的操作。
當目光重新轉向非C++的時候,問題又出現了:Delphi/Java不支持運算符重載。做為補救措施,可以用函數來代替運算符。例如,統一使用equals函數來代替==。可是,更大的問題在於:容器類操作的對象是TObject,而TObject並沒有equals這樣的方法。(在Java中,object有equals方法,但並沒有其它的完整的運算方法。)
在不改變Delphi/Java現有語法的前提下,作者能想到的解決辦法是,建立一個TADT類,作為所有數據結構的基類。TADT中定義了很多象equals、add之類的抽象方法。自定義的數據類型一律從TADT派生,並重載相應方法。而容器類則只需要對TADT進行操作,即可實現通用的算法了。但是,這種解決方法並不理想。
(補充一點,其實Delphi自己提供了一些通用的容器類,如TList、TCollection、TStack、TQueue等。但與本文中所說的不同,它們存儲的不是TObject,而是pointer。由於Delphi的“引用對象模型”機制,存儲TObject對象,其實也就等於存儲一個pointer,不同之處在於,pointer不但可以存儲對象,而且可以存儲基本數據類型。這應該也是Borland如此設計的原因。但是,這些容器類只提供了add、delete之類的管理方法,並沒有提供通用的算法,因為對pointer不能進行復雜的操作。實際操作中,往往是程序員從容器類派生出一個新類,或者在自己的類中維護一個容器類。這也是一種解決辦法,但算法就不能獨立出來了。)
綜上所述,作者的觀點如下:
1、C++中通過模板機制來實現ADT的存儲,Delphi/Java同樣可以通過單根結構+RTTI的機制來實現。其不同之處在於,C++的實現是語法上的,而Delphi/Java是邏輯上的,也就是說,C++是通過一套特殊的語法來實現的,而Delphi/Java是根據OOP的理論和本身的類庫體系自然地實現的。作者個人以為,從這個角度來看,Delphi/Java的實現機制更加簡單和直觀。
2、從運行效率來算,C++肯定要強於Delphi/Java,因為C++的實現是編譯期的,而Delphi/Java是運行期的。RTTI的使用將對效率造成不小的影響。但是,從另一個角度來考慮,ADT在運行期的實現也帶來了好處,那就是可以在運行期隨意地改變容器類所存儲的數據結構類型。作者還沒有考慮過這種“數據類型的多態”會對編程帶來什麼實質性的後果。不過大膽地設想一下,以後也許可以將標准算法封裝在DLL之類已編譯的模塊中,甚至封裝在OS提供的COM對象裡,程序員只需要創建自己的數據類型,將其提交給相應的接口?……
3、C++中通過運算符重載,來實現對ADT的操作。在不支持運算符重載的Delphi/Java中,作者未能發現好的代替方法。建立統一的ADT抽象類的解決方法,只能說是一個馊主意。:-(有程序員傾向於Delphi中應該增加運算符重載,這應該是理由之一,不知道Borland是否重視。另外,據說下一版的Java將會全面支持GP,不知道是通過什麼機制來實現?請了解內幕的高手介紹一下。