程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 事件編程(二)

事件編程(二)

編輯:關於VC++

在本文的第一部分(事件編程一),我回答了一個關於用 C++ 實現本機事件的問題。討論了一般意義上的事件並示范了如何用接口為你的類定義事件處理器,事件的處理必須在客戶機實現。我的實現有一些缺陷,我承諾過最終要解決掉,本文就來完成這件事情。

在開始之前,先簡單回顧一下前面寫的那個程序,PrimeCalc。如 Figure 1 所示:

Figure 1 計算素數

程序中使用了一個計算素數的類 CPrimeCalculator,這個類發起兩個事件:Progress 和 Done。當搜索到素數時,該類觸發 Progress 事件以報告目前發現了多少素數。完成處理後觸發 Done 事件。這兩個事件都是由接口 IPrimeEvents 定義的:

class IPrimeEvents {
public:
 virtual void OnProgress(UINT nPrimes) = 0;
 virtual void OnDone() = 0;
};

客戶機要想處理事件必須得從 IPrimeEvents 派生,實現事件處理函數,並調用 CPrimeCalculator::Register 來注冊其接口。CPrimeCalculator::Register 會將客戶機對象/接口添加到其內部列表。當觸發了一個 Progress 事件時,CPrimeCalculator 便調用輔助函數 NotifyProgress:

void CPrimeCalculator::NotifyProgress(UINT nFound)
{
 list::iterator it;
 for (it=m_clients.begin(); it!=m_clients.end(); it++) {
  (*it)->OnProgress(nFound);
 }
}

NotifyProgress 遍歷客戶機列表,調用每個客戶機的 OnProgress 處理函數。當某個程序員使用 CPrimeCalculator 時,編寫事件處理代碼很容易——只要從 IPrimeEvents 派生並實現處理器即可。但是在實現這種觸發事件的 CPrimeCalculator 類機制時冗長乏味。你必須得為每個事件(如 Foo)實現諸如 NotifyFoo 這樣的函數,即使處理模式一模一樣。事件觸發代碼被劃分在兩個類中,事件接口 IPrimeEvents 和 事件源 CPrimeCalculator。如果你想將同樣的事件接口用於不同的事件源那該怎麼辦?IPrimeEvents 是很通用,我可能將它改名為 IProgressEvents 並將它用於任何以整數形式報告處理進度的類並在完成處理時用 Done。但每個觸發 Progess 事件的類必須重新實現觸發事件的通知函數。理想情況下,所有事件代碼都應該放在單個類中。

既然通知函數在本文中是一種實驗模型,那麼自然會問這樣的問題:它們有沒有某種通用的實現方法?我能將整個事件機制封裝到單個的類、模板或宏,或者任何事件源能使用的其它什麼類型中嗎?答案是肯定中的肯定。我將示范如何創建一個使用宏和模板的事件系統,以便將事件處理的代碼量降至最低限度。我們的旅程需要借助一些高境界的 C++ 操作,比如嵌套模板以及仿函數類(functor class)。

我將分幾個步驟實現這個系統。目的是編寫一個實現通知函數 NotifyProgress 以及 NotifyDone 的模板。每個函數都具備相似而又不完全一樣的模型:

// NotifyFoo — raise Foo event
list<IPrimeEvents*>::iterator it;
for (it=m_clients.begin(); it!=m_clients.end(); it++) {
 (*it)->OnFoo(/*args*/);
}

也就是說迭代客戶機列表,並針對每個客戶機調用 OnFoo,傳遞事件參數。如何把它寫成一個模板呢?可以將接口 IPrimeEvents 參數化為一個類型 T,但如何參數化事件處理函數 OnFoo,程序員可能選擇的任何名字和簽名。

任何時候你參數化某個函數時,都應該考慮:仿函數,也叫做 functor。仿函數是 C++ 語言中將函數轉換為類的一種機制,它代替了給回調函數傳遞指針的做法,而是傳遞仿函數類的實例。在標准模板庫 STL 中包含有豐富的 Functor,並實現了一些使用 functor 的算法,尤其是 for_each 算法,在本文中很有用:for_each(m_clients.begin(), m_clients.end(),
NotifyProgress(nFound));

for_each 算法從頭到尾迭代容器元素,並對每個元素調用函數對象 NotifyProgress。這裡說的“函數對象”到底是指的什麼呢?不是一個函數,它是一個對象。這個類看起來像下面這個樣子:

class NotifyProgress {
protected:
 UINT m_nFound;
public:
 NotifyProgress(UINT n) : nFound(n) { }
 void operator()(IPrimeEvents* obj)
 {
  obj->OnProgress(nFound);
 }
};

