泛型程式設計在非C++語言中的實現之探討 (轉)

worldblog發表於2007-12-09
泛型程式設計在非C++語言中的實現之探討 (轉)[@more@]

 

泛型在非C++語言中的實現之探討

左輕侯
2001.9.22


  GP(Generic Programming,泛型程式設計)號稱程式設計思想的又一次革命。但是,在論述GP的資料中,一般都是以C++語言為基礎來討論。那麼,GP是否可以在其它的程式語言中實現呢?這是作者一直在思考的一個問題,因為水平有限和資料匱乏,收穫甚微。現將一些不成熟的想法整理出來,請方家不吝指教。
  本文以為例(的情況與此類似,可參照),討論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都透過強制而保證成為T的子類(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是根據的理論和本身的類庫體系自然地實現的。作者個人以為,從這個角度來看,Delphi/Java的實現機制更加簡單和直觀。
  2、從執行效率來算,C++肯定要強於Delphi/Java,因為C++的實現是編譯期的,而Delphi/java是執行期的。RTTI的使用將對效率造成不小的影響。但是,從另一個角度來考慮,ADT在執行期的實現也帶來了好處,那就是可以在執行期隨意地改變容器類所儲存的資料結構型別。作者還沒有考慮過這種“資料型別的多型”會對程式設計帶來什麼實質性的後果。不過大膽地設想一下,以後也許可以將標準演算法封裝在DLL之類已編譯的模組中,甚至封裝在OS提供的COM物件裡,程式設計師只需要建立自己的資料型別,將其提交給相應的介面?……
  3、C++中透過運算子過載,來實現對ADT的操作。在不支援運算子過載的Delphi/Java中,作者未能發現好的代替方法。建立統一的ADT抽象類的解決方法,只能說是一個餿主意。:-(有程式設計師傾向於Delphi中應該增加運算子過載,這應該是理由之一,不知道Borland是否重視。另外,據說下一版的Java將會全面支援GP,不知道是透過什麼機制來實現?請了解內幕的高手介紹一下。

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-990669/,如需轉載,請註明出處,否則將追究法律責任。

相關文章