亚洲av成人无遮挡网站在线观看,少妇性bbb搡bbb爽爽爽,亚洲av日韩精品久久久久久,兔费看少妇性l交大片免费,无码少妇一区二区三区

  免費注冊 查看新帖 |

Chinaunix

  平臺 論壇 博客 文庫
最近訪問板塊 發(fā)新帖
查看: 24698981 | 回復(fù): 24698981
打印 上一主題 下一主題

[C] 在ANSI C下設(shè)計和實現(xiàn)簡便通用的signal-slot機制 [復(fù)制鏈接]

論壇徽章:
0
跳轉(zhuǎn)到指定樓層
1 [收藏(0)] [報告]
發(fā)表于 2009-10-09 11:41 |只看該作者 |倒序瀏覽
注:在幾處發(fā)表同樣的主題,希望通過討論,接收到大家提出各種建議或意見,拋磚引玉。

在ANSI C下設(shè)計和實現(xiàn)簡便通用的signal-slot機制
     ——一種平臺相關(guān)但易于移植的,lambda表達式風(fēng)格的,經(jīng)由抵抗編譯器而得的方案

    最近在ARM平臺下做一些開發(fā),考慮到這個場合下的風(fēng)氣,入鄉(xiāng)隨俗,使用的語言是C而不是一向偏好的C++。因為面向?qū)ο蟮纫恍┰O(shè)計在C中同樣可以達到,基本上對自己的習(xí)慣不會有太大的影響。唯一感到不太方便的就是在設(shè)計還沒有成型或者遇到無法設(shè)計的地方時,很難(或者要花很大的代價)通過提升抽象層次,延遲問題的解決以換取一定的靈活性和隔離各個層面之間的關(guān)聯(lián)。這個方面的一個典型案例就是signal-slot機制,在c++中借助signa-slot輔以函數(shù)對象,高階函數(shù),以及l(fā)ambda表達式幾乎可以達到隨心所欲書寫代碼的程度。雖然在接口(設(shè)計模式)正是為解決這些問題產(chǎn)生的,但考慮到前面說的設(shè)計問題,往往這個時候會感到十分為難。實際上分類是一種很難的事情,在對問題域了解程度有限的情況下,或者面臨十分復(fù)雜的非線性場景時,一開始所做的抉擇大多數(shù)不會有好的結(jié)果。在現(xiàn)實的代碼中,局部類型里充斥著怪味的地方比比皆是。而這正是signal-slot或者叫閉包,委托的東西大展手腳的時候,你可以不必要按照書上的固定模式來規(guī)劃代碼,而處理幾乎所有的類似需要解耦以及靈活性的問題。

    所以接下來的事情就演變成決心在有限的條件(ANSI C)下看看能不能自己實現(xiàn)一個可重用的signal-slot方案。在初步的設(shè)想中,這個方案可能會有地方使用到一些平臺相關(guān)的手筋,但希望盡量不要影響到大局的設(shè)計,以方便移植到各種平臺下。

[ 本帖最后由 TiGEr.zZ 于 2009-10-10 12:17 編輯 ]

論壇徽章:
0
2 [報告]
發(fā)表于 2009-10-09 12:39 |只看該作者
先做一下這個機制的介紹吧

