轉自http://blog.csdn.net/myan/article/details/5928531
ios
這是那篇C++0X的正文。太長,先寫上半部分發了。程序員
Function/bind能夠是一個很簡單的話題,由於它其實不過就是一個泛型的函數指針。可是若是那麼來談,就沒意思了,也犯不上寫這篇東西。在我看來,這個事情要講的話,就應該講透,講到回調(callback)、代理(delegate)、信號(signal)和消息傳遞(messaging)的層面,由於它確實是過重要了。這個話題不但與面向對象的核心思想密切相關,並且是面向對象兩大流派之間交鋒的中心。圍繞這個問題的思考和爭論,幾乎把20年來全部主流的編程平臺和編程語言都攪進來了。因此,若是詳盡鋪陳,這個話題直接能夠寫一本書。編程
寫書我固然沒那個水平,但這個題目確實一直想動一動。然而這個主題實在太大,我實在沒有精力把它完整的寫下來;這個主題也很深,特別是涉及到併發環境有關的話題,個人理解還很是膚淺,總以爲我認識的不少高手都比我更有資格寫這個話題。因此猶豫了好久,要不要如今寫,該怎麼寫。最後我以爲,確實不能把一篇博客文章寫成一本20年面向對象技術史記,因此決定保留大的架構,可是對其中具體的技術細節點到爲止。我不會去詳細地列舉代碼,分析對象的內存佈局,畫示意圖,可是會把最重要的結論和觀點寫下來,說得好聽一點是提綱挈領,說的很差聽就是語焉不詳。但不管如何,我想這樣一篇東西,一是談談我對這個事情的見解,二是「拋磚引玉」,引來高手的關注,引出更深入和完整的敘述。安全
下面開始。網絡
0. 程序設計有一個範式(paradigm)問題。所謂範式,就是組織程序的基本思想,而這個基本思想,反映了程序設計者對程序的一個基本的哲學觀,也就是說,他認爲程序的本質是什麼,他認爲一個大的程序是由什麼組成的。而這,又跟他對於現實世界的見解有關。顯然,這樣的見解不可能有不少種。編程做爲一門行業,獨立存在快60年了,可是所出現的範式不過三種——過程範式、函數範式、對象範式。其中函數範式與現實世界差距比較大,在這裏不討論。而過程範式和對象範式能夠視爲對程序本質的兩種根本不一樣的見解,並且可以分別在現實世界中找到相應的映射。架構
對象範式與過程範式相比,有三個突出的優點,第一,因爲實現了邏輯上的分工,下降了大規模程序的開發難度。第二,靈活性更好——若干對象在一塊兒,能夠靈活組合,能夠以不一樣的方式協做,完成不一樣的任務,也能夠靈活的替換和升級。第三,對象範式更加適應圖形化、網絡化、消息驅動的現代計算環境。併發
因此,較之於過程範式,對象範式,或者說「面向對象」,確實是更具優點的編程範式。最近看到一些文章抨擊面向對象,說面向對象是胡扯,我認爲要具體分析。對面向對象的一部分批評,是衝着主流的「面向對象」語言去的,這確實是有道理的,我在下面也會談到,並且會罵得更狠。而另外一個批評的聲音,主要而來自STL之父Alex Stepanov,他說的固然有他的道理,不過要知道該牛人是前蘇聯莫斯科國立羅蒙諾索夫大學數學系博士,你只要翻翻前蘇聯的大學數學教材就知道了,可以在莫大拿到數學博士的,根本就是披着人皮的外星高等智慧。而咱們編寫地球上的程序,可能仍是應該以地球人的觀點爲主。app
1. 重複一遍對象範式的兩個基本觀念:框架
請注意,這兩個觀念與後來咱們熟知的面向對象三要素「封裝、繼承、多態」根本不在一個層面上,卻是與再後來的「組件、接口」神合。編程語言
2. 世界上第一個面嚮對象語言是Simula-67,第二個面嚮對象語言是Smalltalk-71。Smalltalk受到了Simula-67的啓發,基本出發點相同,但也有重大的不一樣。先說相同之處,Simula和Smalltalk都秉承上述對象範式的兩個基本觀念,爲了方便對象的構造,也都引入了類、繼承等概念。也就是說,類、繼承這些機制是爲了實現對象範式原則而構造出來的第二位的、工具性的機制,那麼爲何後來這些第二位的東西篡了主位,後面我會再來分析。而Simula和Smalltalk最重大的不一樣,就是Simula用方法調用的方式向對象發送消息,而Smalltalk構造了更靈活和更純粹的消息發送機制。
具體的說,向一個Simula對象中發送消息,就是調用這個對象的一個方法,或者稱成員函數。那麼你怎麼知道可以在這個對象上調用這個成員函數呢?或者說,你怎麼知道可以向這個對象發送某個消息呢?這就要求你必須確保這個對象具備合適的類型,也就是說,你得先知道哦這個對象是什麼,才能向它發消息。而消息的實現方式被直接處理爲成員函數調用,或虛函數調用。
而Smalltalk在這一點上作了一個歷史性的跨越,它實現了一個與目標對象無關的消息發送機制,無論那個對象是誰,也無論它是否是能正確的處理一個消息,做爲發送消息的對象來講,能夠毫無顧忌地抓住一個對象就發消息過去。接到消息的對象,要嘗試理解這個消息,並最後調用本身的過程來處理消息。若是這個消息能被處理,那個對象天然會處理好,若是不能被處理,Smalltalk系統會向消息的發送者回傳一個doesNotUnderstand消息,予以通知。對象不用關心消息是如何傳遞給另外一個對象的,傳遞過程被分離出來(而不是像Simula那樣明確地被以成員函數調用的方式實現),能夠是在內存中複製,也能夠是進程間通信。到了Smalltalk-80時,消息傳遞甚至能夠跨越網絡。
爲了方便後面的討論,不妨把源自Simula的消息機制稱爲「靜態消息機制」,把源自Smalltalk的消息機制稱爲「動態消息機制」。
Simula與Smalltalk之間對於消息機制的不一樣選擇,主要是由於二者於用途。前者是用於仿真程序開發,然後者用於圖形界面環境構建,看上去各自合情合理。然而,就是這麼一點簡單的區別,卻形成了巨大的歷史後果。
3. 到了1980年代,C++出現了。Bjarne Stroustrup在博士期間深刻研究過Simula,很是欣賞其思想,因而就在C語言語法的基礎之上,幾乎把Simula的思想照搬過來,造成了最初的C++。C++問世以之初,主要用於解決規模稍大的傳統類型的編程問題,迅速取得了巨大的成功,也證實了對象範式自己所具備的威力。
大約在同期,Brad Cox根據Smalltalk的思想設計了Objective-C,但是因爲其語法怪異,沒有流行起來。只有Steve Jobs這種具備禪宗美學鑑賞力的世外高人,把它奉爲瑰寶,與1988年連鍋把Objective-C的團隊和產品一口氣買了下來。
4. 就在同一時期,GUI成爲熱門。雖然GUI的本質是對象範型的,可是當時(1980年代中期)的面嚮對象語言,包括C++語言,還遠不成熟,所以最初的GUI系統無一例外是使用C和彙編語言開發的。或者說,最初的GUI開發者硬是用抽象級別更低的語言構造了一個面向對象系統。熟悉Win32 SDK開發的人,應該知道我在說什麼。
5. 當時不少人覺得,若是C++更成熟些,直接用C++來構造Windows系統會大大地容易。也有人以爲,儘管Windows系統自己使用C寫的,可是其面向對象的本質與C++更契合,因此在其基礎上包裝一個C++的GUI framework必定是垂手可得。但是一動手人們就發現,徹底不是那麼回事。用C++開發Windows框架可貴要死。爲何呢?主要就是Windows系統中的消息機制其實是動態的,與C++的靜態消息機制根本配合不到一塊兒去。在Windows裏,你能夠向任何一個窗口發送消息,這個窗口本身會在本身的wndproc裏來處理這個消息,若是它處理不了,就交給default window/dialog proc去處理。而在C++裏,你要向一個窗口發消息,就得確保這個窗口能處理這個消息,或者說,具備合適的類型。這樣一來的話,就會致使一個錯綜複雜的窗口類層次結構,沒法實現。而若是你要讓全部的窗口類都能處理全部可能的消息,且不論這樣在邏輯上就行不通(用戶定義的消息怎麼處理?),單在實現上就不可接受——爲一個小小的不一樣就得創造一個新的窗口類,每個小小的窗口類都要背上一個多達數百項的v-table,而其中可能99%的項都是浪費,不要說在當時,就是在今天,內存數量很是豐富的時候,若是每個GUI程序都這麼搞,用戶也吃不消。
6. 實際上C++的靜態消息機制還引發了更深嚴重的問題——扭曲了人們對面向對象的理解。既然必需要先知道對象的類型,才能向對象發消息,那麼「類」這個概念就特別重要了,而對象只不過是類這個模子裏造出來的東西,反而不重要。漸漸的,「面向對象編程」變成了「面向類編程」,「面向類編程」變成了「構造類繼承樹」。放在眼前的鮮活的對象活動不重要了,反而是其背後的靜態類型系統成爲關鍵。「封裝、繼承」這些第二等的特性,喧賓奪主,儼然成了面向對象的要素。每一個程序員彷佛都要先成爲領域專家,而後成爲領域分類學專家,而後構造一個完整的繼承樹,而後才能new出對象,讓程序跑起來。正是由於這個過程太漫長,太困難,再加上C++自己的複雜度就很大,因此C++出現這麼多年,真正堪稱經典的面向對象類庫和框架,幾乎屈指可數。不少流行的庫,好比MFC、iostream,都暴露出很多問題。通常程序員總以爲是本身的水平不夠,因而下更大功夫去練劍。卻不知根本上是方向錯了,脫離了對象範式的本質,企圖用靜態分類法來對現實世界建模,去刻畫變化萬千的動態世界。這麼難的事,你水平再高也很難作好。
能夠從一個具體的例子來理解這個道理,好比在一個GUI系統裏,一個 Push Button 的設計問題。事實上在一個實際的程序裏,一個 push button 到底「是否是」一個 button,進而是否是一個 window/widget,並不重要,本質上我根本不關心它是什麼,它從屬於哪個類,在繼承樹裏處於什麼位置,只要那裏有這麼一個東西,我能夠點它,點完了能夠發生相應的效果,就能夠了。但是Simula –> C++ 所鼓勵的面向對象設計風格,非要上來就想清楚,a Push Button is-a Button, a Button is-a Command-Target Control, a Command-Target Control is-a Control, a Control is-a Window. 把這一圈都想透徹以後,才能 new 一個 Push Button,而後才能讓它工做。這就形而上學了,這就脫離實際了。因此很難作好。你看到 MFC 的類繼承樹,以爲設計者太牛了,能把這些層次概念都想清楚,本身的水平還不夠,還得修煉。實際上呢,這個設計是通過數不清的失敗和錢磨出來、砸出來的,MFC的前身 Afx 不是就失敗了嗎?1995年還有一個叫作 Taligent 的大項目,召集了包括 Eric Gamma 在內的一大堆牛人,要用C++作一個一統天下的application framework,最後也以慘敗了結,連公司都倒閉了,CEO車禍身亡,牛人們悉數遣散。附帶說一下,這個Taligent項目是爲了跟NextSTEP和Microsoft Cairo競爭,前者用Objective-C編寫,後來發展爲Cocoa,後者用傳統的Win32 + COM做爲基礎架構,後來發展爲Windows NT。而Objective-C和COM,偏偏就在動態消息分派方面,與C++迥然不一樣。後面還會談到。
客觀地說,「面向類的設計」並非沒有意義。來源於實踐又高於實踐的抽象和概念,每每能更有力地把握住現實世界的本質,好比MVC架構,就是這樣的有力的抽象。可是這種抽象,應該是來源於長期最佳實踐的總結和提升,而不是面對問題時主要的解決思路。過於強調這種抽象,無異於假定程序員各個都是哲學家,具備對現實世界準確而深入的抽象能力,固然是不符合實際狀況的。結果呢,剛學習面向對象沒幾天的程序員,對眼前鮮活的對象世界視而不見,一個個都煞有介事地去搞哲學冥想,企圖越過現實世界,去抽象出其背後本質,固然敗得很慘。
其實C++問世以後不久,這個問題就暴露出來了。第一個C++編譯器 Cfront 1.0 是單繼承,而到了 Cfront 2.0,加入了多繼承。爲何?就是由於使用中人們發現邏輯上彷佛完美的靜態單繼承關係,碰到複雜靈活的現實世界,就破綻百出——蝙蝠是鳥也是獸,水上飛機能飛也能遊,它們該如何歸類呢?原本這應該促使你們反思繼承這個機制自己,可是那個時候全世界陷入繼承狂熱,因而就開始給繼承打補丁,加入多繼承,進而加入虛繼承,。到了虛繼承,明眼人一看便知,這只是一個語法補丁,是爲了逃避職責而製造的一塊無用的遮羞布,它已經徹底已經脫離實踐了——有誰在事前可以判斷是否應該對基類進行虛繼承呢?
到了1990年代中期,問題已經十分明顯。UML中有一個對象活動圖,其描述的就是運行時對象之間相互傳遞消息的模型。1994年Robert C. Martin在《Object-Oriented C++ Design Using Booch Method》中,曾建議面向對象設計從對象活動圖入手,而不是從類圖入手。而1995年出版的經典做品《Design Patterns》中,建議優先考慮組合而不是繼承,這也是盡人皆知的事情。這些跡象代表,在那個時候,面向對象社區裏的思想領袖們,已經意識到「面向類的設計」並很差用。只惋惜他們的革命精神還不夠。
7. 你可能要問,Java 和.NET也是用繼承關係組織類庫,並進行設計的啊,怎麼那麼成功呢?這裏有三點應該注意。第一,C++的難不只僅在於其靜態結構體系,還有不少源於語言設計上的包袱,好比對C的兼容,好比沒有垃圾收集機制,好比對效率的強調,等等。一旦把這些包袱丟掉,設計的難度確實能夠大大降低。第二,Java和.NET的核心類庫是在C++十幾年成功和失敗的經驗教訓基礎之上,結合COM體系優勢設計實現的,天然要好上一大塊。事實上,在Java和.NET核心類庫的設計中不少地方,體現的是基於接口的設計,和真正的基於對象的設計。有了這兩個主角站臺,「面向類的設計」不能喧賓奪主,也能發揮一些好的做用。第三,如後文指出,Java和.NET中分別對C++最大的問題——缺乏對象級別的delegate機制作出了本身的迴應,這就大大彌補了原來的問題。
儘管如此,Java仍是沾染上了「面向類設計」的癌症,基礎類庫裏就有不少架牀疊屋的設計,而J2EE/Java EE當中,這種形而上學的設計也很廣泛,因此也引起了好幾回輕量化的運動。這方面我並非太懂,可能須要真正的Java高手出來現身說法。我對Java的見解之前就講過——平臺和語言核心很是好,但風氣很差,崇尚華麗繁複的設計,裝牛逼的人太多。
至於.NET,我聽陳榕介紹過,在設計.NET的時候,微軟內部對因而否容許繼承爆發了很是激烈的爭論。不少資深高人都強烈反對繼承。至於最後引入繼承,很大程度上是營銷須要壓倒了技術理性。儘管如此,因爲有COM的基礎,又實現了很是完全的delegate,因此 .NET 的設計水平仍是很高的。它的主要問題不在這,在於太急於求勝,更新速度太快,基礎不牢。固然,根本問題仍是微軟沒有可以在Web和Mobile領域裏佔到多大的優點,也就使得.NET沒有用武之地。
8. COM。COM的要義是,軟件是由COM Components組成,components之間彼此經過接口相互通信。這是否讓你回想起本文開篇所提出的對象範型的兩個基本原則?有趣的是,在COM的術語裏,「COM Component 」 與「object 」通假,這就使COM的心思昭然若揭了。Don Box在Essential COM裏開篇就說,COM是更好的C++,事實上就是告訴你們,形而上學的「面向類設計」很差使,仍是回到對象吧。
用COM開發的時候,一個組件「是什麼」不重要,它具備什麼接口,也就是說,可以對它發什麼消息,纔是重要的。你能夠用IUnknown::QueryInterface問組件能對哪一組消息做出反應。向組件分派消息也不必定要被綁定在方法調用上,若是實現了 IDispatch,還能夠實現「自動化」調用,也就是COM術語裏的 Automation,而經過 列集(mashal),能夠跨進程、跨網絡向另外一組件發送消息,經過 moniker,能夠在分佈式系統裏定位和發現組件。若是你抱着「對象——消息」的觀念去看COM的設計,就會意識到,整個COM體系就是用規範如何作對象,如何發消息的。或者更直白一點,COM就是用C/C++硬是模擬出一個Smalltalk。並且COM的概念世界裏沒有繼承,就其純潔性而言,比Smalltalk還Smalltalk。在對象泛型上,COM達到了一個高峯,領先於那個時代,甚至於比它的繼任.NET還要純潔。
COM的主要問題是它的學習難度和安全問題,並且,它過於追求純潔性,徹底放棄了「面向類設計」 的機制,顯得有點過。
9. 好像有點扯遠了,其實仍是在說正事。上面說到因爲C++的靜態消息機制,致使了形而上學的「面向類的設計」,禍害無窮。但實際上,C++是有一個補救機會的,那就是實現對象級別的delegate機制。學過.NET的人,一聽delegate這個詞就知道是什麼意思,但Java裏沒有對應機制。在C++的術語體系裏,所謂對象級別delegate,就是一個對象回調機制。經過delegate,一個對象A能夠把一個特定工做,好比處理用戶的鼠標事件,委託給另外一個對象B的一個方法來完成。A沒必要知道B的名字,也不用知道它的類型,甚至都不須要知道B的存在,只要求B對象具備一個簽名正確的方法,就能夠經過delegate把工做交給B的這個方法來執行。在C語言裏,這個機制是經過函數指針實現的,因此很天然的,在C++裏,咱們但願經過指向成員函數的指針來解決相似問題。
然而就在這個問題上,C++讓人扼腕痛惜。