西西軟件園多重安全檢測下載網(wǎng)站、值得信賴的軟件下載站!
軟件
軟件
文章
搜索

首頁編程開發(fā)其它知識 → 什么是信號槽?深入理解信號槽

什么是信號槽?深入理解信號槽

相關(guān)軟件相關(guān)文章發(fā)表評論 來源:豆子空間時間:2010/11/19 11:47:50字體大。A-A+

作者:FinderCheng點擊:480次評論:0次標簽: 信號槽 Qt信號 Boost

  • 類型:游戲其他大。6.6M語言:多國語言[中文] 評分:3.7
  • 標簽:
立即下載
這篇文章來自于 A Deeper Look at Signals and Slots,Scott Collins 2005.12.19。需要說明的是,我們這里所說的“信號槽”不僅僅是指 Qt 庫里面的信號槽,而是站在一個全局的高度,從系統(tǒng)的角度來理解信號槽。所以在這篇文章中,Qt 信號槽僅僅作為一種實現(xiàn)來介紹,我們還將介紹另外一種信號槽的實現(xiàn)——boost::signal。因此,當(dāng)你在文章中看到一些信號的名字時,或許僅僅是為了描述方便而杜撰的,實際并沒有這個信號。

什么是信號槽?

這個問題我們可以從兩個角度來回答,一個簡短一些,另外一個則長些。

讓我們先用最簡潔的語言來回答這個問題——什么是信號槽?

信號槽是觀察者模式的一種實現(xiàn),或者說是一種升華;
一個信號就是一個能夠被觀察的事件,或者至少是事件已經(jīng)發(fā)生的一種通知;
一個槽就是一個觀察者,通常就是在被觀察的對象發(fā)生改變的時候——也可以說是信號發(fā)出的時候——被調(diào)用的函數(shù);
你可以將信號和槽連接起來,形成一種觀察者-被觀察者的關(guān)系;
當(dāng)事件或者狀態(tài)發(fā)生改變的時候,信號就會被發(fā)出;同時,信號發(fā)出者有義務(wù)調(diào)用所有注冊的對這個事件(信號)感興趣的函數(shù)(槽)。
信號和槽是多對多的關(guān)系。一個信號可以連接多個槽,而一個槽也可以監(jiān)聽多個信號。

信號可以有附加信息。例如,窗口關(guān)閉的時候可能發(fā)出 windowClosing 信號,而這個信號就可以包含著窗口的句柄,用來表明究竟是哪個窗口發(fā)出這個信號;一個滑塊在滑動時可能發(fā)出一個信號,而這個信號包含滑塊的具體位置,或者新的值等等。我們可以把信號槽理解成函數(shù)簽名。信號只能同具有相同簽名的槽連接起來。你可以把信號看成是底層事件的一個形象的名字。比如這個 windowClosing 信號,我們就知道這是窗口關(guān)閉事件發(fā)生時會發(fā)出的。

信號槽實際是與語言無關(guān)的,有很多方法都可以實現(xiàn)信號槽,不同的實現(xiàn)機制會導(dǎo)致信號槽差別很大。信號槽這一術(shù)語最初來自 Trolltech 公司的 Qt 庫(現(xiàn)在已經(jīng)被 Nokia 收購)。1994年,Qt 的第一個版本發(fā)布,為我們帶來了信號槽的概念。這一概念立刻引起計算機科學(xué)界的注意,提出了多種不同的實現(xiàn)。如今,信號槽依然是 Qt 庫的核心之一,其他許多庫也提供了類似的實現(xiàn),甚至出現(xiàn)了一些專門提供這一機制的工具庫。

簡單了解信號槽之后,我們再來從另外一個角度回答這個問題:什么是信號槽?它們從何而來?

前面我們已經(jīng)了解了信號槽相關(guān)的概念。下面我們將從更細致的角度來探討,信號槽機制是怎樣一步步發(fā)展的,以及怎樣在你自己的代碼中使用它們。

程序設(shè)計中很重要的一部分是組件交互:系統(tǒng)的一部分需要告訴另一部分去完成一些操作。讓我們從一個簡單的例子開始:

// 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)。假設(shè)我們有一個函數(shù)返回當(dāng)前頁面 currentPage(),那么,當(dāng) button 被點擊的時候,當(dāng)前頁面應(yīng)該被重新載入。