論壇徽章:
0
3 [報告]
發(fā)表于 2009-10-10 11:15 |只看該作者
  1. signal-slot(信號-信號槽)機制顧名思義就是一個發(fā)送和接收信號的系統(tǒng)。通常描述為signal是一個發(fā)布者,slot可以是一個或多個訂閱者,這些slots作為回調(diào)者連接到signal上,在signal發(fā)出(emit)的時候會自動的被調(diào)用。一般情況下,一個signal何時會發(fā)生是不可預(yù)知的,或者至少在建立slots連接對象的時候如此,這非常適合用于在邏輯上獨立性很強的代碼間建立通訊。比如QT(1)和GTK+(2)這些GUI庫都用signal來發(fā)布界面中產(chǎn)生的事件,這樣程序中的功能模塊和界面流程就可以完全解耦,便于獨立的設(shè)計和開發(fā)。

  2. 在Unix/Linux操作系統(tǒng)里也有signal的概念,不過和這里所說的不是一個東西,不過在發(fā)生和響應(yīng)的關(guān)系上它們具有一定的相似性。

  3. 從回調(diào)的角度看,這和C語言中的函數(shù)指針表現(xiàn)一致,但僅有函數(shù)指針是遠遠不夠的,因為當(dāng)回調(diào)發(fā)生的時候,slot代碼中運行的一些信息可能不僅是函數(shù)參數(shù)這么多的東西,或者至少不僅僅是signal所能附帶的那些參數(shù)。后者往往表現(xiàn)為回調(diào)者參數(shù)是signal發(fā)出參數(shù)的超集,所以某些語言中提出閉包(closuer)(3)這樣的概念,比如Borland在其C++實現(xiàn)中擴展有__closuer關(guān)鍵字用于其VCL組件庫。C++中一個普遍的場景是需要調(diào)用類成員函數(shù)時,必須提供有類的實例,而這是不能表現(xiàn)為signal參數(shù)的。在微軟的ATL中采用thunk(4)這種不能跨平臺的特定技巧來實現(xiàn)傳遞類實例以及調(diào)用其成員函數(shù),之后的C#,更是把這種東西規(guī)范到語言層面上,稱為委托(delegate)(5)。

  4. 在一些場合下,signal-slot和事件(event)或者消息(message)是類似的概念。比如Java中提出有Event對象,并通過傳遞監(jiān)聽者(Listener)來響應(yīng)。C中也有類似的實現(xiàn),比如libevent庫(6)。在我的理解中,通常情況下Event多數(shù)被定義為程序外部的輸入事件,而signal是程序內(nèi)部的一種機制,實際用法上,signal-slot是更接近語言層面的解決方案,比較早引入signal-slot概念QT采用的方案就是附加了一個預(yù)編譯階段。所以可以在程序輸入輸出的邊界上使用Event,內(nèi)部映射到signal-slot系統(tǒng)上。

  5. 在C++中,signal-slot已經(jīng)通過模板被完美的實現(xiàn)的,這方面最完善的當(dāng)屬boost.signal(7)庫和libsigc++(8),前者自不必言,后者在GTK+的C++封裝GTKMM(9)里被首先開發(fā),后來獨立成為了一個庫。這里有一篇兩者的對比文章(10),有興趣的人可以通過閱讀它很快的了解到signal-slot大致是個什么樣。

  6. 在C中由于語言本身的制約,很難在語言層面提供支持,所以多數(shù)是以庫的形式存在著,使用上會比較繁瑣。這方面最完善的例子就是GTK+開發(fā)的C支撐庫GLib所帶的GSignal,屬于GObject(11)的一個部分?紤]到GLib本身所面向的場合,這也是一個非常重的實現(xiàn)。它依賴于其自身的類型的系統(tǒng),并且用戶需要專門為回調(diào)的附加數(shù)據(jù)提供列集(marshal)和散列(unmarshal)方法。盡管這并不是什么困難的事情,而且GTK提供有工具可以自動生成代碼,但顯然不適合用于資源比較苛刻的場合下。不過GSignal對使用者提供了宏來封裝接口,還是非常簡潔清晰的,可以達到多數(shù)情況下和C++中的沒有太大的區(qū)別。

  7. (1) [url]http://qt.nokia.com/[/url]
  8. (2) [url]http://www.gtk.org/[/url]
  9. (3) [url]http://en.wikipedia.org/wiki/Closure_%28computer_science%29[/url]
  10. (4) [url]http://en.wikipedia.org/wiki/Thunk[/url]
  11. (5) [url]http://msdn.microsoft.com/en-us/magazine/cc301810.aspx[/url]
  12. (6) [url]http://www.monkey.org/~provos/libevent/[/url]
  13. (7) [url]http://www.boost.org/[/url]
  14. (8) [url]http://libsigc.sourceforge.net/[/url]
  15. (9) [url]http://www.gtkmm.org/[/url]
  16. (10) [url]http://www.3sinc.com/opensource/boost.bind-vs-sigc2.html[/url]
  17. (11) [url]http://library.gnome.org/devel/gobject/[/url]
復(fù)制代碼

[ 本帖最后由 TiGEr.zZ 于 2009-10-10 11:20 編輯 ]

論壇徽章:
0
4 [報告]
發(fā)表于 2009-10-10 11:42 |只看該作者
沒怎么看明白,搬個凳子坐著慢慢看

論壇徽章:
0
5 [報告]
發(fā)表于 2009-10-11 14:07 |只看該作者
關(guān)于signal-slot具體的細節(jié)在后面會逐步展開,先讓我們來看一下程序開發(fā)中的場景。

一個程序或者代碼可以看作是大大小小的許多數(shù)據(jù)集合,以及處理這些數(shù)據(jù)集合的方法的組合。數(shù)據(jù)集合的存在是因為內(nèi)聚的需要,我們把一個大問題逐步細分的結(jié)果,這樣才可能完成復(fù)雜的工作。通過一個恰當(dāng)大小的集合我們可以把瑣碎問題的復(fù)雜性限制在足夠可以對付的范圍內(nèi)。到底什么樣是恰當(dāng)?shù)臎]有一個固定的標準,從解決問題的角度看,這應(yīng)該是由問題域內(nèi)在的關(guān)聯(lián)性決定的。大家都知道好的代碼組織的標準是“強內(nèi)聚,松耦合”(這實際上是通行的設(shè)計標準),拋開設(shè)計的劃分好壞不談從實現(xiàn)的角度看,這里的難點是后者。

代碼中處理這些數(shù)據(jù)的方法,有些是集合內(nèi)部的,有些是需要跨越兩個甚至更多集合的。前者相當(dāng)于面向?qū)ο笾蓄惖乃接蟹椒,后者相?dāng)于公開成員(被調(diào)用或訪問)。私有方法是內(nèi)聚的,而公開成員則是用來跟外界耦合的。細節(jié)上這種耦合一般是通過參數(shù)傳遞來完成的,相當(dāng)于集合之間的通訊。C語言中雖然不能完全一一對應(yīng),但設(shè)計的時候可以同樣如此考慮。