NotifyProgress 實現函數 operator()(IPrimeEvents*),它是 for_each 算法需要的東西。一般來講,如果你具備一個類型為 T 對象集合,for_each 會需要一個實現 operator()(T) 的仿函數(functor)。它調用該集合中 T 對象的這個操作符。所以這裡函數 operator 有一個 IPrimeEvents 指針參數並返回 void —— 因為客戶機列表是一個 IPrimeEvents 指針列表。為了傳遞附加參數,構造函數將它們保存在數據成員裡。NotifyProgress(nFound) 調用構造函數以創建一個用 m_nFound=nFound 初始化的堆棧實例。所以,任何觸發 Foo 事件的 Foo 仿函數的一般模式是這樣的:

class NotifyFoo {
protected:
 ARG1 m_arg1; // whatever, as many as needed
public:
 NotifyProgress(ARG1 a1, ...) : m_arg1(a1) { }
 void operator()(IMyEvents* obj)
 {
  obj->OnFoo(m_arg1, ...);
 }
};

構造函數將事件參數作為數據成員來保存,函數 operator 將它們傳遞到對象事件處理函數。對於所有仿函數來說,最終結果是——將函數 OnFoo 轉換為類 NotifyFoo。這樣做為什麼會有用呢?因為我能編寫一個模板。在我開始做之前,有一件事我必須得提一下。那就是你必須從一個叫 unary_function 的 STL 類派生你的仿函數類:

class NotifyProgress :
 public unary_function
{
 .
 . // as before
 .
};

也就是說,NotifyProgress 是一個一元函數,其函數 operator 帶一個參數,IPrimeEvents 指針並返回 void。該一元函數使你的仿函數類“可適配”,使你能將它與 STL 適配器,如:not1、bind2nd 等等進行結合。但是即使你從來都沒有打算使用適配器,就像我的事件處理例程,unary_function 仍然不失為一個好主意,因為它向這個世界宣告:“這是一個函數類。”它是一種將代碼文檔化的方式。有關適配器的詳細討論,參見 Effective STL:50 Specific Ways to Improve Your Use of the Standard Template Library (Addison-Wesley, 2001) by Scott Meyers

STL 的高手們也許會問:為什麼我不使用 mem_fun 適配器直接將 IPrimeEvents::OnProgress 轉換為函數對象。因為 OnProgress 是虛擬函數,我不能適配一個虛擬函數。如果這樣做,要觸及到基類。如果你使用 Boost 庫,可以用其捆綁適配器直接將 OnFoo 這樣的虛擬事件處理器轉換為仿函數,不用編寫仿函數。如果你不明白我所講的這些內容,不用害怕,不看這些內容好了。

當然,我還需要一個 Done 事件的 NotifyDone。由於 Done 沒有參數,構造函數也沒有:

class NotifyDone : public unary_function
{
public:
 NotifyDone() { }
 void operator()(IPrimeEvents* obj)
 {
  obj->OnDone();
 }
};

現在我有了自己的仿函數類,我可以用 for_each 代替手工迭代客戶機列表。可我把它們放在哪呢?仿函數屬於與事件說明有關的范疇,所以我把它們放在 IPrimeEvents 接口中,用嵌套類的形式。代碼如 Figure 2 所示。細心的讀者會注意到我在兩個地方還做了細小的惡修改。仿函數的命名沒有用 NotifyProgress,而是叫做 Progress。稍後你會明白這樣做使代碼更易讀;還有就是我沒有把事件處理器都聲明為純虛擬函數,而是將它們定義為空實現。IPrimeEvents 只有兩個事件,但對於一般的事件機制來說,如果程序員感興趣的的處理並不多,但要讓他們實現每一個事件處理器似乎不是很友好。所以這裡每個處理器默認實現什麼也不做。為了使基類抽象化,我聲明了一個純虛擬析構函數。當你想抽象化一個沒有任何純虛函數的基類時,這是一個標准的C++技巧。唯一的要做的是你必須定義一個析構函數。純虛擬函數沒有定義——除非它是析構函數。既然每一個派生類的析構都調用其基類的析構函數,那麼基類需要一個實現,即便它是純虛擬的:

inline IPrimeEvents::~IPrimeEvents() { }

有了我的仿函數定義,CPrimeCalculator 是這樣觸發 Progress 事件的:

// in CPrimeCalculator:
void NotifyProgress(UINT nFound)
{
 for_each(m_clients.begin(), m_clients.end(),
  IPrimeEvents::Progress(nFound));
}

到這裡,我已經介紹了仿函數類 Progress 和 Done,同時,NotifyProgress 和 NotifyDone 都能用 STL 的 for_each 算法。下一步該做什麼?記住,我的目的是完全擺脫 NotifyFoo 函數——或者說得更具體一點,就是把它們實現為模板,以便程序員在創建事件時不必為他們定義的每個事件編寫千篇一律的函數。將 for 循環轉化為 for_each 算法只是萬裡長征的第一步。

