[說明]
本文不僅介紹了C++語言應用非常好的一種方法(我甚至覺得應該将它歸結為一種設計模式),而且也是對C#語言中委托特性底層實作的一個很好的說明。
閱讀本文,你應當對委托的概念有所了解;在讨論委托是實作時,你應當對标準模闆庫(STL)中的list容器以及疊代器(iterator)有所了解。
在這篇文章中,暫不讨論類成員函數。
1.C#中的委托
你如果對C#語言比較了解的話,就應該會知道C#語言中有一個很好的特性,那就是委托。它能夠大大簡化在某些特定的場合調用多個相同形式函數的處理。特别是在像Windows程式中,用委托響應消息十分友善。舉一個常見的例子。現在的Windows應用程式架構都比較複雜,一個應用程式可能由許多部件組成,很多時候它們都需要響應同一個消息。如在一個MDI中,多個子窗體都要響應主窗體的WM_QUIT消息。很多時候,我們并不需要像MFC那樣将所有的處理都封裝在類中,我們需要一種簡單易用的方法,直覺地解決這個問題。這篇文章為你提供了這樣的方法。
在具體介紹這篇文章之前,讓我們先看看在C#中是怎樣使用委托的。首先,在像處理Windows消息這樣的操作時,被通知的對象都以一種固定的格式來接受消息。在C#中,處理消息的委托被定義為如下格式:
public delegate void WinEventHandler(object sender, EventArgs arg);
假設在一個MDI中,主窗體由MainFrame實作。我們在主窗體定義一個專門用于處理WM_QUIT消息:
public event WinEventHandler OnQuit;
在上面的聲明中,public表明該委托可以在外部通路(注冊/删除),event關鍵字表明這是一個事件,隻能在類内部調用,外部不能直接觸發它。在MainFrame的窗體過程函數中,WM_QUIT消息是這樣被分發的:
protected virtual void WndProc(Message msg,object sender,EventArgs arg)
{
......
switch(msg)
{
......
case WM_QUIT:
OnQuit(sender,arg);
break;
......
}
}
那麼,MainFrame的子窗體如何響應主窗體的WM_QUIT消息呢?首先,你要實作子窗體Child1處理該消息的函數,它的聲明形式要跟WinEventHandler委托相同:
//In Child1 Class
protected void Child1_On_Quit(object sd, EventArgs ags)
{
this.SaveAll(); file://做些善後工作,如儲存目前資訊等
……
}
在子窗體被初始化時向MainFrame的OnQuit委托注冊這個函數。如果MainFrame是Child1的父窗體,那麼其實作可能是這樣的:
parent.OnQuit += new WinEventHandler(this.Child1_On_Quit);
這樣,當MainFrame收到WM_QUIT消息時,調用OnQuit委托,同時Child1.Child1_On_Quit也被調用,進而實作消息傳遞。當然,也有子窗體不再需要響應主窗體的WM_QUIT消息的時候。我們可以通過下面的方式從MainFrame的OnQuit委托中登出它:
parent.OnQuit -= new WinEventHandler(this.Child1_On_Quit);
這一步也是很必要的。如果Child1先于主窗體MainFrame被摧毀,而Child1_On_Quit沒有從MainFrame.OnQuit委托中登出,則主窗體收到WM_QUIT消息時調用OnQuit委托,它又順序調用到Child1.Child1_On_Quit,則可能引發空引用異常了。[詳細介紹,請參見《IL代碼底層運作機制:函數相關]
委托可以接受多個執行個體方法,是以你可以向一個委托注冊多個方法。實際上,委托包含了一個方法引用的清單,當委托被調用時,它将順序調用其清單中的方法引用。這一點我還會在後面詳細說明。
2.在C++中實作委托
我們知道了C#中委托的原理,是不是也可以在C++中實作呢?答案是肯定的。不同的是C#在語言級别提供了對委托的支援,而C++沒有,它需要我們對委托進行定制。這樣,每種不同形式的委托都要有不同的實作,靈活性大打折扣。幸運的是,作者已經提供了一個名為delegate.exe的實用小工具,它可以幫我們實作由委托聲明生成實際代碼。其用法将在後面詳細介紹。
現在,我們主要考慮的是如何來實作我們自己定制的委托。前面我已經簡單介紹了一個委托應當具備的因素:儲存方法引用(在C++中是指針,但在這裡我還是習慣稱之為引用)的清單,添加/删除方法引用,以及最重要的調用例程。有多中方法可以實作這些操作,這裡我們采用類來實作。
第一個要考慮的是如何來儲存方法引用。因為方法引用(指針)實際上是一個32位無符号整數,是以我也采用無符号整型來存儲方法引用(指針)。這裡,我定義了這種資料類型:typedef unsigned int NativePtr。在接受
第二是聲明這個委托的形式。在這個例子中,我采用void Handle(char *str)的形式作為示例。我們定義這種函數指針類型typedef void (* Handler)(char *str)以供函數調用時,作為由無符号整形向函數指針的轉換類型。
第三個要考慮的是怎樣實作多個方法引用(指針)的存儲。最簡單的方法是使用STL中的list清單容器存儲。list模闆類為我們提供了一組非常友善的清單存取操作方法,它提供的疊代器使我們能夠很容易地使用它。在CDelegate類中,我定義了用于存儲函數引用的字段ftns:list <NativePtr> ftns.
第四是實作添加/删除函數引用。這裡兩個操作分别由AddFunction/RemoveFunction來實作。在這裡,有一個問題是我們接受什麼樣的參數類型,怎樣接受。毫無疑問,它要接受的是前面定義的Handler類型。但我們已使用32為無符号整型來儲存其資訊,是以,為了簡單起見,AddFunction/RemoveFunction函數的參數為void * 類型,這樣它可以接受許多類型的參數。關于使用什麼樣的類型作為AddFunction/RemoveFunction的參數這一點,我想可能還要詳細讨論一下,究竟是前面定義的Handler類型還是void *類型。應該說兩種類型都有其優缺點,關鍵是看我們在什麼時候應用。當然,使用我們定制的委托類型(也即前面定義的Handler類型)作為其參數類型可以讓編譯器為我們做必要的文法檢查,以防止不比對的參數被傳替。這就要看實際情況了。
當然,重載 += 和 - = 操作符是必須的了。
最後,也是最重要的是實作我們的Invoke方法。我們對Invoke方法有要求,它必須和我們定制的委托類型是一緻的。同時,我們也需要重載()操作符号,以友善我們像一般函數那樣調用它。
下面我給出類的定義部分:
#include <list>
using namespace std;
typedef unsigned int NativePtr;
typedef void (* Handler)(char *);
class CDelegate
{
private:
list<NativePtr> ftns;
public:
void AddFunction(void *);
void RemoveFunction(void *);
int Invoke(char *);
void operator += (void *);
void operator -= (void *);
int operator ()(char *);
};
下面我給出各個方法實作的代碼。
#include "Delegate.h"
void CDelegate::AddFunction(void *ftn)
{
NativePtr np=(NativePtr)ftn;
ftns.push_back(np);
}
AddFunction函數接受類型為void * 的參數,然後将這個參數強制轉換為NativePtr(unsigned int)類型,存放于ftns清單中。注意,這個數值從清單的尾部插入,以實作FIFO。
void CDelegate::RemoveFunction(void *ftn)
{
NativePtr np=(NativePtr)ftn;
ftns.remove(np);
}
RemoveFunction函數接受類型為void * 的參數,然後将這個參數強制轉換為NativePtr ( unsigned int ) 類型,再從ftns中删除與它的值相同的元素。
void CDelegate::operator += (void *ftn)
{
this->AddFunction(ftn);
}
+=操作符重載AddFunction方法。
void CDelegate::operator -= (void *ftn)
{
this->RemoveFunction(ftn);
}
-=操作符RemoveFunction方法。
int CDelegate::Invoke(char * pch)
{
Handler handle;
list<NativePtr>::iterator itr=ftns.begin();
try
{
for(;itr!=ftns.end();itr++)
{
handle=(Handler)*itr;
handle(pch);
}
}
catch(char *)
{
return 0;
}
return 1;
}
使用list模闆類提供的疊代器,周遊ftns中的每個元素,順次将元素轉化為定制的函數引用(指針)類型,并調用其所對于的函數。這裡要求委托傳回值必須為空。如有異常,則Invoke傳回0值。
int CDelegate::operator ()(char *pch)
{
return Invoke(pch);
}
()操作符重載Invoke方法。
可以看到,我們實作的這個委托類其實很簡單。将添加的函數引用(指針)添加到一個清單中;當委托被調用時,将清單中的函數引用逐個取出并調用。C#中的委托的實作也是如此;它對委托的處理,程式生成器的腳色是由編譯器扮演的。其實如果你對由C#編譯器生成的IL代碼進行剖析,每個C#委托聲明也都是被轉化為繼承自某個支援類似功能的類處理的。同時,也正是由于委托管理着多個方法的調用,它不能處理它們的傳回值,是以委托要求被委托的函數不能具有傳回值。
下面是運作示例:
#include "Delegate.h"
#include <iostream>
#include <windows.h>
void Say1(char *s)
{
cout<<"In Function Say1: ";
cout<<s<<endl;
}
void Say2(char *s)
{
cout<<"In Function Say2: ";
cout<<s<<endl;
}
void STHeoaie(char *s)
{
MessageBox(NULL,s,"Delegate",MB_OK);
}
void main()
{
CDelegate dlg;
dlg.AddFunction(Say1);
dlg.AddFunction(Say2);
dlg+=STHeoaie;
int rs=dlg.Invoke("Hello,World!");
if(!rs) cout<<"Failed."<<endl;
dlg-=Say2;
rs=dlg("The second invoking by CDelegate!");
file://等同于dlg. Invoke("The second invoking by CDelegate!")
if(!rs) cout<<"Failed."<<endl;
dlg-=Say1;
dlg-=STHeoaie;
rs=dlg.Invoke("The Third invoking by CDelegate!");
if(!rs) cout<<"Failed."<<endl;
}
3.關于實用小工具delegate.exe
為了解決在定制委托時的不靈活性,我特意編寫了這個小工具,它能夠友善地将委托聲明轉化為如上面所述的代碼。下面是其基本用法。
在你的某個頭檔案中,如test.h,以__delegate 關鍵字聲明一個委托:
__delegate void WinHandler ( HWND hwnd ,UINT message ,WPARAM wParam,LPARAM lParam);
然後轉到指令行模式,進入test.h的目錄,鍵如如下指令:
delegate.exe test.h /out test.hxx
它将生成test.hxx檔案。你可以向你的源程式中包含這個檔案,以使用你所定義的委托。如,可以是這樣:#include “test.hxx”
你可以在一個檔案裡定義多個委托,也可以在多個檔案裡定義多個委托.但是你隻能指定一個的輸出檔案.如果你沒有用/out 選項指定輸出檔案,則預設輸出為delegate.h。如:
delegate.exe test.h test1.h test2.h /out test.hxx
使用/help選項得到幫助資訊,使用/version選項得到版本資訊.
注意:最新的Visual C++ .Net 版本已經支援同名關鍵字 __delegate,這是微軟公司為了将Visual C++向.net移植而添加的新關鍵字,隻有 Visual C++ .Net 支援,其他如Visual C++ 6.0、Borland C++ Builder 、GNU C++ 等都不支援。但兩個完全沒有聯系.幸運的是,delegate小工具支援/keyword 選項,它可以指定你自己定義的關鍵字,如__delegate__。
由于隻是測試版本,這個小工具的功能還不是很強。比如,你不能在委托聲明語句中間有注釋,而隻能在聲明前或聲明之後添加注釋。另外由于時間關系,在處理委托傳回值沒有做到很好的處理。希望這個問題很快會解決。有什麼意見和建議,請将之回報到郵箱:[email protected].
[1]關于委托的更多資訊,請參見拙作<IL代碼底層運作機制:函數相關>
[2]關于标準模闆庫(STL),請參考相關文檔或書籍,如MSDN或 <STL源代碼剖析>
[3]關于疊代器(iterator),<設計模式>中有詳細論述
[4]關于delegate.exe小工具詳細資訊,請參見附文<委托代碼生成器>