然后一個大的集合可以套上一個或多個小的集合,相當(dāng)于又做了一次內(nèi)聚的工作,每次內(nèi)聚之后,外在的復(fù)雜性會比原先降低一些,所以是值得的。大小集合的包含可以通過繼承、組合實現(xiàn),也有可能屬于描述中的層次的概念。比如當(dāng)大集合跟小集合通訊時,需要向它傳遞外界數(shù)據(jù)(這是前面工作的一種可能的副作用)的時候,相當(dāng)于某種形式的代理。仔細分析,按照通常的方法,隨著層次的深度越來越大,可能產(chǎn)生的通訊量也會急劇的增加,因為通訊中所有可能出現(xiàn)的參數(shù)都要傳遞到,盡管最后用到的可能只是其中很小的子集。在代碼中這一點可能并沒有直接反應(yīng)出來,因為這種復(fù)雜性被其他方式代替了。

我們通過一個圖來描繪一下(圖一):

圖中的矩形相當(dāng)于數(shù)據(jù)集合,而圓形(橢圓形)相當(dāng)于方法(方法所處理的數(shù)據(jù))?梢灾庇^的看到它們之間的關(guān)系。
這個圖更加符合C語言的場景,從純粹面向?qū)ο蟮膩砻枥L,可以由另外一張圖來表示(圖二):

這里明確所有的通訊都是通過接口完成的。方法和調(diào)用都屬于某個接口組合,所以看不到了。除此之外跟上圖不同的地方是多出了一些三角形,這是為了耦合不同形狀或數(shù)量的接口而附加的,相當(dāng)于添加一個層次,還有就是彌合同時訪問兩個以上對象接口,相當(dāng)于在多個集合間通訊,也需要添加一個層次。這就是那句著名的話:任何軟件設(shè)計問題都可以通過添加一個抽象層加以解決。

Eric Raymond在評價幾種語言時(1),在談到C++的地方,指出這是面向?qū)ο髮ο蟮娜秉c:使用面向?qū)ο蠓椒▽?dǎo)致組件之間出現(xiàn)很厚的粘合層,并且?guī)砹藝乐氐目删S護性問題。他認為應(yīng)該讓粘合層盡可能的薄。

其實用C開發(fā)這個問題也同樣存在,只是C語言中我們有絕招——我已經(jīng)記不清楚什么時候聽說過的這個名字了——那就是“全局變量大法”,相當(dāng)于把數(shù)據(jù)從小的集合中移到圖中最大的那個矩形里,相信大伙都在“牛人”的代碼中見過,至于有什么問題就不必說了。

(1) http://www.catb.org/~esr/writing ... s04.html#c_language

圖一.GIF (3.1 KB, 下載次數(shù): 175)

圖一.GIF

圖二.GIF (2.96 KB, 下載次數(shù): 156)

圖二.GIF

論壇徽章:
0
6 [報告]
發(fā)表于 2009-10-12 14:20 |只看該作者
在前面的敘述中多處提及到層,究竟什么是層,在軟件設(shè)計中這是一個很抽象的概念,這里我試著解釋一二。局部上看,當(dāng)產(chǎn)生一次調(diào)用A->B,那么B就比A低上一層;于全局而言,實現(xiàn)功能供別人調(diào)用的代碼一般層次較低,而負責(zé)調(diào)度和組織程序運行的代碼屬于較高層次——“指揮的都是領(lǐng)導(dǎo),干活的都是小兵”。一個程序的運行路徑是錯綜復(fù)雜的,實際上構(gòu)成的是一個跳躍編織的立體網(wǎng)格。

從數(shù)據(jù)劃分的角度分析,當(dāng)一個對象屬于另外一個對象的時候,意味著相應(yīng)的整體上大概要低一層,當(dāng)然這不是絕對的。

一般來說,層次越低,靈活性就越低,因為它所擁有的資源(數(shù)據(jù))就相應(yīng)受到了局限。如果需要提高靈活性,一個手段是通過和外界的通訊獲取更多的信息,也就產(chǎn)生了前面說的通訊量問題。