// C++ --- making the connection directly
void Button::clicked()
{
currentPage()->reload(); // Buttons know exactly what to do when clicked
}這看起來并不很好。因為 Button 這個類名似乎暗示了這是一個可重用的類,但是這個類的點擊操作卻同 Page 緊緊地耦合在一起了。這使得只要 button 一被點擊,必定調(diào)用 currentPage() 的 reload() 函數(shù)。這根本不能被重用,或許把它改名叫 PageReloadButton 更好一些。

實際上,不得不說,這確實是一種實現(xiàn)方式。如果 Button::click() 這個函數(shù)是 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
}
};好了,現(xiàn)在 Button 可以被重用了。但是這并不是一個很好的解決方案。

引入回調(diào)

讓我們停下來,回想一下在只有 C 的時代,我們該如何解決這個問題。如果只有 C,就不存在 virtual 這種東西。重用有很多種方式,但是由于沒有了類的幫助,我們采用另外的解決方案:函數(shù)指針。

/* 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_);
}這就是通常所說的“回調(diào)”。buttonClicked() 函數(shù)在編譯期并不知道要調(diào)用哪一個函數(shù)。被調(diào)用的函數(shù)是在運行期傳進來的。這樣,我們的 Button 就可以被重用了,因為我們可以在運行時將不同的函數(shù)指針傳遞進來,從而獲得不同的點擊操作。

增加類型安全

對于 C++ 或者 Java 程序員來說,總是不喜歡這么做。因為這不是類型安全的(注意 url 有一步強制類型轉(zhuǎn)換)。

我們?yōu)槭裁葱枰愋桶踩?一個對象的類型其實暗示了你將如何使用這個對象。有了明確的對象類型,你就可以讓編譯器幫助你檢查你的代碼是不是被正確的使用了,如同你畫了一個邊界,告訴編譯器說,如果有人越界,就要報錯。然而,如果沒有類型安全,你就丟失了這種優(yōu)勢,編譯器也就不能幫助你完成這種維護。這就如同你開車一樣。只要你的速度足夠,你就可以讓你的汽車飛起來,但是,一般來說,這種速度就會提醒你,這太不安全了。同時還會有一些裝置,比如雷達之類,也會時時幫你檢查這種情況。這就如同編譯器幫我們做的那樣,是我們出浴一種安全使用的范圍內(nèi)。

回過來再看看我們的代碼。使用 C 不是類型安全的,但是使用 C++,我們可以把回調(diào)的函數(shù)指針和數(shù)據(jù)放在一個類里面,從而獲得類型安全的優(yōu)勢。例如:

// 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 已經(jīng)可以很方便的重用了,并且也是類型安全的,再也沒有了強制類型轉(zhuǎn)換。這種實現(xiàn)已經(jīng)可以解決系統(tǒng)中遇到的絕大部分問題了。似乎現(xiàn)在的解決方案同前面的類似,都是繼承了一個類。只不過現(xiàn)在我們對動作進行了抽象,而之前是對 Button 進行的抽象。這很像前面 C 的實現(xiàn),我們將不同的動作和 Button 關(guān)聯(lián)起來,F(xiàn)在,我們一步步找到一種比較令人滿意的方法。

多對多

下一個問題是,我們能夠在點擊一次重新載入按鈕之后做多個操作嗎?也就是讓信號和槽實現(xiàn)多對多的關(guān)系?

實際上,我們只需要利用一個普通的鏈表,就可以輕松實現(xiàn)這個功能了。比如,如下的實現(xiàn):

class MultiAction : public AbstractAction
// ...an action that is composed of zero or more other actions;
// executing it is really executing each of the sub-actions
{
public:
// ...
virtual void execute();
private:
std::vector<AbstractAction*> actionList_;
// ...or any reasonable collection machinery
};

void MultiAction::execute()
{
// call execute() on each action in actionList_
std::for_each( actionList_.begin(),
actionList_.end(),
boost::bind(&AbstractAction::execute, _1) );
}這就是其中的一種實現(xiàn)。不要覺得這種實現(xiàn)看上去沒什么水平,實際上我們發(fā)現(xiàn)這就是一種相當(dāng)簡潔的方法。同時,不要糾結(jié)于我們代碼中的 std:: 和 boost:: 這些命名空間,你完全可以用另外的類,強調(diào)一下,這只是一種可能的實現(xiàn),F(xiàn)在,我們的一個動作可以連接多個 button 了,當(dāng)然,也可以是別的 Action 的使用者,F(xiàn)在,我們有了一個多對多的機制。通過將 AbstractAction* 替換成 boost::shared_ptr<AbstractAction>,可以解決 AbstractAction 的歸屬問題,同時保持原有的多對多的關(guān)系。

這會有很多的類!

如果你在實際項目中使用上面的機制,很多就會發(fā)現(xiàn),我們必須為每一個 action 定義一個類,這將不可避免地引起類爆炸。至今為止,我們前面所說的所有實現(xiàn)都存在這個問題。不過,我們之后將著重討論這個問題,現(xiàn)在先不要糾結(jié)在這里啦!

特化!特化!

當(dāng)我們開始工作的時候,我們通過將每一個 button 賦予不同的 action,實現(xiàn) Button 類的重用。這實際是一種特化。然而,我們的問題是,action 的特化被放在了固定的類層次中,在這里就是這些 Button 類。這意味著,我們的 action 很難被更大規(guī)模的重用,因為每一個 action 實際都是與 Button 類綁定的。那么,我們換個思路,能不能將這種特化放到信號與槽連接的時候進行呢?這樣,action 和 button 這兩者都不必進行特化了。

函數(shù)對象

將一個類的函數(shù)進行一定曾度的封裝,這個思想相當(dāng)有用。實際上,我們的 Action 類的存在,就是將 execute() 這個函數(shù)進行封裝,其他別無用處。這在 C++ 里面還是比較普遍的,很多時候我們用 ++ 的特性重新封裝函數(shù),讓類的行為看起來就像函數(shù)一樣。例如,我們重載 operator() 運算符,就可以讓類看起來很像一個函數(shù):

class AbstractAction
{
public:
virtual void operator()() = 0;
};

// using an action (given AbstractAction& action)
action();這樣,我們的類看起來很像函數(shù)。前面代碼中的 for_each 也得做相應(yīng)的改變:

// previously
std::for_each( actionList_.begin(),
actionList_.end(),
boost::bind(&AbstractAction::execute, _1) );
// now
std::for_each( actionList_.begin(),
actionList_.end(),
boost::bind(&AbstractAction::operator(), _1) );現(xiàn)在,我們的 Button::clicked() 函數(shù)的實現(xiàn)有了更多的選擇:

// previously
action_->execute();

// option 1: use the dereferenced pointer like a function
(*action_)();

// option 2: call the function by its new name
action_->operator()();看起來很麻煩,值得這樣做嗎?

下面我們來試著解釋一下信號和槽的目的?瓷先ィ貙 operator() 運算符有些過分,并不值得我們?nèi)ミ@么做。但是,要知道,在某些問題上,你提供的可用的解決方案越多,越有利于我們編寫更簡潔的代碼。通過對一些類進行規(guī)范,就像我們要讓函數(shù)對象看起來更像函數(shù),我們可以讓它們在某些環(huán)境下更適合重用。在使用模板編程,或者是 Boost.Function,bind 或者是模板元編程的情形下,這一點尤為重要。

這是對無需更多特化建立信號槽連接重要性的部分回答。模板就提供了這樣一種機制,讓添加了特化參數(shù)的代碼并不那么難地被特化,正如我們的函數(shù)對象那樣。而模板的特化對于使用者而言是透明的。

松耦合

現(xiàn)在,讓我們回顧一下我們之前的種種做法。

我們執(zhí)著地尋求一種能夠在同一個地方調(diào)用不同函數(shù)的方法,這實際上是 C++ 內(nèi)置的功能之一,通過 virtual 關(guān)鍵字,當(dāng)然,我們也可以使用函數(shù)指針實現(xiàn)。當(dāng)我們需要調(diào)用的函數(shù)沒有一個合適的簽名,我們將它包裝成一個類。我們已經(jīng)演示了如何在同一地方調(diào)用多個函數(shù),至少我們知道有這么一種方法(但這并不是在編譯期完成的)。我們實現(xiàn)了讓“信號發(fā)送”能夠被若干個不同的“槽”監(jiān)聽。

不過,我們的系統(tǒng)的確沒有什么非常與眾不同的地方。我們來仔細審核一下我們的系統(tǒng),它真正不同的是:

定義了兩個不同的術(shù)語:“信號”和“槽”;
在一個調(diào)用點(信號)與零個或者多個回調(diào)(槽)相連;
連接的焦點從提供者處移開,更多地轉(zhuǎn)向消費者(也就是說,Button 并不需要知道如何做是正確的,而是由回調(diào)函數(shù)去告知 Button,你需要調(diào)用我)。
但是,這樣的系統(tǒng)還遠達不到松耦合的關(guān)系。Button 類并不需要知道 Page 類。松耦合意味著更少的依賴;依賴越少,組件的可重用性也就越高。

當(dāng)然,肯定需要有組件同時知道 Button 和 Page,從而完成對它們的連接。現(xiàn)在,我們的連接實際是用代碼描述的,如果我們不用代碼,而用數(shù)據(jù)描述連接呢?這么一來,我們就有了松耦合的類,從而提高二者的可重用性。

新的連接模式

什么樣的連接模式才算是非代碼描述呢?假如僅僅只有一種信號槽的簽名,例如 void (*signature)(),這并不能實現(xiàn)。使用散列表,將信號的名字映射到匹配的連接函數(shù),將槽的名字映射到匹配的函數(shù)指針,這樣的一對字符串即可建立一個連接。

然而,這種實現(xiàn)其實包含一些“握手”協(xié)議。我們的確希望具有多種信號槽的簽名。在信號槽的簡短回答中我們提到,信號可以攜帶附加信息。這要求信號具有參數(shù)。我們并沒有處理成員函數(shù)與非成員函數(shù)的不同,這又是一種潛在的函數(shù)簽名的不同。我們還沒有決定,我們是直接將信號連接到槽函數(shù)上,還是連接到一個包裝器上。如果是包裝器,這個包裝器需要已經(jīng)存在呢,還是我們在需要時自動創(chuàng)建呢?雖然底層思想很簡單,但是,真正的實現(xiàn)還需要很好的努力才行。似乎通過類名能夠創(chuàng)建對象是一種不錯的想法,這取決于你的實現(xiàn)方式,有時候甚至取決于你有沒有能力做出這種實現(xiàn)。將信號和槽放入散列表需要一種注冊機制。一旦有了這么一種系統(tǒng),前面所說的“有太多類了”的問題就得以解決了。你所需要做的就是維護這個散列表的鍵值,并且在需要的時候?qū)嵗悺?br />
給信號槽添加這樣的能力將比我們前面所做的所有工作都困難得多。在由鍵值進行連接時,多數(shù)實現(xiàn)都會選擇放棄編譯期類型安全檢查,以滿足信號和槽的兼容。這樣的系統(tǒng)代價更高,但是其應(yīng)用也遠遠高于自動信號槽連接。這樣的系統(tǒng)允許實例化外部的類,比如 Button 以及它的連接。所以,這樣的系統(tǒng)有很強大的能力,它能夠完成一個類的裝配、連接,并最終完成實例化操作,比如直接從資源描述文件中導(dǎo)出的一個對話框。既然它能夠憑借名字使函數(shù)可用,這就是一種腳本能力。如果你需要上面所說的種種特性,那么,完成這么一套系統(tǒng)絕對是值得的,你的信號槽系統(tǒng)也會從中受益,由數(shù)據(jù)去完成信號槽的連接。

對于不需要這種能力的實現(xiàn)則會忽略這部分特性。從這點看,這種實現(xiàn)就是“輕量級”的。對于一個需要這些特性的庫而言,完整地實現(xiàn)出來就是一個輕量級實現(xiàn)。這也是區(qū)別這些實現(xiàn)的方法之一。

信號槽的實現(xiàn)實例—— Qt 和 Boost

Qt 的信號槽和 Boost.Signals 由于有著截然不同的設(shè)計目標,因此二者的實現(xiàn)、強度也十分不同。將二者混合在一起使用也不是不可能的,我們將在本系統(tǒng)的最后一部分來討論這個問題。

    相關(guān)評論

    閱讀本文后您有什么感想? 已有人給出評價!

    • 8 喜歡喜歡
    • 3 頂
    • 1 難過難過
    • 5 囧
    • 3 圍觀圍觀
    • 2 無聊無聊

    熱門評論

    最新評論

    發(fā)表評論 查看所有評論(0)

    昵稱:
    表情: 高興 可 汗 我不要 害羞 好 下下下 送花 屎 親親
    字數(shù): 0/500 (您的評論需要經(jīng)過審核才能顯示)