這篇文章來自於 A Deeper Look at Signals and Slots,Scott Collins 2005.12.19。需要說明的是,我們這裡所說的“信號槽”不僅僅是指 Qt 庫裡面的信號槽,而是站在一個全局的高度,從系統的角度來理解信號槽。所以在這篇文章中,Qt 信號槽僅僅作為一種實現來介紹,我們還將介紹另外一種信號槽的實現——boost::signal。因此,當你在文章中看到一些信號的名字時,或許僅僅是為了描述方便而杜撰的,實際並沒有這個信號。
什麼是信號槽?
這個問題我們可以從兩個角度來回答,一個簡短一些,另外一個則長些。
讓我們先用最簡潔的語言來回答這個問題——什麼是信號槽?
信號和槽是多對多的關系。一個信號可以連接多個槽,而一個槽也可以監聽多個信號。
信號可以有附加信息。例如,窗口關閉的時候可能發出 windowClosing 信號,而這個信號就可以包含著窗口的句柄,用來表明究竟是哪個窗口發出這個信號;一個滑塊在滑動時可能發出一個信號,而這個信號包含滑塊的具體位置,或者新的值等等。我們可以把信號槽理解成函數簽名。信號只能同具有相同簽名的槽連接起來。你可以把信號看成是底層事件的一個形象的名字。比如這個 windowClosing 信號,我們就知道這是窗口關閉事件發生時會發出的。
信號槽實際是與語言無關的,有很多方法都可以實現信號槽,不同的實現機制會導致信號槽差別很大。信號槽這一術語最初來自 Trolltech 公司的 Qt 庫現在已經被 Nokia 收購)。1994年,Qt 的第一個版本發布,為我們帶來了信號槽的概念。這一概念立刻引起計算機科學界的注意,提出了多種不同的實現。如今,信號槽依然是 Qt 庫的核心之一,其他許多庫也提供了類似的實現,甚至出現了一些專門提供這一機制的工具庫。
簡單了解信號槽之後,我們再來從另外一個角度回答這個問題:什麼是信號槽?它們從何而來?
前面我們已經了解了信號槽相關的概念。下面我們將從更細致的角度來探討,信號槽機制是怎樣一步步發展的,以及怎樣在你自己的代碼中使用它們。
程序設計中很重要的一部分是組件交互:系統的一部分需要告訴另一部分去完成一些操作。讓我們從一個簡單的例子開始:
// C++ class Button { public: void clicked(); // something that happens: Buttons may be clicked }; class Page { public: void reload(); // ...which I might want to do when a Button is clicked };
換句話說,Page 類知道如何重新載入頁面reload),Button 有一個動作是點擊click)。假設我們有一個函數返回當前頁面 currentPage(),那麼,當 button 被點擊的時候,當前頁面應該被重新載入。
// C++ --- making the connection directly void Button::clicked() { currentPage()->reload(); // Buttons know exactly what to do when clicked }
這看起來並不很好。因為 Button 這個類名似乎暗示了這是一個可重用的類,但是這個類的點擊操作卻同 Page 緊緊地耦合在一起了。這使得只要 button 一被點擊,必定調用 currentPage() 的 reload() 函數。這根本不能被重用,或許把它改名叫 PageReloadButton 更好一些。
實際上,不得不說,這確實是一種實現方式。如果 Button::click() 這個函數是 virtual 的,那麼你完全可以寫一個新類去繼承這個 Button:
// C++ --- connecting to different actions by specializing class Button { public: virtual void clicked() = 0; // Buttons have no idea what to do when clicked }; class PageReloadButton : public Button { public: virtual void clicked() { currentPage()->reload(); // ...specialize Button to connect it to a specific action } };
好了,現在 Button 可以被重用了。但是這並不是一個很好的解決方案。
引入回調
讓我們停下來,回想一下在只有 C 的時代,我們該如何解決這個問題。如果只有 C,就不存在 virtual 這種東西。重用有很多種方式,但是由於沒有了類的幫助,我們采用另外的解決方案:函數指針。
/* C --- connecting to different actions via function pointers */ void reloadPage_action( void* ) /* one possible action when a Button is clicked */ { reloadPage(currentPage()); } void loadPage_action( void* url ) /* another possible action when a Button is clicked */ { loadPage(currentPage(), (char*)url); } struct Button { /* ...now I keep a (changeable) pointer to the function to be called */ void (*actionFunc_)(); void* actionFuncData_; }; void buttonClicked( Button* button ) { /* call the attached function, whatever it might be */ if ( button && button->actionFunc_ ) (*button->actionFunc_)(button->actionFuncData_); }
這就是通常所說的“回調”。buttonClicked() 函數在編譯期並不知道要調用哪一個函數。被調用的函數是在運行期傳進來的。這樣,我們的 Button 就可以被重用了,因為我們可以在運行時將不同的函數指針傳遞進來,從而獲得不同的點擊操作。
增加類型安全
對於 C++ 或者 Java 程序員來說,總是不喜歡這麼做。因為這不是類型安全的注意 url 有一步強制類型轉換)。
我們為什麼需要類型安全呢?一個對象的類型其實暗示了你將如何使用這個對象。有了明確的對象類型,你就可以讓編譯器幫助你檢查你的代碼是不是被正確的使用了,如同你畫了一個邊界,告訴編譯器說,如果有人越界,就要報錯。然而,如果沒有類型安全,你就丟失了這種優勢,編譯器也就不能幫助你完成這種維護。這就如同你開車一樣。只要你的速度足夠,你就可以讓你的汽車飛起來,但是,一般來說,這種速度就會提醒你,這太不安全了。同時還會有一些裝置,比如雷達之類,也會時時幫你檢查這種情況。這就如同編譯器幫我們做的那樣,是我們出浴一種安全使用的范圍內。
回過來再看看我們的代碼。使用 C 不是類型安全的,但是使用 C++,我們可以把回調的函數指針和數據放在一個類裡面,從而獲得類型安全的優勢。例如:
// re-usable actions, C++ style (callback objects) class AbstractAction { public: virtual void execute() = 0; // sub-classes re-implement this to actually do something }; class Button { // ...now I keep a (changeable) pointer to the action to be executed AbstractAction* action_; }; void Button::clicked() { // execute the attached action, whatever it may be if ( action_ ) action_->execute(); } class PageReloadAction : public AbstractAction // one possible action when a Button is clicked { public: virtual void execute() { currentPage()->reload(); } }; class PageLoadAction : public AbstractAction // another possible action when a Button is clicked { public: // ... virtual void execute() { currentPage()->load(url_); } private: std::string url_; };
好了!我們的 Button 已經可以很方便的重用了,並且也是類型安全的,再也沒有了強制類型轉換。這種實現已經可以解決系統中遇到的絕大部分問題了。似乎現在的解決方案同前面的類似,都是繼承了一個類。只不過現在我們對動作進行了抽象,而之前是對 Button 進行的抽象。這很像前面 C 的實現,我們將不同的動作和 Button 關聯起來。現在,我們一步步找到一種比較令人滿意的方法。
本文出自 “豆子空間” 博客,請務必保留此出處http://devbean.blog.51cto.com/448512/417658