和靈活性相對的是復(fù)雜性,為了降低復(fù)雜性,我們不得不分層,后遺癥就是局部靈活性的喪失!叭肿兞看蠓ā币馕兜淖畲蟮撵`活性可能,因為所有的數(shù)據(jù)都可以在任何時候訪問。最極端的情況下,只需要一層就可以完成所有的功能(所有的方法都不帶有參數(shù)),相對的復(fù)雜性也就最大。

另外一個可能存在問題的方面就是耦合,因為高層可以直接或者間接訪問到低層,一旦低層需要作出修改的話,很可能擴散出去——牽一發(fā)而動全身。

整體上,層次屬于設(shè)計對付的事情,不是我們這里所要解決(也無法解決)的問題。不過通過一些手段可以提升局部的層次,比如pimpl慣用法(1)——作為設(shè)計模式的一種前身,相當(dāng)于把具體實現(xiàn)的層次提升至傳遞pimpl指針的那一刻;卣{(diào)也是如此,其層次屬于建立回調(diào)連接的那一刻,而不再是調(diào)用后的那一層,通常情況下,前者都遠遠要高于后者。從耦合的角度看,也大大縮小了耦合的范圍——跳過之下的很多層。還有,比如pimpl用法,可以通過在某一層上散布指針,相當(dāng)于擴展了層的面積,而進一步縮小的層的平均厚度,減少了需要的通訊量。

讓我們舉一些具體的例子,看看如何用接口對付類似的問題——軟件開發(fā)經(jīng)典著作《設(shè)計模式》(2)開篇要旨的一句話:針對接口編程,而不是針對實現(xiàn)編程。

比如,有一個類是用于發(fā)布時間的,稱為EventDispatch,發(fā)布類型是Event,然后需要接收時間的類是我們需要實現(xiàn)的。最原始的做法就是,在EventDispatch類中有一個類EventListener的實例event_listener,然后直接調(diào)用其方法receive(Event *),也就是event_listener.receive(p_event)。這里假設(shè)p_event是實際產(chǎn)生的一個Event實例的指針,我們采用C++的語法以區(qū)分指針和實例本身。

代碼是:

class Event {
  ...
};

class EventListener {
  ...
public:
  void receive(Event *) { ... }
};

class EventDispatch {
  ...
  class EventListener event_listener;
  ...
public:
  some_func() { ... ; event_listener.receive(p_event); ... }
  ...
};

這樣的話,EventDispatch類就和EventListener類緊密的耦合在一起了,EventDispatch類還要負責(zé)EventListener類實例的生老病死,顯然靈活性上要大打折扣,也很難做到模塊化式獨立設(shè)計和開發(fā)。

如果用原始的pimpl方法,我們可以把一個EventListener類指針類型作為類EventDispatch的成員。通過構(gòu)造函數(shù)或者其他某種初始化方法對其賦值,相當(dāng)于EventListener類可以在外部生成,開發(fā)EventDispatch的人只需要關(guān)心調(diào)用那一刻的事就可以了。

代碼是:

class EventDispatch {
  ...
  class EventListener *event_listener;
  ...
public:
  EventDispatch(..., class EventListener *_event_listener, ...) : event_listener(_event_listener) { ... }
  ...
  some_func() { ...; event_listener->receive(p_event); ... }
  ...
}

這一下靈活性有了很大的提升,耦合的問題也基本上解決了。但還是有些小問題,就是對EventListener類型的依賴還存在著,而且沒有一個明確的約束,一旦EventListener作出重大的修改,也許就不適用了。

接口則把這種約束提升到語義的層次上,也使得對EventListener的依賴達到可能的最小程度,便于設(shè)計和規(guī)劃。

代碼是:

struct EventListener {
  virtual void receive(Event *) = 0;  // 以抽象類來定義接口
};

class EventListenerA : public EventListener { // 可以接受事件的類必須繼承接口
  ...
public:
  ...
  void receive(Event *) { ... };
  ...
};

class EventListenerB : public EventListener { // 另一個也可以接受事件的類
  ...
};

上述兩個類在實際使用中可以相互替換,達到靈活的開發(fā)和部署。

讓我們進一步考慮,如果事件需要多播(multicast)怎么辦?就是上述EventListenerA,EventListernerB等等更多的類各有若干實例需要接收到事件。

這個時候的辦法有兩個,一個是最直接可以想到了,修改EventDispatch類,讓它可以接收多個成員。顯然這個時候?qū)π薷牡姆忾]性沒有做到,模塊的封裝被破壞了,或者至少可能會覺得不爽。這沒有關(guān)系,我們還有絕招沒使——可以通過增加一個層,專門負責(zé)管理這若干個實例,然后本身也繼承自EventListener接口,把它交給EventDispatch好了。

代碼是:

struct EventListenerCollection : public EventListener {
  std::vector<EventListener *> listener_collection;
public:
  void receive(Event *event) {
    BOOST_FOREACH(EventListener *listener, listener_collection) { // 順便了解一下BOOST_FOREACH的用法
      listener->receive(event);
    }
  }
  void add(Event *event) { // 往其中添加需要接受事件的對象
    listener_collection.push_back(event);
  }
  ...
};

這個時候就能嘗到接口帶來的甜頭了吧。

這是一個很簡單的例子,實際中遇到的情況往往要復(fù)雜許多。

隨著層次的提高,接口也會變的逐漸龐大起來;還有在對問題域了解不夠充分的情況下,接口也是很難定制或者需要不斷修改的。一旦接口發(fā)生的修改,也就意味著前面的努力可能要付之東流了。在開發(fā)壓力特別大的場合下,很多時候就干脆放棄了這個原則,那個原則,直接拿代碼開刀。這里補一塊那里補一塊,還來的快些,不過也就隨之留下了許多隱患。最令人喪氣的是,再也體會不到那種成事后的喜悅感了。

與之相對的,是過度設(shè)計的問題,看起來代碼布局非常不錯,只是數(shù)據(jù)被打的很散,需要讓他們工作起來我們不得不做很多粘合的過程,類似上面增加層的工作,瑣碎的工作越做越做不完。這是一個度或平衡的問題,往往在實踐者和理論者之間產(chǎn)生不大不小的沖突。

某些“新手”即使在使用接口的情況下,也會把實例化的過程放到構(gòu)造函數(shù)中,而且會非常滿意的覺得內(nèi)聚的很不錯:你看瑣碎的東西我都封裝掉了,外部只管使用它就可以工作了,看起來多么簡潔。

這顯然是沒有理解接口的含義。在Java出現(xiàn)開始大幅度推廣純面向?qū)ο蟮膽?yīng)用的時候,IOC(3)的概念得到了廣泛的宣傳。本質(zhì)上它仍然是接口的范疇。不過如雨后春筍般冒出來的各種IOC容器,利用Java乃至C#等新語言的特性,把接口的實例化提升到了程序的外部,通過專門的配置過程來完成,也算是徹底的把這種靈活性發(fā)揮到極致了吧。

現(xiàn)在讓我們看看signal-slot是如何對待上述同樣問題的。

代碼是:

class EventDispatch {
  ...
public:
  Signal(...Event *...) signal_event;
  ...
  some_func() { ...; signal_event(p_event); ... }
  ...
};

假設(shè)Signal是某種已經(jīng)定義好的signal類型,簡便起見,這里直接把其實例公有,外界可以直接訪問到。這里沒有關(guān)于EventListener的定義,不是說放到其他地方定義,而是根本不需要。下面是某種形式對signal的使用(或者叫響應(yīng)),注意這只是形式之一,具體要看語言能提供什么樣的表達能力。

假設(shè)有一個對象some_object,具有receive方法,可以接收同樣的Event *類型參數(shù),注意這里receive方法不是必須這么叫,可以是其他任何名字,也就是說它不是要求的某種形式的接口。

那么我們可以在程序中任何能訪問到某個EventDispatch實例,假設(shè)叫event_dispatch,以及some_object的地方,這么建立兩者之間的聯(lián)系。

代碼示意為:
event_dispatch.signal_event.connect(some_object.receive, 參數(shù)1 ....);

因為并不特指用哪個庫或者哪種方法,所以這里沒有規(guī)范的代碼,就解釋一下大概的意識。這里的參數(shù)1,就是指實際上signal發(fā)送出來的event,而some_object.receive,指以參數(shù)以及后面省略的更多信息去調(diào)用some_object的receive方法,當(dāng)然實際上目前也不是這么寫。

這里真正調(diào)用的是signal_event的connect方法,因為signal是一種通用或者預(yù)定義好的類型,所以connect方法是不會變化的。你可以在任何地方調(diào)用0到任意多次數(shù)的connect,而EventDispacth對象對此是絲毫不覺的。Signal本身會幫助管理和維護所有可能需要完成的事情。這里在EventDispatch中的some_func過程里一旦調(diào)用signal_event的重載()函數(shù)方法時,Signal會自動幫助完成調(diào)用在此前所有連接到其上的函數(shù)。

connect函數(shù)中的內(nèi)容稱為slot,也就是實際上調(diào)用產(chǎn)生時執(zhí)行的內(nèi)容,雖然此例中實際上最后就是調(diào)用some_object.receive,但slot維護的內(nèi)容不止于此。如果使用高階函數(shù)或者lambda表達式作為slot,那么就更無法對應(yīng)到事先定義的方法上了。

可以看出,這里EventDispatch跟事件的接收者已經(jīng)完全解耦了。而且如之前所述,回調(diào)過程的層次可以提升到事件發(fā)生之前,大大提高了代碼的靈活性。無論是前述的任何情況,一個EventDispatch都足可以應(yīng)付。

這里關(guān)鍵是signal_event,它可以看作是EventDispatch暴露出來的一個接口。這跟跟前面恰好反過來,所以也可以看作是反向接口。不過我并不贊同用接口來稱呼它。

IOC模式,也被人們稱為叫“好萊塢準則”(4),就是把接口的實現(xiàn)和部署從代碼中分離出來。跟接口仍然無法阻止“新人”犯錯不同的是,signal-slot則是徹底的杜絕了這種情況——你無法在類中任何建立連接對象,包括構(gòu)造函數(shù)(雖然我常常在構(gòu)造函數(shù)里這么做,但那是事件編程的范疇,不屬于這次討論的主題)——因為你對被連接到的對象毫無所知。好萊塢物色對象,估計至少要對對方的長相,高矮胖瘦調(diào)查清楚,這跟接口倒是別無二致。而signal-slot倒更類似中國的一個說法“姜太公釣魚——愿者上鉤”——盡管未必每次都有那么好的運氣,釣上文王這條大魚。

在設(shè)計上看,signal-slot是從事件的產(chǎn)生出發(fā)起,能夠直接映射到現(xiàn)實世界中,所以非常容易定義。而且它具有極小的粒度,增刪修改都很方便,所以即使是作為接口,也要比狹義上接口的定義來的容易的多。小粒度的缺點是數(shù)量上會增多,所以它也并不是完全代替接口,實際中兩者各有擅場,可以斟酌采用。也可以在遇到設(shè)計難題先用signal-slot代替,等穩(wěn)定后看看是否可以歸納出接口。

(1) http://www.gotw.ca/gotw/028.htm
(2) http://zh.wikipedia.org/wiki/%E8%AE%BE%E8%AE%A1%E8%8C%83%E4%BE%8B
(3) http://martinfowler.com/articles/injection.html
(4) http://en.wikipedia.org/wiki/Hollywood_Principle

論壇徽章:
0
7 [報告]
發(fā)表于 2009-10-12 21:18 |只看該作者
tpl 上面看到了

論壇徽章:
0
8 [報告]
發(fā)表于 2009-10-14 18:34 |只看該作者
該是轉(zhuǎn)入到正題的時候了。前面啰啰嗦嗦扯了一大通,可能不懂的還是看不懂,有點興趣的人卻早就不耐煩了。十分抱歉,最近事情都撞到一塊了,有些安排不過來。不過慢一些也好,可以一邊敘述一邊思考,盡量考慮的周全一些,也歡迎大家一起來出主意。

首先需要給想達到的目標定下個框框。因為有前述很多的別人早已實現(xiàn)好的案例作為參考,所以對于一個signal-slot大致的模樣很容易清楚的,只需要模仿就可以了——這是中國特色的辦法。

讓我們看看使用boost.signal的一個完整的例子,這個例子基本上摘抄自前面的鏈接中,我把它組合到一個C/C++例子程序里,這樣有條件的可以自己試著運行一下。

運行這個例子需要以下環(huán)境:boost.signal庫及相關(guān)頭文件,GNU C++編譯器(我一般使用它來做例子測試,你也可以使用微軟的VC++6.0及以后版本,但不保證測試過)。因為boost是以源代碼方式發(fā)布的,需要自己編譯,這個比較耗時。網(wǎng)絡(luò)上有一些別人編譯好的版本可以拿來用,你可以針對自己的環(huán)境選擇。我實際使用的環(huán)境是cygwin(1)及隨之發(fā)布的庫及工具,國內(nèi)用戶可以從cygwin.cn網(wǎng)站安裝。

這個例子十分簡單,全部放到一個c++代碼文件里,編譯運行就可以,具體步驟就不說了。

在代碼頭部需要包含<boost/signals.hpp>頭文件,以及<iostream>,<string>等用于測試目的,然后加上申明"using namespace std;",以符合可能的習(xí)慣。

首先申明Signal,這個申明可以作為局部或者全局變量:

boost::signal<int(float, string)> sig;

模板中的內(nèi)容是一個函數(shù)類型,直接指明了signal發(fā)出時,調(diào)用接口是int(float, std::string)這樣一個函數(shù)。

演示簡便起見,在程序入口main函數(shù)里,我們直接發(fā)出它:

int main()
{
// ..., 之前這里我們需要預(yù)先建立signal-slot連接,后面添加
sig(3.1415926, "pi"); // 發(fā)出(emit)信號
return 0;
}

我們定義一個同樣類型的自由函數(shù)供調(diào)用,方便起見,你可以把它直接放到main函數(shù)的上面:

int Func(float val, string str)
{
// 顯示被調(diào)用到,我們打印調(diào)用信息
std::cout << "Func: " << "the value of " << str << " is " << val << endl;
return 0;
}

好了,可以建立signal-slot連接:
int main()
{
sig.connect(&Func); // 連接到Func函數(shù)
sig(3.1415926, "pi");
return 0;
}

可以編譯實際運行一下看看效果,鏈接要求預(yù)先編譯好的boost.signal,一般名字是libboost_signals-gcc-mt這樣。

上面這個例子實際上就相當(dāng)于:
int main()
{
Func(3.1415926, "pi");
return 0;
}

該沒人問這樣的問題吧——那干嘛還兜那么個圈子?

上面例子僅僅是相當(dāng)于原始函數(shù)指針的效果,讓我們繼續(xù)看看調(diào)用對象成員函數(shù)怎么辦。

先定義類,同樣擺到main函數(shù)的上面:
class Foo {
public:
int Func(float val, string str) {
std::cout << "Foo->Func: " << "the value of " << str << " is " << val << endl;
return 0;
}
};

然后添加類的實例及對實例方法的調(diào)用,這個時候需要要代碼頭部添加頭文件包含#include <boost/bind.hpp>

int main()
{
Foo obj; // 類Foo的實例

sig.connect(&Func); // 連接到Func函數(shù)
sig.connect(boost::bind(&Foo::Func, obj, _1, _2)); // 連接到obj的Func方法
sig(3.1415926, "pi");
return 0;
}

這里連接就復(fù)雜了一些,slot部分不再是一個簡單的函數(shù)指針,而是包括成員函數(shù)指針,實例以及參數(shù)部分的一個綁定。這是生成了一個新的函數(shù)供 signal調(diào)用,把成員函數(shù)和具體實例組合在一起,所以稱為函數(shù)組合或綁定;也因為相當(dāng)于實際調(diào)用函數(shù)中參數(shù)的增多,被稱為高階函數(shù),這也是來自于函數(shù)式編程領(lǐng)域里的標準稱呼。_1,_2這樣的東西被稱為占位符,為了給生成的函數(shù)對象的附加參數(shù),實際上是可以攜帶某種類型數(shù)據(jù)的對象。編譯運行看看效果是不是和想象一樣。

上述的用法有點復(fù)雜,但想法很直接,所以先舉例,下面讓我們看看如何調(diào)用函數(shù)對象。函數(shù)對象是一個特定的概念,該對象包含一個重載了()操作符的方法,這樣調(diào)用的時候就不需要使用函數(shù)名,而可以直接以類或?qū)ο竺,看上去就跟自由函?shù)一樣。舉例如下:

struct Bar {
int operator()(float val, string str) {
std::cout << "Bar: " << "the value of " << str << " is " << val << endl;
}
};

同樣把類定義放到main上面,我們增加main實體如下:
int main()
{
Foo obj; // 類Foo的實例

sig.connect(&Func); // 連接到Func函數(shù)
sig.connect(boost::bind(&Foo::Func, obj, _1, _2)); // 連接到obj的Func方法
sig.connect(Bar()); // 連接到某個Bar函數(shù)對象
sig(3.1415926, "pi");
return 0;
}

這次顯得非常簡潔,似乎比調(diào)用成員方法容易的多,但實際上是一回事。注意這里的Bar()本身不是調(diào)用,而只是通過缺省構(gòu)造函數(shù)生成一個臨時對象,然后當(dāng)signal發(fā)出的時候會調(diào)用其重載的()的操作符。它等價于:

sig.connect(boost::bind(&Bar::operator(), Bar(), _1, _2)); // 這個復(fù)雜的寫法是不是容易理解一些

還有這里暗含一個對象生存期的問題。無論是使用obj實例,還是臨時對象,boost::bind的實現(xiàn)是保持一份它們的拷貝,直到實際調(diào)用產(chǎn)生的那一刻。臨時對象因為都是一樣的不用考慮,如果是實例,那么在連接建立后的對實例的修改將不會影響到slot中持有的那個對象。如果你需要的不是這樣而是想要引用原有的實例對象,那么可以采用指針傳遞該實例,也就是:

sig.connect(boost::bind(&Foo::Func, &obj, _1, _2)); // 連接到指向obj實例指針/引用的Func方法

不要小看它們之間的差異,實際使用過程中,我們更多需要的可能是引用,而不是無數(shù)長相一致的初始對象。這種需求是如此廣泛,以至于boost中有一個非常小但用處很大的庫boost::ref,可以用來產(chǎn)生對象引用或者引用對象這個東西。它的實現(xiàn)如此簡單但思想經(jīng)典,所以我就摘抄如下:

template<class T>
class reference_wrapper
{
public:
typedef T type;
explicit reference_wrapper(T& t): t_(&t) {}
operator T& () const { return *t_; }
// ...
private:
T* t_;
};

template<class T>
inline reference_wrapper<T> const ref(T &t)
{
return reference_wrapper<T>(t);
}

然后,上面用取地址符的地方就可以換成:

sig.connect(boost::bind(&Foo::Func, boost::ref(obj), _1, _2)); // 這是不是更有c++的味道

上述的這些東西基本上等價于closuer,thunk之類的東西,這算不上什么,讓我們把問題變稍微復(fù)雜些。如果有人寫了個類似上述Func功能的函數(shù)Func1,但不小心參數(shù)弄反了(這是經(jīng)常的事):

int Func1(string str, float val)
{
std::cout << "Func1: " << "the value of " << str << " is " << val << endl;
return 0;
}

程序可能有其他地方也用到這個函數(shù),要改起來可能會有些混亂,沒關(guān)系,我們可以這么做:

sig.connect(boost::bind(&Func1, _2, _1));

類似的,通過boost::bind我們可以在signal參數(shù)的基礎(chǔ)上任意的增減調(diào)用參數(shù),達到匹配最終調(diào)用的目的。

進一步,boost::bind可以通過使用函數(shù)對象作為參數(shù),把函數(shù)和函數(shù)堆疊起來,這在函數(shù)式語言中稱作currying(2),和Haskell語言一樣這是為了紀念著名的邏輯學(xué)家Haskell Curry(3)。

比如,當(dāng)我們獲得pi值的時候,需要計算的是半徑為2的圓的面積,我們分別有上述的輸出函數(shù),和一個計算面積的函數(shù):

float Area(float r, float pi)
{
return r * r * pi;
}

然后在main代碼里增加:

sig.connect(boost::bind(&Func, boost::bind(Area, 2, _1), _2));

這里看出來boost::bind返回的就是一個函數(shù)對象。

無論是上述何種做法,都局限于函數(shù)的粒度上。為此我們不得不做些預(yù)先的設(shè)計工作,或者在遇到變化的時候,隨時準備添加一層類似于上述計算面積的過程。這本身當(dāng)然不是什么問題,問題在于當(dāng)我們說到層的時候,往往是指它們是需要獨立設(shè)計和思考的。我們有限的思維能力,讓我們局限于一個不算太長的代碼范圍內(nèi),這對像我這樣天資愚笨的人而言已經(jīng)是十分吃力了。而一旦出現(xiàn)了上述情況的話,我們就不得不打斷當(dāng)前的工作,跳轉(zhuǎn)到其他一個代碼空間里,做出某種變更,然后再返回到剛才被打斷的地方。這分散了我們的注意力,破壞了已經(jīng)存在于大腦中一個連續(xù)的邏輯思維過程,降低了效率和提高了出錯概率。

這個時候我們需要的是一個可以不用跳來跳去就可以完成工作的辦法,這就是lambda表達式所能帶來的好處。

什么是lambda表達式?數(shù)學(xué)意義上的定義就不在這里討論了(我水平也不夠)。程序設(shè)計語言中的lambda表達式也就是指由一系列運算符結(jié)合而成的表達式,它自身也可以同時成為一個新的表達式的部分。在命令式語言里,你可以把一個語句塊看成就是一個lambda表達式,雖然它不是嚴格意義上的一個表達式。和函數(shù)式語言中不同的是,在C語言里,你不能把語句塊直接拿來作為函數(shù)使用,C++也同樣如此。但是在Object C里有block的概念(4),可以把語句塊當(dāng)作可以調(diào)用的對象,在一些動態(tài)語言里也有類似的東西。我們用lambda表示式重寫上面的例子:

sig.connect([](float pi, string str) { Func(2 * 2 * pi, str); });

connect的參數(shù)就是一個lambda表達式,它和函數(shù)很像,只是沒有名字并且和函數(shù)體語句塊一起被定義。lambda表達式可以組合成小到一個語句的任意粒度,如果我們的調(diào)用對象粒度不大,并且不需要重用的話,這種表述方式無疑大大提高了編碼效率。

上述的例子是按照已經(jīng)被接納成為C++ 0x一部分的lambda表達式標準提議(5)寫就,相信不久將來就會出現(xiàn)大眾的代碼里。

關(guān)于lambda表達式,boost里面也早已有實現(xiàn),而且有兩個,一個是boost::lambda,還有一個是作為boost::spirit一部分的boost::phoenix(6)。兩者大同小異,前者比較早,已經(jīng)沒有什么變化,后者更新一些,支持更加全面一些。我們看看 boost::lambda如何來寫上面的例子:

sig.connect(boost::lambda::bind(&Func, boost::lambda::_1 * 2 * 2, boost::lambda::_2));

去掉boost::lambda這個長長的前導(dǎo)命名空間,是不是就很漂亮了?傮w上boost::lambda在表達一些簡單的運算是足可以勝任的,但在表示復(fù)雜的代碼流程和引用復(fù)雜的成員對象時有些繁瑣,因此使用上會受到一定的制約。

注:后面的部分例子只是為了展示一些高階能力,細節(jié)上沒有面面俱到,感興趣的可以自己去嘗試。

到目前為止我們演示了signal-slot主要部分:連接(connect)和發(fā)出(emit),使用中還有對連接的管理,比如至少可以斷開;以及對返回值的使用。這個例子中signal的函數(shù)原型具有int類型的返回值,可以在發(fā)出signal的地方獲得,也就是把signal當(dāng)作函數(shù)使用。在C++ 中這很正常,因為它實際上就是一個函數(shù)對象。不過這些都不關(guān)鍵,暫時就不一一討論了。

(1)http://cygwin.com
(2)http://en.wikipedia.org/wiki/Currying
(3)http://en.wikipedia.org/wiki/Haskell_Curry
(4)http://en.wikipedia.org/wiki/Blocks_%28C_language_extension%29
(5)http://www.open-std.org/JTC1/SC22/WG...2009/n2927.pdf
(6)http://spirit.sourceforge.net/dl_doc...tml/index.html

[ 本帖最后由 TiGEr.zZ 于 2009-10-14 21:14 編輯 ]

論壇徽章:
0
9 [報告]
發(fā)表于 2009-10-14 21:59 |只看該作者
在CCF看到你的帖子,這邊又看到你的帖子,同樣mark一下

論壇徽章:
1
2015小元宵徽章
日期:2015-03-06 15:57:20
10 [報告]
發(fā)表于 2009-10-15 09:09 |只看該作者
lz的帖子是不是可以分成兩部分,一部分是Signal機制,我看類似于Event,或者,更樸素一點說,是一個發(fā)布/訂閱機制的方式(區(qū)別于消息隊列)。
第二部分,Signal的實現(xiàn),或者說是Signal的boost實現(xiàn)。我看了半天,覺得就是為了實現(xiàn)類似于java的反射,只不過是,由于沒有jvm這么一個context-aware(不知道是不是這么拼寫,中文大意是上下文感知),可能你們看起來(boost的實現(xiàn))挺nb,反正我是覺得挺累贅麻煩的,實現(xiàn)得一點不自然。(當(dāng)然,作為一個純正的OO語言,java的任何方法(函數(shù))必須屬于某個對象或者對象的實例。)

最近在看QT,對它的能將兩個組件connnect起來的機制沒有看明白,還請熟悉這方面的解惑。
您需要登錄后才可以回帖 登錄 | 注冊

本版積分規(guī)則 發(fā)表回復(fù)

  

北京盛拓優(yōu)訊信息技術(shù)有限公司. 版權(quán)所有 京ICP備16024965號-6 北京市公安局海淀分局網(wǎng)監(jiān)中心備案編號:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年舉報專區(qū)
中國互聯(lián)網(wǎng)協(xié)會會員  聯(lián)系我們:huangweiwei@itpub.net
感謝所有關(guān)心和支持過ChinaUnix的朋友們 轉(zhuǎn)載本站內(nèi)容請注明原作者名及出處

清除 Cookies - ChinaUnix - Archiver - WAP - TOP