
Game Booster(優(yōu)化電腦游戲性能)v2.2 高級(jí)注冊(cè)版
- 類型:游戲其他大小:6.6M語(yǔ)言:多國(guó)語(yǔ)言[中文] 評(píng)分:3.7
- 標(biāo)簽:
什么是信號(hào)槽?
這個(gè)問(wèn)題我們可以從兩個(gè)角度來(lái)回答,一個(gè)簡(jiǎn)短一些,另外一個(gè)則長(zhǎng)些。
讓我們先用最簡(jiǎn)潔的語(yǔ)言來(lái)回答這個(gè)問(wèn)題——什么是信號(hào)槽?
信號(hào)槽是觀察者模式的一種實(shí)現(xiàn),或者說(shuō)是一種升華;
一個(gè)信號(hào)就是一個(gè)能夠被觀察的事件,或者至少是事件已經(jīng)發(fā)生的一種通知;
一個(gè)槽就是一個(gè)觀察者,通常就是在被觀察的對(duì)象發(fā)生改變的時(shí)候——也可以說(shuō)是信號(hào)發(fā)出的時(shí)候——被調(diào)用的函數(shù);
你可以將信號(hào)和槽連接起來(lái),形成一種觀察者-被觀察者的關(guān)系;
當(dāng)事件或者狀態(tài)發(fā)生改變的時(shí)候,信號(hào)就會(huì)被發(fā)出;同時(shí),信號(hào)發(fā)出者有義務(wù)調(diào)用所有注冊(cè)的對(duì)這個(gè)事件(信號(hào))感興趣的函數(shù)(槽)。
信號(hào)和槽是多對(duì)多的關(guān)系。一個(gè)信號(hào)可以連接多個(gè)槽,而一個(gè)槽也可以監(jiān)聽(tīng)多個(gè)信號(hào)。
信號(hào)可以有附加信息。例如,窗口關(guān)閉的時(shí)候可能發(fā)出 windowClosing 信號(hào),而這個(gè)信號(hào)就可以包含著窗口的句柄,用來(lái)表明究竟是哪個(gè)窗口發(fā)出這個(gè)信號(hào);一個(gè)滑塊在滑動(dòng)時(shí)可能發(fā)出一個(gè)信號(hào),而這個(gè)信號(hào)包含滑塊的具體位置,或者新的值等等。我們可以把信號(hào)槽理解成函數(shù)簽名。信號(hào)只能同具有相同簽名的槽連接起來(lái)。你可以把信號(hào)看成是底層事件的一個(gè)形象的名字。比如這個(gè) windowClosing 信號(hào),我們就知道這是窗口關(guān)閉事件發(fā)生時(shí)會(huì)發(fā)出的。
信號(hào)槽實(shí)際是與語(yǔ)言無(wú)關(guān)的,有很多方法都可以實(shí)現(xiàn)信號(hào)槽,不同的實(shí)現(xiàn)機(jī)制會(huì)導(dǎo)致信號(hào)槽差別很大。信號(hào)槽這一術(shù)語(yǔ)最初來(lái)自 Trolltech 公司的 Qt 庫(kù)(現(xiàn)在已經(jīng)被 Nokia 收購(gòu))。1994年,Qt 的第一個(gè)版本發(fā)布,為我們帶來(lái)了信號(hào)槽的概念。這一概念立刻引起計(jì)算機(jī)科學(xué)界的注意,提出了多種不同的實(shí)現(xiàn)。如今,信號(hào)槽依然是 Qt 庫(kù)的核心之一,其他許多庫(kù)也提供了類似的實(shí)現(xiàn),甚至出現(xiàn)了一些專門(mén)提供這一機(jī)制的工具庫(kù)。
簡(jiǎn)單了解信號(hào)槽之后,我們?cè)賮?lái)從另外一個(gè)角度回答這個(gè)問(wèn)題:什么是信號(hào)槽?它們從何而來(lái)?
前面我們已經(jīng)了解了信號(hào)槽相關(guān)的概念。下面我們將從更細(xì)致的角度來(lái)探討,信號(hào)槽機(jī)制是怎樣一步步發(fā)展的,以及怎樣在你自己的代碼中使用它們。
程序設(shè)計(jì)中很重要的一部分是組件交互:系統(tǒng)的一部分需要告訴另一部分去完成一些操作。讓我們從一個(gè)簡(jiǎn)單的例子開(kāi)始:
// 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
};換句話說(shuō),Page 類知道如何重新載入頁(yè)面(reload),Button 有一個(gè)動(dòng)作是點(diǎn)擊(click)。假設(shè)我們有一個(gè)函數(shù)返回當(dāng)前頁(yè)面 currentPage(),那么,當(dāng) button 被點(diǎn)擊的時(shí)候,當(dāng)前頁(yè)面應(yīng)該被重新載入。
// C++ --- making the connection directly
void Button::clicked()
{
currentPage()->reload(); // Buttons know exactly what to do when clicked
}這看起來(lái)并不很好。因?yàn)?Button 這個(gè)類名似乎暗示了這是一個(gè)可重用的類,但是這個(gè)類的點(diǎn)擊操作卻同 Page 緊緊地耦合在一起了。這使得只要 button 一被點(diǎn)擊,必定調(diào)用 currentPage() 的 reload() 函數(shù)。這根本不能被重用,或許把它改名叫 PageReloadButton 更好一些。
實(shí)際上,不得不說(shuō),這確實(shí)是一種實(shí)現(xiàn)方式。如果 Button::click() 這個(gè)函數(shù)是 virtual 的,那么你完全可以寫(xiě)一個(gè)新類去繼承這個(gè) 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 可以被重用了。但是這并不是一個(gè)很好的解決方案。
引入回調(diào)
讓我們停下來(lái),回想一下在只有 C 的時(shí)代,我們?cè)撊绾谓鉀Q這個(gè)問(wèn)題。如果只有 C,就不存在 virtual 這種東西。重用有很多種方式,但是由于沒(méi)有了類的幫助,我們采用另外的解決方案:函數(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_);
}這就是通常所說(shuō)的“回調(diào)”。buttonClicked() 函數(shù)在編譯期并不知道要調(diào)用哪一個(gè)函數(shù)。被調(diào)用的函數(shù)是在運(yùn)行期傳進(jìn)來(lái)的。這樣,我們的 Button 就可以被重用了,因?yàn)槲覀兛梢栽谶\(yùn)行時(shí)將不同的函數(shù)指針傳遞進(jìn)來(lái),從而獲得不同的點(diǎn)擊操作。
增加類型安全
對(duì)于 C++ 或者 Java 程序員來(lái)說(shuō),總是不喜歡這么做。因?yàn)檫@不是類型安全的(注意 url 有一步強(qiáng)制類型轉(zhuǎn)換)。
我們?yōu)槭裁葱枰愋桶踩兀恳粋(gè)對(duì)象的類型其實(shí)暗示了你將如何使用這個(gè)對(duì)象。有了明確的對(duì)象類型,你就可以讓編譯器幫助你檢查你的代碼是不是被正確的使用了,如同你畫(huà)了一個(gè)邊界,告訴編譯器說(shuō),如果有人越界,就要報(bào)錯(cuò)。然而,如果沒(méi)有類型安全,你就丟失了這種優(yōu)勢(shì),編譯器也就不能幫助你完成這種維護(hù)。這就如同你開(kāi)車(chē)一樣。只要你的速度足夠,你就可以讓你的汽車(chē)飛起來(lái),但是,一般來(lái)說(shuō),這種速度就會(huì)提醒你,這太不安全了。同時(shí)還會(huì)有一些裝置,比如雷達(dá)之類,也會(huì)時(shí)時(shí)幫你檢查這種情況。這就如同編譯器幫我們做的那樣,是我們出浴一種安全使用的范圍內(nèi)。
回過(guò)來(lái)再看看我們的代碼。使用 C 不是類型安全的,但是使用 C++,我們可以把回調(diào)的函數(shù)指針和數(shù)據(jù)放在一個(gè)類里面,從而獲得類型安全的優(yōu)勢(shì)。例如:
// 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)可以很方便的重用了,并且也是類型安全的,再也沒(méi)有了強(qiáng)制類型轉(zhuǎn)換。這種實(shí)現(xiàn)已經(jīng)可以解決系統(tǒng)中遇到的絕大部分問(wèn)題了。似乎現(xiàn)在的解決方案同前面的類似,都是繼承了一個(gè)類。只不過(guò)現(xiàn)在我們對(duì)動(dòng)作進(jìn)行了抽象,而之前是對(duì) Button 進(jìn)行的抽象。這很像前面 C 的實(shí)現(xiàn),我們將不同的動(dòng)作和 Button 關(guān)聯(lián)起來(lái),F(xiàn)在,我們一步步找到一種比較令人滿意的方法。
多對(duì)多
下一個(gè)問(wèn)題是,我們能夠在點(diǎn)擊一次重新載入按鈕之后做多個(gè)操作嗎?也就是讓信號(hào)和槽實(shí)現(xiàn)多對(duì)多的關(guān)系?
實(shí)際上,我們只需要利用一個(gè)普通的鏈表,就可以輕松實(shí)現(xiàn)這個(gè)功能了。比如,如下的實(shí)現(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) );
}這就是其中的一種實(shí)現(xiàn)。不要覺(jué)得這種實(shí)現(xiàn)看上去沒(méi)什么水平,實(shí)際上我們發(fā)現(xiàn)這就是一種相當(dāng)簡(jiǎn)潔的方法。同時(shí),不要糾結(jié)于我們代碼中的 std:: 和 boost:: 這些命名空間,你完全可以用另外的類,強(qiáng)調(diào)一下,這只是一種可能的實(shí)現(xiàn),F(xiàn)在,我們的一個(gè)動(dòng)作可以連接多個(gè) button 了,當(dāng)然,也可以是別的 Action 的使用者,F(xiàn)在,我們有了一個(gè)多對(duì)多的機(jī)制。通過(guò)將 AbstractAction* 替換成 boost::shared_ptr<AbstractAction>,可以解決 AbstractAction 的歸屬問(wèn)題,同時(shí)保持原有的多對(duì)多的關(guān)系。
這會(huì)有很多的類!
如果你在實(shí)際項(xiàng)目中使用上面的機(jī)制,很多就會(huì)發(fā)現(xiàn),我們必須為每一個(gè) action 定義一個(gè)類,這將不可避免地引起類爆炸。至今為止,我們前面所說(shuō)的所有實(shí)現(xiàn)都存在這個(gè)問(wèn)題。不過(guò),我們之后將著重討論這個(gè)問(wèn)題,現(xiàn)在先不要糾結(jié)在這里啦!
特化!特化!
當(dāng)我們開(kāi)始工作的時(shí)候,我們通過(guò)將每一個(gè) button 賦予不同的 action,實(shí)現(xiàn) Button 類的重用。這實(shí)際是一種特化。然而,我們的問(wèn)題是,action 的特化被放在了固定的類層次中,在這里就是這些 Button 類。這意味著,我們的 action 很難被更大規(guī)模的重用,因?yàn)槊恳粋(gè) action 實(shí)際都是與 Button 類綁定的。那么,我們換個(gè)思路,能不能將這種特化放到信號(hào)與槽連接的時(shí)候進(jìn)行呢?這樣,action 和 button 這兩者都不必進(jìn)行特化了。
函數(shù)對(duì)象
將一個(gè)類的函數(shù)進(jìn)行一定曾度的封裝,這個(gè)思想相當(dāng)有用。實(shí)際上,我們的 Action 類的存在,就是將 execute() 這個(gè)函數(shù)進(jìn)行封裝,其他別無(wú)用處。這在 C++ 里面還是比較普遍的,很多時(shí)候我們用 ++ 的特性重新封裝函數(shù),讓類的行為看起來(lái)就像函數(shù)一樣。例如,我們重載 operator() 運(yùn)算符,就可以讓類看起來(lái)很像一個(gè)函數(shù):
class AbstractAction
{
public:
virtual void operator()() = 0;
};
// using an action (given AbstractAction& action)
action();這樣,我們的類看起來(lái)很像函數(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ù)的實(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()();看起來(lái)很麻煩,值得這樣做嗎?
下面我們來(lái)試著解釋一下信號(hào)和槽的目的?瓷先,重寫(xiě) operator() 運(yùn)算符有些過(guò)分,并不值得我們?nèi)ミ@么做。但是,要知道,在某些問(wèn)題上,你提供的可用的解決方案越多,越有利于我們編寫(xiě)更簡(jiǎn)潔的代碼。通過(guò)對(duì)一些類進(jìn)行規(guī)范,就像我們要讓函數(shù)對(duì)象看起來(lái)更像函數(shù),我們可以讓它們?cè)谀承┉h(huán)境下更適合重用。在使用模板編程,或者是 Boost.Function,bind 或者是模板元編程的情形下,這一點(diǎn)尤為重要。
這是對(duì)無(wú)需更多特化建立信號(hào)槽連接重要性的部分回答。模板就提供了這樣一種機(jī)制,讓添加了特化參數(shù)的代碼并不那么難地被特化,正如我們的函數(shù)對(duì)象那樣。而模板的特化對(duì)于使用者而言是透明的。
松耦合
現(xiàn)在,讓我們回顧一下我們之前的種種做法。
我們執(zhí)著地尋求一種能夠在同一個(gè)地方調(diào)用不同函數(shù)的方法,這實(shí)際上是 C++ 內(nèi)置的功能之一,通過(guò) virtual 關(guān)鍵字,當(dāng)然,我們也可以使用函數(shù)指針實(shí)現(xiàn)。當(dāng)我們需要調(diào)用的函數(shù)沒(méi)有一個(gè)合適的簽名,我們將它包裝成一個(gè)類。我們已經(jīng)演示了如何在同一地方調(diào)用多個(gè)函數(shù),至少我們知道有這么一種方法(但這并不是在編譯期完成的)。我們實(shí)現(xiàn)了讓“信號(hào)發(fā)送”能夠被若干個(gè)不同的“槽”監(jiān)聽(tīng)。
不過(guò),我們的系統(tǒng)的確沒(méi)有什么非常與眾不同的地方。我們來(lái)仔細(xì)審核一下我們的系統(tǒng),它真正不同的是:
定義了兩個(gè)不同的術(shù)語(yǔ):“信號(hào)”和“槽”;
在一個(gè)調(diào)用點(diǎn)(信號(hào))與零個(gè)或者多個(gè)回調(diào)(槽)相連;
連接的焦點(diǎn)從提供者處移開(kāi),更多地轉(zhuǎn)向消費(fèi)者(也就是說(shuō),Button 并不需要知道如何做是正確的,而是由回調(diào)函數(shù)去告知 Button,你需要調(diào)用我)。
但是,這樣的系統(tǒng)還遠(yuǎn)達(dá)不到松耦合的關(guān)系。Button 類并不需要知道 Page 類。松耦合意味著更少的依賴;依賴越少,組件的可重用性也就越高。
當(dāng)然,肯定需要有組件同時(shí)知道 Button 和 Page,從而完成對(duì)它們的連接,F(xiàn)在,我們的連接實(shí)際是用代碼描述的,如果我們不用代碼,而用數(shù)據(jù)描述連接呢?這么一來(lái),我們就有了松耦合的類,從而提高二者的可重用性。
新的連接模式
什么樣的連接模式才算是非代碼描述呢?假如僅僅只有一種信號(hào)槽的簽名,例如 void (*signature)(),這并不能實(shí)現(xiàn)。使用散列表,將信號(hào)的名字映射到匹配的連接函數(shù),將槽的名字映射到匹配的函數(shù)指針,這樣的一對(duì)字符串即可建立一個(gè)連接。
然而,這種實(shí)現(xiàn)其實(shí)包含一些“握手”協(xié)議。我們的確希望具有多種信號(hào)槽的簽名。在信號(hào)槽的簡(jiǎn)短回答中我們提到,信號(hào)可以攜帶附加信息。這要求信號(hào)具有參數(shù)。我們并沒(méi)有處理成員函數(shù)與非成員函數(shù)的不同,這又是一種潛在的函數(shù)簽名的不同。我們還沒(méi)有決定,我們是直接將信號(hào)連接到槽函數(shù)上,還是連接到一個(gè)包裝器上。如果是包裝器,這個(gè)包裝器需要已經(jīng)存在呢,還是我們?cè)谛枰獣r(shí)自動(dòng)創(chuàng)建呢?雖然底層思想很簡(jiǎn)單,但是,真正的實(shí)現(xiàn)還需要很好的努力才行。似乎通過(guò)類名能夠創(chuàng)建對(duì)象是一種不錯(cuò)的想法,這取決于你的實(shí)現(xiàn)方式,有時(shí)候甚至取決于你有沒(méi)有能力做出這種實(shí)現(xiàn)。將信號(hào)和槽放入散列表需要一種注冊(cè)機(jī)制。一旦有了這么一種系統(tǒng),前面所說(shuō)的“有太多類了”的問(wèn)題就得以解決了。你所需要做的就是維護(hù)這個(gè)散列表的鍵值,并且在需要的時(shí)候?qū)嵗悺?br />
給信號(hào)槽添加這樣的能力將比我們前面所做的所有工作都困難得多。在由鍵值進(jìn)行連接時(shí),多數(shù)實(shí)現(xiàn)都會(huì)選擇放棄編譯期類型安全檢查,以滿足信號(hào)和槽的兼容。這樣的系統(tǒng)代價(jià)更高,但是其應(yīng)用也遠(yuǎn)遠(yuǎn)高于自動(dòng)信號(hào)槽連接。這樣的系統(tǒng)允許實(shí)例化外部的類,比如 Button 以及它的連接。所以,這樣的系統(tǒng)有很強(qiáng)大的能力,它能夠完成一個(gè)類的裝配、連接,并最終完成實(shí)例化操作,比如直接從資源描述文件中導(dǎo)出的一個(gè)對(duì)話框。既然它能夠憑借名字使函數(shù)可用,這就是一種腳本能力。如果你需要上面所說(shuō)的種種特性,那么,完成這么一套系統(tǒng)絕對(duì)是值得的,你的信號(hào)槽系統(tǒng)也會(huì)從中受益,由數(shù)據(jù)去完成信號(hào)槽的連接。
對(duì)于不需要這種能力的實(shí)現(xiàn)則會(huì)忽略這部分特性。從這點(diǎn)看,這種實(shí)現(xiàn)就是“輕量級(jí)”的。對(duì)于一個(gè)需要這些特性的庫(kù)而言,完整地實(shí)現(xiàn)出來(lái)就是一個(gè)輕量級(jí)實(shí)現(xiàn)。這也是區(qū)別這些實(shí)現(xiàn)的方法之一。
信號(hào)槽的實(shí)現(xiàn)實(shí)例—— Qt 和 Boost
Qt 的信號(hào)槽和 Boost.Signals 由于有著截然不同的設(shè)計(jì)目標(biāo),因此二者的實(shí)現(xiàn)、強(qiáng)度也十分不同。將二者混合在一起使用也不是不可能的,我們將在本系統(tǒng)的最后一部分來(lái)討論這個(gè)問(wèn)題。