https://blog.csdn.net/liuuze5/article/details/53523463html
深刻理解信號槽(一)ios
這篇文章來自於 A Deeper Look at Signals and Slots,Scott Collins 2005.12.19。須要說明的是,咱們這裏所說的「信號槽」不只僅是指 Qt 庫裏面的信號槽,而是站在一個全局的高度,從系統的角度來理解信號槽。因此在這篇文章中,Qt 信號槽僅僅做爲一種實現來介紹,咱們還將介紹另一種信號槽的實現——boost::signal。所以,當你在文章中看到一些信號的名字時,或許僅僅是爲了描述方便而杜撰的,實際並無這個信號。程序員
什麼是信號槽?編程
這個問題咱們能夠從兩個角度來回答,一個簡短一些,另一個則長些。安全
讓咱們先用最簡潔的語言來回答這個問題——什麼是信號槽?架構
信號和槽是多對多的關係。一個信號能夠鏈接多個槽,而一個槽也能夠監聽多個信號。app
信號能夠有附加信息。例如,窗口關閉的時候可能發出 windowClosing 信號,而這個信號就能夠包含着窗口的句柄,用來代表到底是哪一個窗口發出這個信號;一個滑塊在滑動時可能發出一個信號,而這個信號包含滑塊的具體位置,或者新的值等等。咱們能夠把信號槽理解成函數簽名。信號只能同具備相同簽名的槽鏈接起來。你能夠把信號當作是底層事件的一個形象的名字。好比這個 windowClosing 信號,咱們就知道這是窗口關閉事件發生時會發出的。框架
信號槽實際是與語言無關的,有不少方法均可以實現信號槽,不一樣的實現機制會致使信號槽差異很大。信號槽這一術語最初來自 Trolltech 公司的 Qt 庫(如今已經被 Nokia 收購)。1994年,Qt 的第一個版本發佈,爲咱們帶來了信號槽的概念。這一律念馬上引發計算機科學界的注意,提出了多種不一樣的實現。現在,信號槽依然是 Qt 庫的核心之一,其餘許多庫也提供了相似的實現,甚至出現了一些專門提供這一機制的工具庫。ide
簡單瞭解信號槽以後,咱們再來從另一個角度回答這個問題:什麼是信號槽?它們從何而來?函數
前面咱們已經瞭解了信號槽相關的概念。下面咱們將從更細緻的角度來探討,信號槽機制是怎樣一步步發展的,以及怎樣在你本身的代碼中使用它們。
程序設計中很重要的一部分是組件交互:系統的一部分須要告訴另外一部分去完成一些操做。讓咱們從一個簡單的例子開始:
|
換句話說,Page 類知道如何從新載入頁面(reload),Button 有一個動做是點擊(click)。假設咱們有一個函數返回當前頁面 currentPage(),那麼,當 button 被點擊的時候,當前頁面應該被從新載入。
|
這看起來並不很好。由於 Button 這個類名彷佛暗示了這是一個可重用的類,可是這個類的點擊操做卻同 Page 牢牢地耦合在一塊兒了。這使得只要 button 一被點擊,一定調用 currentPage() 的 reload() 函數。這根本不能被重用,或許把它更名叫 PageReloadButton 更好一些。
實際上,不得不說,這確實是一種實現方式。若是 Button::click() 這個函數是 virtual 的,那麼你徹底能夠寫一個新類去繼承這個 Button:
|
好了,如今 Button 能夠被重用了。可是這並非一個很好的解決方案。
引入回調
讓咱們停下來,回想一下在只有 C 的時代,咱們該如何解決這個問題。若是隻有 C,就不存在 virtual 這種東西。重用有不少種方式,可是因爲沒有了類的幫助,咱們採用另外的解決方案:函數指針。
|
這就是一般所說的「回調」。buttonClicked() 函數在編譯期並不知道要調用哪個函數。被調用的函數是在運行期傳進來的。這樣,咱們的 Button 就能夠被重用了,由於咱們能夠在運行時將不一樣的函數指針傳遞進來,從而得到不一樣的點擊操做。
增長類型安全
對於 C++ 或者 Java 程序員來講,老是不喜歡這麼作。由於這不是類型安全的(注意 url 有一步強制類型轉換)。
咱們爲何須要類型安全呢?一個對象的類型其實暗示了你將如何使用這個對象。有了明確的對象類型,你就可讓編譯器幫助你檢查你的代碼是否是被正確的使用了,如同你畫了一個邊界,告訴編譯器說,若是有人越界,就要報錯。然而,若是沒有類型安全,你就丟失了這種優點,編譯器也就不能幫助你完成這種維護。這就如同你開車同樣。只要你的速度足夠,你就可讓你的汽車飛起來,可是,通常來講,這種速度就會提醒你,這太不安全了。同時還會有一些裝置,好比雷達之類,也會時時幫你檢查這種狀況。這就如同編譯器幫咱們作的那樣,是咱們出浴一種安全使用的範圍內。
回過來再看看咱們的代碼。使用 C 不是類型安全的,可是使用 C++,咱們能夠把回調的函數指針和數據放在一個類裏面,從而得到類型安全的優點。例如:
|
好了!咱們的 Button 已經能夠很方便的重用了,而且也是類型安全的,再也沒有了強制類型轉換。這種實現已經能夠解決系統中遇到的絕大部分問題了。彷佛如今的解決方案同前面的相似,都是繼承了一個類。只不過如今咱們對動做進行了抽象,而以前是對 Button 進行的抽象。這很像前面 C 的實現,咱們將不一樣的動做和 Button 關聯起來。如今,咱們一步步找到一種比較使人滿意的方法。
多對多
下一個問題是,咱們可以在點擊一次從新載入按鈕以後作多個操做嗎?也就是讓信號和槽實現多對多的關係?
實際上,咱們只須要利用一個普通的鏈表,就能夠輕鬆實現這個功能了。好比,以下的實現:
|
這就是其中的一種實現。不要以爲這種實現看上去沒什麼水平,實際上咱們發現這就是一種至關簡潔的方法。同時,不要糾結於咱們代碼中的 std:: 和 boost:: 這些命名空間,你徹底能夠用另外的類,強調一下,這只是一種可能的實現。如今,咱們的一個動做能夠鏈接多個 button 了,固然,也能夠是別的 Action 的使用者。如今,咱們有了一個多對多的機制。經過將 AbstractAction* 替換成 boost::shared_ptr,能夠解決 AbstractAction 的歸屬問題,同時保持原有的多對多的關係。
這會有不少的類!
若是你在實際項目中使用上面的機制,不少就會發現,咱們必須爲每個 action 定義一個類,這將不可避免地引發類爆炸。至今爲止,咱們前面所說的全部實現都存在這個問題。不過,咱們以後將着重討論這個問題,如今先不要糾結在這裏啦!
特化!特化!
當咱們開始工做的時候,咱們經過將每個 button 賦予不一樣的 action,實現 Button 類的重用。這實際是一種特化。然而,咱們的問題是,action 的特化被放在了固定的類層次中,在這裏就是這些 Button 類。這意味着,咱們的 action 很難被更大規模的重用,由於每個 action 實際都是與 Button 類綁定的。那麼,咱們換個思路,能不能將這種特化放到信號與槽鏈接的時候進行呢?這樣,action 和 button 這二者都沒必要進行特化了。
函數對象
將一個類的函數進行必定曾度的封裝,這個思想至關有用。實際上,咱們的 Action 類的存在,就是將 execute() 這個函數進行封裝,其餘別無用處。這在 C++ 裏面仍是比較廣泛的,不少時候咱們用 ++ 的特性從新封裝函數,讓類的行爲看起來就像函數同樣。例如,咱們重載 operator() 運算符,就可讓類看起來很像一個函數:
|
這樣,咱們的類看起來很像函數。前面代碼中的 for_each 也得作相應的改變:
|
如今,咱們的 Button::clicked() 函數的實現有了更多的選擇:
|
看起來很麻煩,值得這樣作嗎?
下面咱們來試着解釋一下信號和槽的目的。看上去,重寫 operator() 運算符有些過度,並不值得咱們去這麼作。可是,要知道,在某些問題上,你提供的可用的解決方案越多,越有利於咱們編寫更簡潔的代碼。經過對一些類進行規範,就像咱們要讓函數對象看起來更像函數,咱們可讓它們在某些環境下更適合重用。在使用模板編程,或者是 Boost.Function,bind 或者是模板元編程的情形下,這一點尤其重要。
這是對無需更多特化創建信號槽鏈接重要性的部分回答。模板就提供了這樣一種機制,讓添加了特化參數的代碼並不那麼難地被特化,正如咱們的函數對象那樣。而模板的特化對於使用者而言是透明的。
鬆耦合
如今,讓咱們回顧一下咱們以前的種種作法。
咱們執着地尋求一種可以在同一個地方調用不一樣函數的方法,這其實是 C++ 內置的功能之一,經過 virtual 關鍵字,固然,咱們也可使用函數指針實現。當咱們須要調用的函數沒有一個合適的簽名,咱們將它包裝成一個類。咱們已經演示瞭如何在同一地方調用多個函數,至少咱們知道有這麼一種方法(但這並非在編譯期完成的)。咱們實現了讓「信號發送」可以被若干個不一樣的「槽」監聽。
不過,咱們的系統的確沒有什麼很是不同凡響的地方。咱們來仔細審覈一下咱們的系統,它真正不一樣的是:
可是,這樣的系統還遠達不到鬆耦合的關係。Button 類並不須要知道 Page 類。鬆耦合意味着更少的依賴;依賴越少,組件的可重用性也就越高。
固然,確定須要有組件同時知道 Button 和 Page,從而完成對它們的鏈接。如今,咱們的鏈接實際是用代碼描述的,若是咱們不用代碼,而用數據描述鏈接呢?這麼一來,咱們就有了鬆耦合的類,從而提升兩者的可重用性。
新的鏈接模式
什麼樣的鏈接模式纔算是非代碼描述呢?假如僅僅只有一種信號槽的簽名,例如 void (*signature)(),這並不能實現。使用散列表,將信號的名字映射到匹配的鏈接函數,將槽的名字映射到匹配的函數指針,這樣的一對字符串便可創建一個鏈接。
然而,這種實現其實包含一些「握手」協議。咱們的確但願具備多種信號槽的簽名。在信號槽的簡短回答中咱們提到,信號能夠攜帶附加信息。這要求信號具備參數。咱們並無處理成員函數與非成員函數的不一樣,這又是一種潛在的函數簽名的不一樣。咱們尚未決定,咱們是直接將信號鏈接到槽函數上,仍是鏈接到一個包裝器上。若是是包裝器,這個包裝器須要已經存在呢,仍是咱們在須要時自動建立呢?雖然底層思想很簡單,可是,真正的實現還須要很好的努力才行。彷佛經過類名可以建立對象是一種不錯的想法,這取決於你的實現方式,有時候甚至取決於你有沒有能力作出這種實現。將信號和槽放入散列表須要一種註冊機制。一旦有了這麼一種系統,前面所說的「有太多類了」的問題就得以解決了。你所須要作的就是維護這個散列表的鍵值,而且在須要的時候實例化類。
給信號槽添加這樣的能力將比咱們前面所作的全部工做都困可貴多。在由鍵值進行鏈接時,多數實現都會選擇放棄編譯期類型安全檢查,以知足信號和槽的兼容。這樣的系統代價更高,可是其應用也遠遠高於自動信號槽鏈接。這樣的系統容許實例化外部的類,好比 Button 以及它的鏈接。因此,這樣的系統有很強大的能力,它可以完成一個類的裝配、鏈接,並最終完成實例化操做,好比直接從資源描述文件中導出的一個對話框。既然它可以憑藉名字使函數可用,這就是一種腳本能力。若是你須要上面所說的種種特性,那麼,完成這麼一套系統絕對是值得的,你的信號槽系統也會從中受益,由數據去完成信號槽的鏈接。
對於不須要這種能力的實現則會忽略這部分特性。從這點看,這種實現就是「輕量級」的。對於一個須要這些特性的庫而言,完整地實現出來就是一個輕量級實現。這也是區別這些實現的方法之一。
信號槽的實現實例—— Qt 和 Boost
Qt 的信號槽和 Boost.Signals 因爲有着大相徑庭的設計目標,所以兩者的實現、強度也十分不一樣。將兩者混合在一塊兒使用也不是不可能的,咱們將在本系統的最後一部分來討論這個問題。
使用信號槽
信號槽是偉大的工具,可是如何能更好的使用它們?相比於直接函數調用,有三點值得咱們的注意。一個信號槽的調用:
使用信號槽進行解耦,咱們得到的最大的好處是,鏈接兩端的對象不須要知道對方的任何信息。Button 同動做的鏈接是一個很典型的案例。例如以下信息:
|
Elevator 類,也就是電梯,不須要知道有多少顯示器正在監聽它的信號,也不須要知道這些顯示器的任何信息。每一層可能有一個屏幕和一組燈,用於顯示電梯的當前位置和方向,另一些遠程操控的面板也會顯示出一樣的信息。電梯並不關心這些東西。當它穿過(或者停在)某一層的時候,它會發出一個 floorChanged(int) 信號。或許,交通訊號燈是更合適的一個例子。
你也能夠實現一個應用程序,其中每個函數調用都是經過信號來觸發的。這在技術上說是徹底沒有問題的,然而倒是不大可行的,由於信號槽的使用無疑會喪失一部分代碼可讀性和系統性能。如何在這其中作出平衡,也是你須要考慮的很重要的一點。
Qt 方式
瞭解 Qt 信號槽最好的莫過於 Qt 的文檔。不過,這裏咱們從一個小例子來了解信號槽的 Qt 方式的使用。
|
Boost.Signals 方式
瞭解 Boost.Signals 的最好方式一樣是 Boost 的文檔。這裏,咱們仍是先從代碼的角度瞭解一下它的使用。
|
對比
或許你已經注意到上面的例子中,不管是 Qt 的實現方式仍是 Boost 的實現方式,除了必須的 Button 和 Page 兩個類以外,都不須要額外的類。兩種實現都解決了類爆炸的問題。下面讓咱們對照着來看一下咱們前面的分析。如今咱們有:
Boost.Signals | Qt Signals 和 Slots |
一個信號就是一個對象 | 信號只能是成員函數 |
發出信號相似於函數調用 | 發出信號相似於函數調用,Qt 提供了一個 emit 關鍵字來完成這個操做 |
信號能夠是全局的、局部的或者是成員對象 | 信號只能是成員函數 |
任何可以訪問到信號對象的代碼均可以發出信號 | 只有信號的擁有者才能發出信號 |
槽是任何可被調用的函數或者函數對象 | 槽是通過特別設計的成員函數 |
能夠有返回值,返回值能夠在多個槽中使用 | 沒有返回值 |
同步的 | 同步的或者隊列的 |
非線程安全 | 線程安全,能夠跨線程使用 |
當且僅當槽是可追蹤的時候,槽被銷燬時,鏈接自動斷開 | 槽被銷燬時,鏈接都會自動斷開(由於全部槽都是可追蹤的) |
類型安全(編譯器檢查) | 類型安全(運行期檢查) |
參數列表必須徹底一致 | 槽能夠忽略信號中多餘的參數 |
信號、槽能夠是模板 | 信號、槽不能是模板 |
C++ 直接實現 | 經過由 moc 生成的元對象實現(moc 以及元對象系統都是 C++ 直接實現的) |
沒有內省機制 | 能夠經過內省發現 能夠經過元對象調用 鏈接能夠從資源文件中自動推斷出 |
最重要的是,Qt 的信號槽機制已經深深地植入到框架之中,成爲不可分割的一部分。它們可使用 Qt 專門的開發工具,例如 QtCreator,經過拖拽的方式很輕鬆的建立、刪除、修改。它們甚至能夠經過動態加載資源文件,由特定命名的對象自動動態生成。這些都是 boost 做爲一個通用庫所不可能提供的。
將 Qt 的信號槽系統與 Boost.Signals 結合使用
實際上,將 Qt 的信號槽系統與 Boost.Signals 結合在一塊兒使用並不是不可能。經過前面的闡述,咱們都知道了兩者的不一樣,至於爲何要將這兩者結合使用,則是見仁見智的了。這裏,咱們給出一種結合使用的解決方案,可是並非說咱們暗示應該將它們結合使用。這應該是具體問題具體分析的。
將 Qt 的信號槽系統與 Boost.Signals 結合使用,最大的障礙是,Qt 使用預處理器定義了關鍵字 signals,slots 以及 emit。這些能夠看作是 Qt 對 C++ 語言的擴展。同時,Qt 也提供了另一種方式,即便用宏來實現這些關鍵字。爲了屏蔽掉這些擴展的關鍵字,Qt 4.1 的 pro 文件引入了 no_keywords 選項,以便使用標準 C++ 的方式,方便 Qt 與其餘 C++ 同時使用。你能夠經過打開 no_keywords 選項,來屏蔽掉這些關鍵字。下面是一個簡單的實現:
|
請注意,咱們已經在 pro 文件中打開了 no_keywords 選項,那麼,相似 signals 這樣的關鍵字已經不起做用了。因此,咱們必須將這些關鍵字修改爲相應的宏的版本。例如,咱們須要將 signals 改成 Q_SIGNALS,將 slots 改成 Q_SLOTS 等等。請看下面的代碼:
|
如今咱們有了一個發送者,下面來看看接收者:
|
下面,咱們來測試一下:
|
這段代碼將會有相似下面的輸出:
|
咱們能夠看到,這兩種實現的不一樣之處在於,Boost.Signals 的信號,boostSignal,是 public 的,任何對象均可以直接發出這個信號。也就是說,咱們可使用以下的代碼:
|
從而繞過咱們設置的 sendBoostSignal() 這個觸發函數。另外,咱們能夠看到,boostSignal 徹底能夠是一個全局對象,這樣,任何對象均可以使用這個信號。而對於 Qt 來講,signal 必須是一個成員變量,在這裏,只有 Sender 可使用咱們定義的信號。
這個例子雖然簡單,然而已經很清楚地爲咱們展現了,如何經過 Qt 發出信號來獲取 Boost 的行爲。在這裏,咱們使用一個公共的 sendQtSignal() 函數發出 Qt 的信號。然而, 爲了從 Boost 的信號獲取 Qt 的行爲,咱們須要多作一些工做:隱藏信號,可是須要提供獲取鏈接的函數。這樣看上去有些麻煩:
|
應該說,這樣的實現至關醜陋。實際上,咱們將 Boost 的信號與鏈接分割開了。咱們但願可以有以下的實現:
|
注意,這只是個人但願,並無作出實現。若是你有興趣,不妨嘗試一下。
總結
前面囉嗦了這麼多,如今總結一下。
信號和槽的機制其實是觀察者模式的一種變形。它是面向組件編程的一種很強大的工具。如今,信號槽機制已經成爲計算機科學的一種術語,也有不少種不一樣的實現。
Qt 信號槽是 Qt 整個架構的基礎之一,所以它同 Qt 提供的組件、線程、反射機制、腳本、元對象機制以及可視化 IDE 等等緊密地集成在一塊兒。Qt 的信號是對象的成員函數,因此,只有擁有信號的對象才能發出信號。Qt 的組件和鏈接能夠由非代碼形式的資源文件給出,而且可以在運行時動態創建這種鏈接。Qt 的信號槽實現創建在 Qt 元對象機制之上。Qt 元對象機制由 Qt 提供的 moc 工具實現。moc 也就是元對象編譯器,它可以將用戶指定的具備 Q_OBJECT 宏的類進行必定程度的預處理,給這個增長元對象能力。
Boost.Signals 是具備靜態的類型安全檢查的,基於模板的信號槽系統的實現。全部的信號都是模板類 boost::signal 的一個特化;全部的槽函數都具備相匹配的可調用的簽名。Boost.Signals 是獨立的,不須要內省、元對象系統,或者其餘外部工具的支持。然而,Boost.Signals 沒有從資源文件動態創建鏈接的能力。
這兩種實現都很是漂亮,而且都具備工業強度。將它們結合在一塊兒使用也不是不可能的,Qt 4.1 即提供了這種可能性。
任何基於 Qt GUI 的系統都會天然而然的使用信號槽。你能夠從中獲取很大的好處。任何大型的系統,若是但願可以下降組件之間的耦合程度,都應該借鑑這種思想。正如其餘的機制和技術同樣,最重要的是把握一個度。在正確的地方使用信號槽,可讓你的系統更易於理解、更靈活、高度可重用,而且你的工做也會完成得更快。