通過將虛擬成員函數 OnFoo 轉換為 Foo 仿函數類型,從而為模板化創造條件。(仿函數在這裡有點像 .NET 中的委托。)現在我的通知函數根據類型的不同而不同,替代了函數名,我可以將它們參數化。這樣一來,我便可以將整個事件實現移出 CPrimeCalculator ,把它們放入新的模板類 CEventMgr 中,這是一個完全通用的類。如 Figure 3 所示。CEventMgr<I> 保存 I* 指針列表。它具備 Register 和 Unregister 方法以便添加元素和從其列表中刪除元素,此外它還有一個模板成員函數 Raise 用於觸發事件:

template
class CEventMgr
{
 ...
 template
 void Raise(F fn)
 {
  for_each(m_clients.begin(), m_clients.end(), fn);
 }
};
很難相信,平時幾乎碰不到的模板套模板的情況?在此處派上用場了。現在觸發事件我們可以這樣做:void NotifyProgress(UINT nFound)
{
 m_eventmgr.Raise(IPrimeEvents::Progress(nFound));
}

沒有 for 循環,甚至都沒有 for_each,所有細節都被封裝在 CEventMgr 之中,事件的觸發使用一行代碼。我甚至可以完全省略掉 NotifyProgress,每當想要觸發事件時僅僅調用 CEventMgr::Raise 即可——然而,好的編碼規范促使我寧願將 Raise 封裝在某個函數中,以防萬一我要修改 CEventMgr 或將事件觸發函數暴露給客戶機。既然 NotifyProgress 是內聯函數,就不會有幸能丟失。

如果模板使你傷腦筋,我就再講清楚一些吧。CEventMgr 是一個參數化的模板類,其參數是事件接口 I。因此 CEventMgr<IPrimeEvents> 根據 IPrimeEvents 實例化一個事件管理器。它保存數據成員 m_clients,該成員是一個 IPrimeEvents 指針列表:list<IPrimeEvents*>。CEventMgr 中是一個模板成員函數:Raise<F>,它將仿函數參數 F 傳遞給 for_each。所以當你寫下面這條語句時:

m_eventmgr.Raise(IPrimeEvents::Progress(nFound));

編譯器明白你試圖以 IPrimeEvents::Progress 類型參數調用 CEventMgr::Raise,於是它用模板產生成員函數 CEventMgr::Raise(IPrimeEvents::Progress)。實現代碼將仿函數實例傳遞給 for_each,它為客戶機列表中的每個 I* 對象調用仿函數的 operator()。仿函數調用對象的 OnProgress 處理例程——這就是我想要的!模板不是很酷嗎?

我們已經快到終點了。仿函數讓我參數化事件方法並使用 for_each,但它們還是太長,我討厭敲入太多的東西。所以最後一步是引入一些宏來解決這個問題。下面就是 IPrimeEvents 最終的濃縮定義。

class IPrimeEvents {
 DECLARE_EVENTS(IPrimeEvents);
public:
 DEFINE_EVENT1(IPrimeEvents, Progress, UINT /*nFound*/);
 DEFINE_EVENT0(IPrimeEvents, Done);
};
IMPLEMENT_EVENTS(IPrimeEvents);

完整的源代碼參見 Figure 4 —— 從代碼中你可以體會到我竭盡全力進行精簡和濃縮。只留下最基本的信息:每個事件處理器的名字和簽名。宏假設 Foo 的事件處理器是 OnFoo。一些編程的唯美主義者不喜歡宏,但我不那樣。有工具為什麼不使用呢?DECLARE_EVENTS 聲明構造函數和析構函數;IMPLEMENT_EVENTS 實現內聯析構。宏 DEFINE_EVENT0,DEFINE_EVENT1 以及 DEFINE_EVENT2 分別聲明和定義了 OnFoo 事件處理器以及不帶參數,帶一個參數和帶兩個參數的 Foo 事件仿函數。如果你需要更多的參數,可以定義一個結構,用一個事件參數來傳遞此結構的指針:

MumbleArgs args;
args.a = 1;
args.b = 2;
// etc.
m_eventMgr.Raise(IMyEvents::Mumble(&args));

還有一種選擇,你可以實現 DEFINE_EVENT3。但是記住:仿函數對象通過值傳遞的,所以它們應該很小。當可以傳遞指針時,為什麼要在堆區和棧區來回拷貝一大堆參數呢?如果事件處理器需要返回值,也可以借助結構。為了簡單起見,我讓事件處理器返回 void。

經常有程序員會問仿函數會不會帶來太大的額外開銷。事實上,仿函數通常比函數更有效率。理由是它是內聯的。當你在 C++ 中傳遞指向函數的指針時,即使你將函數定義為內聯,它就是一個指針。你不能通過傳值的方式來傳遞一個函數。但是當你傳遞一個對象實例到某個模板函數時,如果你象那樣定義函數,編譯器產生的所有東西都是內聯的。對於事件來說,通過指針仍然只有一個函數調用,它發生在函數 operator 調用虛擬 OnFoo 處理器的時候。

  1. 上一頁:
  2. 下一頁:
欄目導航
Copyright © 程式師世界 All Rights Reserved