• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            隨筆-341  評(píng)論-2670  文章-0  trackbacks-0

            在思考怎么寫(xiě)這一篇文章的時(shí)候,我又想到了以前討論正交概念的事情。如果一個(gè)系統(tǒng)被設(shè)計(jì)成正交的,他的功能擴(kuò)展起來(lái)也可以很容易的保持質(zhì)量這是沒(méi)錯(cuò)的,但是對(duì)于每一個(gè)單獨(dú)給他擴(kuò)展功能的個(gè)體來(lái)說(shuō),這個(gè)系統(tǒng)一點(diǎn)都不好用。所以我覺(jué)得現(xiàn)在的語(yǔ)言被設(shè)計(jì)成這樣也是有那么點(diǎn)道理的。就算是設(shè)計(jì)Java的那誰(shuí),他也不是傻逼,那為什么Java會(huì)被設(shè)計(jì)成這樣?我覺(jué)得這跟他剛開(kāi)始想讓金字塔的底層程序員也可以順利使用Java是有關(guān)系的。

            ?

            難道好用的語(yǔ)言就活該不好擴(kuò)展碼?實(shí)際上不是這樣的,但是這仍然是上面那個(gè)正交概念的問(wèn)題。一個(gè)容易擴(kuò)展的語(yǔ)言要讓你覺(jué)得好用,首先你要投入時(shí)間來(lái)學(xué)習(xí)他。如果你想簡(jiǎn)單的借鑒那些不好擴(kuò)展的語(yǔ)言的經(jīng)驗(yàn)(如Java)來(lái)在短時(shí)間內(nèi)學(xué)會(huì)如何使用一個(gè)容易擴(kuò)展的語(yǔ)言(如C++/C#)——你的出發(fā)點(diǎn)就已經(jīng)投機(jī)了。所以這里有一個(gè)前提值得我再?gòu)?qiáng)調(diào)一次——首先你需要投入時(shí)間去學(xué)習(xí)他。

            ?

            正如我一直在群里說(shuō)的:"C++需要不斷的練習(xí)——vczh"。要如何練習(xí)才能讓自己借助語(yǔ)言做出一個(gè)可擴(kuò)展的架構(gòu)呢?先決條件就是,當(dāng)你在練習(xí)的時(shí)候,你必須是在練習(xí)如何實(shí)現(xiàn)一個(gè)從功能上就要求你必須保證他的可擴(kuò)展性的系統(tǒng),舉個(gè)例子,GUI庫(kù)就是其中的一類(lèi)。我至今認(rèn)為,學(xué)會(huì)實(shí)現(xiàn)一個(gè)GUI庫(kù),比通過(guò)練習(xí)別的什么東西來(lái)提高自己的能力來(lái)講,簡(jiǎn)直就算一個(gè)捷徑了。

            ?

            那么什么是擴(kuò)展呢?簡(jiǎn)單的來(lái)講,擴(kuò)展就是在不修改原有代碼的情況下,僅僅通過(guò)添加新的代碼,就可以讓原有的功能適應(yīng)更多的情況。一般來(lái)講,擴(kuò)展的主要目的并不是要增加新的功能,而是要只增加新代碼的前提下修改原有的功能。譬如說(shuō)原來(lái)你的系統(tǒng)只支持SQLServer,結(jié)果有一天你遇到了一個(gè)喜歡Oracle的新客戶(hù),你要把東西賣(mài)給他,那就得支持Oracle了吧。但是我們知道,SQLServer和Oracle在各種協(xié)議(asp.net、odbc什么的)上面是有偏好的,用DB不喜歡的協(xié)議來(lái)連接他的時(shí)候bug特別多,這就造成了你又可能沒(méi)辦法使用單一的協(xié)議來(lái)正確的使用各種數(shù)據(jù)庫(kù),因此擴(kuò)展的這個(gè)擔(dān)子就落在你的身上了。當(dāng)然這種系統(tǒng)并不是人人都要寫(xiě),我也可以換一個(gè)例子,假如你在設(shè)計(jì)一個(gè)GPU集群上的程序,那么這個(gè)集群的基礎(chǔ)架構(gòu)得支持NVidia和AMD的顯卡,還得支持DirectCompute、Cuda和OpenCL。然而我們知道,OpenCL在不同的平臺(tái)上,有互不兼容的不同的bug,導(dǎo)致你實(shí)際上并不可能僅僅通過(guò)一份不變的代碼,就充分發(fā)揮OpenCL在每一個(gè)平臺(tái)上的最佳狀態(tài)……現(xiàn)實(shí)世界的需求真是orz(OpenCL在windows上用AMD卡定義一個(gè)struct都很容易導(dǎo)致崩潰什么的,我覺(jué)得這根本不能用)……

            ?

            在語(yǔ)言里面談擴(kuò)展,始終都離不開(kāi)兩個(gè)方面:編譯期和運(yùn)行期。這些東西都是用看起來(lái)很像pattern matching的方法組織起來(lái)的。如果在語(yǔ)言的類(lèi)型系統(tǒng)的幫助下,我們可以輕松做出這樣子的架構(gòu),那這個(gè)語(yǔ)言就算有可擴(kuò)展的類(lèi)型了。

            ?

            1. 編譯期對(duì)類(lèi)型的擴(kuò)展

            ?

            這個(gè)其實(shí)已經(jīng)被人在C++和各種靜態(tài)類(lèi)型的函數(shù)式語(yǔ)言里面做爛了。簡(jiǎn)單的來(lái)講,C++處理這種問(wèn)題的方法就是提供偏特化。可惜C++的偏特化只讓做在class上面,結(jié)果因?yàn)榇蠹覍?duì)class的誤解很深,順便連偏特化這種比OO簡(jiǎn)單一萬(wàn)倍的東西也誤解了。偏特化不允許用在函數(shù)上,因?yàn)楹瘮?shù)已經(jīng)有了重載,但是C++的各種標(biāo)準(zhǔn)在使用函數(shù)來(lái)擴(kuò)展類(lèi)型的時(shí)候,實(shí)際上還是當(dāng)他是偏特化那么用的。我舉個(gè)例子。

            ?

            C++11多了一個(gè)foreach循環(huán),寫(xiě)成for(auto x : xs) { … }。STL的類(lèi)型都支持這種新的for循環(huán)。C++11的for循環(huán)是為了STL的容器設(shè)計(jì)的嗎?顯然不是。你也可以給你自己寫(xiě)的容器加上for循環(huán)。方法有兩種,分別是:1、給你的類(lèi)型T加上T::begin和T::end兩個(gè)成員函數(shù);2、給你的類(lèi)型T實(shí)現(xiàn)begin(T)和end(T)兩個(gè)全局函數(shù)。我還沒(méi)有去詳細(xì)考證,但是我認(rèn)為缺省的begin(T)和end(T)全局函數(shù)就是去調(diào)用T::begin和T::end的,因此for循環(huán)只需要認(rèn)begin和end兩個(gè)全局函數(shù)就可以了。

            ?

            那自己的類(lèi)型怎么辦呢?當(dāng)然也要去重載begin和end了?,F(xiàn)在全局函數(shù)沒(méi)有重載,因此寫(xiě)出來(lái)大概是:
            template<typename T> auto begin(const T& t)->decltype(t.begin()) { return t.begin(); }

            template<typename T> my_iterator<T> begin(const my_container<T>& t);

            template<typename T> my_range_iterator<T> begin(pair<T, T> range);

            ?

            如果C++的函數(shù)支持偏特化的話,那么上面這段代碼就會(huì)被改成這樣,而且for循環(huán)也就不去找各種各樣的begin函數(shù)了,而只認(rèn)定那一個(gè)std::begin就可以了:
            template<typename T> auto begin(const T& t)->decltype(t.begin()) { return t.begin(); }

            template<typename T> my_iterator<T> begin< my_container<T>>(const my_container<T>& t);

            template<typename T> my_range_iterator<T> begin< pair<T, T>>( const pair<T, T>& range);

            ?

            為什么要偏特化呢?因?yàn)檫@至少保證你寫(xiě)出來(lái)的begin函數(shù)跟for函數(shù)想要的begin函數(shù)的begin函數(shù)的簽名是相容的(譬如說(shuō)不能有兩個(gè)參數(shù)之類(lèi)的)。事實(shí)上C++11的for循環(huán)剛開(kāi)始是要求大家通過(guò)偏特化一個(gè)叫做std::range的類(lèi)型來(lái)支持的,這個(gè)range類(lèi)型里面有兩個(gè)static函數(shù),分別叫begin和end。后來(lái)之所以改成這樣,我猜大概是因?yàn)镃++的每一個(gè)函數(shù)重載也可以是模板函數(shù),因此就不需要引入一個(gè)新的類(lèi)型了,就讓大家去重載好了。而且for做出來(lái)的時(shí)候,C++標(biāo)準(zhǔn)里面還沒(méi)有concept,因此也沒(méi)辦法表達(dá)"對(duì)于所有可以循環(huán)的類(lèi)型T,我們都有std::range<T>必須滿(mǎn)足這個(gè)叫做range_loopable<T>的concept"這樣的前置條件。

            ?

            重載用起來(lái)很容易讓人走火入門(mén),很多人到最后都會(huì)把一些僅僅看起來(lái)像而實(shí)際上語(yǔ)義完全不同的東西用重載來(lái)表達(dá),函數(shù)的參數(shù)連相似性都沒(méi)有。其實(shí)這是不對(duì)的,這種時(shí)候就應(yīng)該把函數(shù)改成兩個(gè)不同的名字。假如當(dāng)初設(shè)計(jì)C++的是我,那我一定會(huì)把函數(shù)重載干掉,然后允許人們對(duì)函數(shù)進(jìn)行偏特化,并且加上concept。既然std::begin已經(jīng)被定義為循環(huán)的輔助函數(shù)了,那么你重載一個(gè)std::begin,他卻不能用來(lái)循環(huán)(譬如說(shuō)有兩個(gè)參數(shù)什么的),那有意義嗎?完全沒(méi)有。

            ?

            這種例子還有很多,譬如如何讓自己的類(lèi)型可以被<<到wcout的做法啦,boost的那個(gè)serialization框架,還有各種各樣的庫(kù),其實(shí)都利用了相同的思想——對(duì)類(lèi)型做編譯期的擴(kuò)展,使用一些手段使得在不需要修改原來(lái)的代碼的前提下,就可以讓編譯器找到你新加進(jìn)去的函數(shù),從而使得調(diào)用的寫(xiě)法不用發(fā)生變化就可以對(duì)原有的功能支持更多的情況。至少我們讓我們自己的類(lèi)型支持for循環(huán)就不需要翻開(kāi)std::begin的代碼把我們的類(lèi)型寫(xiě)進(jìn)去,只需要在隨便什么空白的地方重載一個(gè)std::begin就可以了。這就是一個(gè)很好地體現(xiàn)。C++的標(biāo)準(zhǔn)庫(kù)一直在引導(dǎo)大家正確設(shè)計(jì)一個(gè)可擴(kuò)展的架構(gòu),可惜很多人都意識(shí)不到這一點(diǎn),為了自己那一點(diǎn)連正確性都談不上的強(qiáng)迫癥,放棄了很多東西。

            ?

            很多靜態(tài)類(lèi)型的函數(shù)式語(yǔ)言使用concept來(lái)完成上述的工作。當(dāng)一個(gè)concept定義好了之后,我們就可以通過(guò)對(duì)concept的實(shí)現(xiàn)進(jìn)行偏特化來(lái)讓我們的類(lèi)型T滿(mǎn)足concept的要求,來(lái)讓那些調(diào)用這個(gè)concept的泛型代碼,可以在處理的對(duì)象是T的時(shí)候,轉(zhuǎn)而調(diào)用我們提供的實(shí)現(xiàn)。Haskell就是一個(gè)典型的例子,一個(gè)sort函數(shù)必然要求元素是可比較的,一個(gè)可以比較的類(lèi)型定義為實(shí)現(xiàn)了Ord這個(gè)type class的類(lèi)型。所以你只要給你自己的類(lèi)型T實(shí)現(xiàn)Ord這個(gè)type class,那sort函數(shù)就可以對(duì)T的列表進(jìn)行排序了。

            ?

            對(duì)于C++和C#這種沒(méi)有concept或者concept不是主要概念的語(yǔ)言里面,對(duì)類(lèi)型做靜態(tài)的擴(kuò)展只需要你的類(lèi)型滿(mǎn)足"我可以這么這么干"就可以了。譬如說(shuō)你重載一個(gè)begin和end,那你的類(lèi)型就可以被foreach;你給你的類(lèi)型實(shí)現(xiàn)了operator<等函數(shù),那么一個(gè)包含你的類(lèi)型的容器就可以被sort;或者C#的只要你的類(lèi)型T<U>有一大堆長(zhǎng)得跟System.Linq.Enumerable里面定義的擴(kuò)展函數(shù)一樣的擴(kuò)展函數(shù),那么Linq的神奇的語(yǔ)法就可以用在你的類(lèi)型上等等。這跟動(dòng)態(tài)類(lèi)型的"只要它長(zhǎng)的像鴨子,那么它就是鴨子"的做法有異曲同工之效。如果你的begin函數(shù)的簽名沒(méi)寫(xiě)對(duì),編譯器也不會(huì)屌你,直到你對(duì)他for的時(shí)候編譯器才會(huì)告訴你說(shuō)你做錯(cuò)了。這跟很多動(dòng)態(tài)類(lèi)型的語(yǔ)言的很多錯(cuò)誤必須在運(yùn)行的時(shí)候才發(fā)現(xiàn)的性質(zhì)也是類(lèi)似的。

            ?

            Concept對(duì)于可靜態(tài)擴(kuò)展的類(lèi)型的約束,就如同類(lèi)型對(duì)于邏輯的約束一樣。沒(méi)有concept的C++模板,就跟用動(dòng)態(tài)類(lèi)型語(yǔ)言寫(xiě)邏輯一樣,只有到用到的那一刻你才知道你到底寫(xiě)對(duì)了沒(méi)有,而且錯(cuò)誤也會(huì)爆發(fā)在你使用它的地方,而不是你定義它的地方。因此本著編譯器幫你找到盡可能多的錯(cuò)誤的原則,C++也開(kāi)始有concept了。

            ?

            C#的擴(kuò)展方法用在Linq上面,其實(shí)編譯器也要求你滿(mǎn)足一個(gè)內(nèi)在的concept,只是這個(gè)概念無(wú)法用C#的語(yǔ)法表達(dá)出來(lái)。所以我們?cè)趯?xiě)Linq Provider的時(shí)候也會(huì)有同樣的感覺(jué)。Java的interface都可以寫(xiě)缺省實(shí)現(xiàn)了,但是卻沒(méi)有靜態(tài)方法。這就造成了我們實(shí)際上無(wú)法跟C++和C#一樣,在不修改原有代碼的前提下,讓原有的功能滿(mǎn)足更多的情況。因?yàn)镃#的添加擴(kuò)展方法的情況,到了Java里面就變成讓一個(gè)類(lèi)多繼承自一個(gè)interface,必須修改代碼了。Java的這個(gè)功能特別的雞肋,不知道是不是他故意想跟C#不一樣才設(shè)計(jì)成這個(gè)樣子的,可惜精華沒(méi)有抄去,卻抄了糟粕。

            ?

            1. 運(yùn)行期對(duì)類(lèi)型的擴(kuò)展

            ?

            自從Java吧靜態(tài)類(lèi)型和面向?qū)ο罄壴谝黄鹬?,業(yè)界對(duì)"運(yùn)行期對(duì)類(lèi)型的擴(kuò)展"這個(gè)主題思考了很多年,甚至還出了一本著作叫《設(shè)計(jì)模式》,讓很多人捧為經(jīng)典。大家爭(zhēng)先恐后的學(xué)習(xí),而效果卻不怎么樣。這是因?yàn)椤对O(shè)計(jì)模式》不好嗎?不是。這是因?yàn)殪o態(tài)類(lèi)型和面向?qū)ο罄壴谝黄鹬?,設(shè)計(jì)一個(gè)可擴(kuò)展的架構(gòu)就很難嗎?也不是。真正的原因是,Java設(shè)計(jì)(好像也是抄的Simular?我記不太清楚了)的虛函數(shù)把這個(gè)問(wèn)題的難題提升了一個(gè)等級(jí)。

            ?

            用正確的概念來(lái)理解問(wèn)題可以讓我們更容易的掌握問(wèn)題的本質(zhì)。語(yǔ)言是有魔力的,習(xí)慣說(shuō)中文的人,思考方式都跟中國(guó)人差不多。習(xí)慣說(shuō)英語(yǔ)的人,思考方式都跟美國(guó)人差不多。因此習(xí)慣了使用C++/C#/Java的人,他們對(duì)于面向?qū)ο蟮南敕ㄆ鋵?shí)也是差不多的。這是人類(lèi)的天性。盡管大家鼓吹說(shuō)語(yǔ)言只是工具,我們應(yīng)該掌握方法論什么的,但是這就跟要求男人面對(duì)一個(gè)萌妹紙不勃起一樣,違背了人類(lèi)的本性,難度簡(jiǎn)直太高了。于是我今天從虛函數(shù)和Visitor模式講起,告訴大家為什么虛函數(shù)的這種形式會(huì)讓"擴(kuò)展的時(shí)候不修改原有的代碼"變難。

            ?

            絕大多數(shù)的系統(tǒng)的擴(kuò)展,都可以最后化簡(jiǎn)(這并不要求你非得這么做)為"當(dāng)它的類(lèi)型是這個(gè)的時(shí)候你就干那個(gè)"的這么件事。對(duì)于在編譯的時(shí)候就已經(jīng)知道的,我們可以用偏特化的方法讓編譯器在生成代碼的時(shí)候就先搞好。對(duì)于運(yùn)行的時(shí)候,你拿到一個(gè)基類(lèi)(其實(shí)為什么一定要有基類(lèi)?應(yīng)該有的是interface!參見(jiàn)上一篇文章——刪減語(yǔ)言的功能),那如何O(1)時(shí)間復(fù)雜度(這里的n指的是所有跟這次跳轉(zhuǎn)有關(guān)系的類(lèi)型的數(shù)量)就跳轉(zhuǎn)到你想要的那個(gè)分支上去呢?于是我們有了虛函數(shù)。

            ?

            靜態(tài)的擴(kuò)展用的是靜態(tài)的分派,于是編譯器幫我們把函數(shù)名都hardcode到生成的代碼里面。動(dòng)態(tài)的類(lèi)型用的是動(dòng)態(tài)的分派,于是我們得到的當(dāng)然是一個(gè)相當(dāng)于函數(shù)指針的東西。于是我們會(huì)把這個(gè)函數(shù)指針保存在從基類(lèi)對(duì)象可以O(shè)(1)訪問(wèn)到的地方。虛函數(shù)就是這么實(shí)現(xiàn)的,而且這種類(lèi)型的分派必須要這么實(shí)現(xiàn)的。但是,寫(xiě)成代碼就一定要寫(xiě)程序函數(shù)嗎

            ?

            其實(shí)本來(lái)沒(méi)什么理由讓一個(gè)語(yǔ)言(或者library)長(zhǎng)的樣子必須有提示你他是怎么實(shí)現(xiàn)的功能。關(guān)心太多容易得病,執(zhí)著太多心生痛苦啊。所以好好的解決問(wèn)題就好了。至于原理是什么,下了班再去關(guān)心。估計(jì)還有一些人不明白為什么不好,我就舉一個(gè)通俗的例子。我們都知道dynamic_cast的性能不怎么樣,虛函數(shù)用來(lái)做if的性能要遠(yuǎn)遠(yuǎn)比dynamic_cast用來(lái)做if的性能好得多。因此下面所有的答案都基于這個(gè)前提——要快,不要dynamic_cast!

            ?

            1. 處理HTML

            ?

            好了,現(xiàn)在我們的任務(wù)是,拿到一個(gè)HTML,然后要對(duì)他做一些功能,譬如說(shuō)把它格式化成文本啦,看一下他是否包含超鏈接啦等等。假設(shè)我們已經(jīng)解決HTML的語(yǔ)法分析問(wèn)題,那么我們會(huì)得到一顆靜態(tài)類(lèi)型的語(yǔ)法樹(shù)。這棵語(yǔ)法樹(shù)如無(wú)意外一定是長(zhǎng)下面這個(gè)樣子的。另外一種選擇是存成動(dòng)態(tài)類(lèi)型的,但是這跟面向?qū)ο鬅o(wú)關(guān),所以就不提了。

            ?

            class DomBase

            {

            public:

            ????virtual ~DomBase();

            ?

            ????static shared_ptr<DomBase> Parse(const wstring& htmlText);

            };

            ?

            class DomText : public DomBase{};

            class DomImg : public DomBase{};

            class DomA : public DomBase{};

            class DomDiv : public DomBase{};

            ......

            ?

            HTML的tag種類(lèi)繁多,大概有那么上百個(gè)吧。那現(xiàn)在我們要給他加上一個(gè)格式化成字符串的功能,這顯然是一個(gè)遞歸的算法,先把sub tree一個(gè)一個(gè)格式化,最后組合起來(lái)就好了。可能對(duì)于不同的非文本標(biāo)簽會(huì)有不同的格式化方法。代碼寫(xiě)出來(lái)就是這樣——基本上是唯一的作法:

            ?

            class DomBase

            {

            public:

            ????virtual ~DomBase();

            ????static shared_ptr<DomBase> Parse(const wstring& htmlText);

            ?

            ????virtual void FormatToText(ostream& o); // 默認(rèn)實(shí)現(xiàn),把所有subtree的結(jié)果合并

            };

            ?

            class DomText : public DomBase

            {

            public:

            ????void FormatToText(ostream& o); // 直接輸出文字

            };

            class DomImg : public DomBase

            {

            public:

            ????void FormatToText(ostream& o); // 輸出imgtag內(nèi)容

            };

            // 其它實(shí)現(xiàn)略

            class DomA : public DomBase{};

            class DomDiv : public DomBase{};

            ?

            這已經(jīng)構(gòu)成一個(gè)基本的HTML的Dom Tree了。現(xiàn)在我提一個(gè)要求如下,要求在不修改原有代碼只添加新代碼的情況下,避免dynamic_cast,實(shí)現(xiàn)一個(gè)考察一顆Dom Tree是否包含超鏈接的功能。能做嗎?

            ?

            無(wú)論大家如何苦思冥想,答案都是做不到。盡管這么一看可能覺(jué)得這不是什么大事,但實(shí)際上這意味著:你無(wú)法通過(guò)添加模塊的方式來(lái)給一個(gè)已知的Dom Tree添加"判斷它是否包含超鏈接"的這個(gè)功能。有的人可能會(huì)說(shuō),那把它建模成動(dòng)態(tài)類(lèi)型的樹(shù)不就可以了?這是沒(méi)錯(cuò),但這實(shí)際上有兩個(gè)問(wèn)題。第一個(gè)是著顯著的增加了你的測(cè)試成本,不過(guò)對(duì)于充滿(mǎn)了廉價(jià)勞動(dòng)力的web行業(yè)來(lái)說(shuō)這好像也不是什么大問(wèn)題。第二個(gè)更加本質(zhì)——HTML可以這么做,并不代表所有的東西都可以裝怎么做事吧。

            ?

            那在靜態(tài)類(lèi)型的前提下,要如何解決這個(gè)問(wèn)題呢?很久以前我們的《設(shè)計(jì)模式》就給我們提供了visitor模式,用來(lái)解決這樣的問(wèn)題。如果把這個(gè)Dom Tree修改成visitor模式的代碼的話,那原來(lái)FormatToText就會(huì)變成這個(gè)樣子:

            ?

            class DomText;

            class DomImg;

            class DomA;

            class DomDiv;

            ?

            class DomBase

            {

            public:

            ????virtual ~DomBase();

            ????static shared_ptr<DomBase> Parse(const wstring& htmlText);

            ?

            ????class IVisitor

            ????{

            ????public:

            ????????virtual ~IVisitor();

            ?

            ????????virtual void Visit(DomText* dom) = 0;

            ????????virtual void Visit(DomImg* dom) = 0;

            ????????virtual void Visit(DomA* dom) = 0;

            ????????virtual void Visit(DomDiv* dom) = 0;

            ????};

            ?

            ????virtual void Accept(IVisitor* visitor) = 0;

            };

            ?

            class DomText : public DomBase

            {

            public:

            ????void Accept(IVisitor* visitor)override

            ????{

            ????????visitor->Visit(this);

            ????}

            };

            class DomImg : public DomBase

            {

            public:

            ????void Accept(IVisitor* visitor)override

            ????{

            ????????visitor->Visit(this);

            ????}

            };

            class DomA : public DomBase

            {

            public:

            ????void Accept(IVisitor* visitor)override

            ????{

            ????????visitor->Visit(this);

            ????}

            };

            class DomDiv : public DomBase

            {

            public:

            ????void Accept(IVisitor* visitor)override

            ????{

            ????????visitor->Visit(this);

            ????}

            };

            ?

            class FormatToTextVisitor : public DomBase::IVisitor

            {

            private:

            ????ostream& o;

            public:

            ????FormatToTextVisitor(ostream& _o)

            ????????:o(_o)

            ????{

            ?

            ????}

            ?

            ????void Visit(DomText* dom){} // 直接輸出文字

            ????void Visit(DomImg* dom){} // 輸出imgtag內(nèi)容

            ????void Visit(DomA* dom){} // 默認(rèn)實(shí)現(xiàn),把所有subtree的結(jié)果合并

            ????void Visit(DomDiv* dom){} // 默認(rèn)實(shí)現(xiàn),把所有subtree的結(jié)果合并

            ?

            ????static void Evaluate(DomBase* dom, ostream& o)

            ????{

            ????????FormatToTextVisitor visitor(o);

            ????????dom->Accept(&visitor);

            ????}

            };

            ?

            看起來(lái)長(zhǎng)了不少,但是我們驚奇地發(fā)現(xiàn),這下子我們可以通過(guò)提供一個(gè)Visitor,來(lái)在不修改原有代碼的前提下,避免dynamic_cast,實(shí)現(xiàn)判斷一顆Dom Tree是否包含超鏈接的功能了!不過(guò)別高興得太早。這兩種做法都是有缺陷的。

            ?

            虛函數(shù)的好處是你可以在不修改原有代碼的前提下添加新的Dom類(lèi)型,但是所有針對(duì)Dom Tree的操作緊密的耦合在了一起,并且邏輯還分散在了每一個(gè)具體的Dom類(lèi)型里面。你添加一個(gè)新功能就要修改所有的DomBase的子類(lèi),因?yàn)槟阋o他們都添加你需要的虛函數(shù)。

            ?

            Visitor的好處是你可以在不修改原有代碼的前提下添加新的Dom操作,但是所有的Dom類(lèi)型卻緊密的耦合在了一起,因?yàn)镮Visitor類(lèi)型要包含所有DomBase的子類(lèi)。你每天加一個(gè)新的Dom類(lèi)型就得修改所有的操作——即IVisitor的接口和所有的具體的Visitor。而且還有另一個(gè)問(wèn)題,就是虛函數(shù)的默認(rèn)實(shí)現(xiàn)寫(xiě)起來(lái)比較鳥(niǎo)了。

            ?

            所以這兩種做法都各有各的耦合。

            ?

            1. 碰撞系統(tǒng)

            ?

            看了上面對(duì)于虛函數(shù)和Visitor的描述,大家大概知道了虛函數(shù)和Visitor其實(shí)都是同一個(gè)東西,只是各有各的犧牲。因此他們是可以互相轉(zhuǎn)換的——大家通過(guò)不斷地練習(xí)就可以知道如何把一個(gè)解法表達(dá)成虛函數(shù)的同時(shí)也可以表達(dá)成Visitor了。但是Visitor的代碼又臭又長(zhǎng),所以下面我只用虛函數(shù)來(lái)寫(xiě),懶得敲太多代碼了。

            ?

            虛函數(shù)只有一個(gè)this參數(shù),所以他是single dynamic dispatch。對(duì)于碰撞系統(tǒng)來(lái)說(shuō),不同種類(lèi)的物體之間的碰撞代碼都是不一樣的,所以他有兩個(gè)"this參數(shù)",所以他是multiple dynamic dispatch。在接下來(lái)的描述會(huì)發(fā)現(xiàn),只要遇上了multiple dynamic dispatch,在現(xiàn)有的架構(gòu)下避免dynamic_cast,無(wú)論你用虛函數(shù)還是visitor模式,做出來(lái)的solution全都是不管操作有沒(méi)有偶合在一起,反正類(lèi)型是肯定會(huì)偶合在一起的。

            ?

            現(xiàn)在我們面對(duì)的問(wèn)題是這樣的。在物理引擎里面,我們經(jīng)常需要判斷兩個(gè)物體是否碰撞。但是物體又不只是三角形組成的多面體,還有可能是標(biāo)準(zhǔn)的球形啊、立方體什么的。因此這顯然還是一個(gè)繼承的結(jié)構(gòu),而且還有一個(gè)虛函數(shù)用來(lái)判斷一個(gè)對(duì)象跟另一個(gè)對(duì)象是否碰撞:

            ?

            class Geometry

            {

            public:

            ????virtual ~Geometry();

            ?

            ????virtual bool IsCollided(Geometry* second) = 0;

            };

            ?

            class Sphere : public Geometry

            {

            public:

            ????bool IsCollided(Geometry* second)override

            ????{

            ????????// then ???

            ????}

            };

            ?

            class Cube : public Geometry

            {

            public:

            ????bool IsCollided(Geometry* second)override

            ????{

            ????????// then ???

            ????}

            };

            ?

            class Triangles : public Geometry

            {

            public:

            ????bool IsCollided(Geometry* second)override

            ????{

            ????????// then ???

            ????}

            };

            ?

            大家猛然發(fā)現(xiàn),在這個(gè)函數(shù)體里面也不知道second到底是什么東西。這意味著,我們還要對(duì)second做一次single dynamic dispatch,這也就意味著我們需要添加新的虛函數(shù)。而且這不是一個(gè),而是很多。他們分別是什么呢?由于我們已經(jīng)對(duì)first(也就是那個(gè)this指針)dispatch過(guò)一次了,所以我們要把dispatch的結(jié)果告訴second,要讓它在dispatch一次。所以當(dāng)first分別是Sphere、Cube和Triangles的時(shí)候,對(duì)second的dispatch應(yīng)該有不同的邏輯。因此很遺憾的,代碼會(huì)變成這樣:

            ?

            class Sphere;

            class Cube;

            class Triangles;

            ?

            class Geometry

            {

            public:

            ????virtual ~Geometry();

            ?

            ????virtual bool IsCollided(Geometry* second) = 0;

            ????virtual bool IsCollided_Sphere(Sphere* first) = 0;

            ????virtual bool IsCollided_Cube(Cube* first) = 0;

            ????virtual bool IsCollided_Triangles(Triangles* first) = 0;

            };

            ?

            class Sphere : public Geometry

            {

            public:

            ????bool IsCollided(Geometry* second)override

            ????{

            ????????return second->IsCollided_Sphere(this);

            ????}

            ?

            ????bool IsCollided_Sphere(Sphere* first)override

            ????{

            ????????// Sphere * Sphere

            ????}

            ?

            ????bool IsCollided_Cube(Cube* first)override

            ????{

            ????????// Cube * Sphere

            ????}

            ?

            ????bool IsCollided_Triangles(Triangles* first)override

            ????{

            ????????// Triangles * Sphere

            ????}

            };

            ?

            class Cube : public Geometry

            {

            public:

            ????bool IsCollided(Geometry* second)override

            ????{

            ????????return second->IsCollided_Cube(this);

            ????}

            ?

            ????bool IsCollided_Sphere(Sphere* first)override

            ????{

            ????????// Sphere * Cube

            ????}

            ?

            ????bool IsCollided_Cube(Cube* first)override

            ????{

            ????????// Cube * Cube

            ????}

            ?

            ????bool IsCollided_Triangles(Triangles* first)override

            ????{

            ????????// Triangles * Cube

            ????}

            };

            ?

            class Triangles : public Geometry

            {

            public:

            ????bool IsCollided(Geometry* second)override

            ????{

            ????????return second->IsCollided_Triangles(this);

            ????}

            ?

            ????bool IsCollided_Sphere(Sphere* first)override

            ????{

            ????????// Sphere * Triangles

            ????}

            ?

            ????bool IsCollided_Cube(Cube* first)override

            ????{

            ????????// Cube * Triangles

            ????}

            ?

            ????bool IsCollided_Triangles(Triangles* first)override

            ????{

            ????????// Triangles * Triangles

            ????}

            };

            ?

            大家可以想象,如果還有第三個(gè)Geometry參數(shù),那還得給Geometry加上9個(gè)新的虛函數(shù),三個(gè)子類(lèi)分別實(shí)現(xiàn)他們,加起來(lái)我們一共要寫(xiě)13個(gè)虛函數(shù)(3^0 + 3^1 + 3^2)39個(gè)函數(shù)體(3^1 + 3^2 + 3^3)。

            ?

            1. 結(jié)尾

            ?

            為什么運(yùn)行期的類(lèi)型擴(kuò)展就那么多翔,而靜態(tài)類(lèi)型的擴(kuò)展就不會(huì)呢?原因是靜態(tài)類(lèi)型的擴(kuò)展是寫(xiě)在類(lèi)型的外部的。假設(shè)一下,我們的C++支持下面的寫(xiě)法:

            ?

            bool IsCollided(switch Geometry* first, switch Geometry* second);

            bool IsCollided(case Sphere* first, case Sphere* second);

            bool IsCollided(case Sphere* first, case Cube* second);

            bool IsCollided(case Sphere* first, case Triangles* second);

            bool IsCollided(case Cube* first, case Sphere* second);

            bool IsCollided(case Cube* first, case Cube* second);

            bool IsCollided(case Cube* first, case Triangles* second);

            bool IsCollided(case Triangles* first, case Sphere* second);

            bool IsCollided(case Triangles* first, case Cube* second);

            bool IsCollided(case Triangles* first, case Triangles* second);

            ?

            最后編譯器在編譯的時(shí)候,把所有的"動(dòng)態(tài)偏特化"收集起來(lái)——就像做模板偏特化的時(shí)候一樣——然后替我們生成上面一大片翔一樣的虛函數(shù)的代碼,那該多好啊!

            ?

            Dynamic dispatch和解耦這從一開(kāi)始以來(lái)就是一對(duì)矛盾,要徹底解決他們其實(shí)是很難的。雖然上面的作法看起來(lái)類(lèi)型和操作都解耦了,可實(shí)際上這就讓我們失去了本地代碼的dll的功能了。因?yàn)榫幾g器不可能收集到以后才動(dòng)態(tài)鏈接進(jìn)來(lái)的dll代碼里面的"動(dòng)態(tài)偏特化"的代碼對(duì)吧。不過(guò)這個(gè)問(wèn)題對(duì)于像CLR一樣基于一個(gè)VM一樣的支持JIT的runtime來(lái)講,這其實(shí)并不是個(gè)大問(wèn)題。而且Java的J2EE也好,Microsoft的Enterprise Library也好,他們的IoC(Inverse of Control)其實(shí)也是在模擬這個(gè)寫(xiě)法。我認(rèn)為以后靜態(tài)類(lèi)型語(yǔ)言的方向,肯定是朝著這個(gè)路線走的。盡管這些概念再也不能被直接map到本地代碼了,但是這讓我們從語(yǔ)義上的耦合中解放了出來(lái),對(duì)于寫(xiě)需要穩(wěn)定執(zhí)行的大型程序來(lái)說(shuō),有著莫大的助。

            posted on 2013-11-10 01:06 陳梓瀚(vczh) 閱讀(10129) 評(píng)論(6)  編輯 收藏 引用 所屬分類(lèi): 啟示

            評(píng)論:
            # re: 如何設(shè)計(jì)一門(mén)語(yǔ)言(十二)——設(shè)計(jì)可擴(kuò)展的類(lèi)型 2013-11-10 06:49 | lichray
            range-for 只找 ADL begin/end,既不是全局也不是 T::begin/end.
            你那個(gè)例子用全特化寫(xiě)也一樣。
            std::range 和 range-for 從沒(méi)扯上關(guān)系過(guò)。前者是 range study group 的開(kāi)端,不過(guò)其本身被迅速否決了。  回復(fù)  更多評(píng)論
              
            # re: 如何設(shè)計(jì)一門(mén)語(yǔ)言(十二)——設(shè)計(jì)可擴(kuò)展的類(lèi)型 2013-11-10 14:36 | lichray
            好吧,我知道你說(shuō)的需要「函數(shù)模板偏特化」的意思了。  回復(fù)  更多評(píng)論
              
            # re: 如何設(shè)計(jì)一門(mén)語(yǔ)言(十二)——設(shè)計(jì)可擴(kuò)展的類(lèi)型 2013-11-10 21:06 | 幻の上帝
            序號(hào)怎么都是1……
            反了。不就是要模式匹配么。還得事先糾結(jié)編譯期和運(yùn)行時(shí)的區(qū)別?
            關(guān)于函數(shù)模板偏特化和重載之間的取舍,你低估了一個(gè)成本:特化對(duì)代碼出現(xiàn)位置要求很死板,比如作用域問(wèn)題,再如考慮extern template往往需要小心調(diào)整順序;重載就沒(méi)這么多條條框框,算上ADL用起來(lái)多快好省。
            C++重載的邪惡之處在于考慮一般情況本身的規(guī)則就非常羅嗦(尤其是各種transformation混搭的overloading resolution),用起來(lái)經(jīng)常讓人沒(méi)底,實(shí)現(xiàn)起來(lái)也麻煩。
            至于不應(yīng)該用重載的地方用了重載,我只能說(shuō),不作死就不會(huì)死。但是另一方面,看到別人的渣代碼因?yàn)楦鼑?yán)格的類(lèi)型規(guī)則的限制反而用了不夠準(zhǔn)確表達(dá)意圖的類(lèi)型,可修改性通常更拙計(jì),造成的麻煩往往更大。
            這里所謂的鴨子,一般地說(shuō)不就是structural typing么。本質(zhì)的優(yōu)缺點(diǎn)和上面類(lèi)似。

              回復(fù)  更多評(píng)論
              
            # re: 如何設(shè)計(jì)一門(mén)語(yǔ)言(十二)——設(shè)計(jì)可擴(kuò)展的類(lèi)型 2013-11-10 21:37 | 幻の上帝
            @lichray
            range-based for不是先找成員begin/end再ADL的么。  回復(fù)  更多評(píng)論
              
            # re: 如何設(shè)計(jì)一門(mén)語(yǔ)言(十二)——設(shè)計(jì)可擴(kuò)展的類(lèi)型 2013-11-11 03:56 | 陳梓瀚(vczh)
            @幻の上帝
            說(shuō)實(shí)話這是C++把偏特化做成這樣造成的,而不是偏特化本身有什么問(wèn)題。再說(shuō)了加上concept就不是structural typing  回復(fù)  更多評(píng)論
              
            # re: 如何設(shè)計(jì)一門(mén)語(yǔ)言(十二)——設(shè)計(jì)可擴(kuò)展的類(lèi)型 2015-02-19 05:23 | earthengine
            其實(shí)編譯器如果要實(shí)現(xiàn)C++的動(dòng)態(tài)分發(fā)的話,策略還是有的。首先,連接器需要支持“組合數(shù)據(jù)區(qū)段”的功能,同一個(gè)符號(hào)可以包含不同的部分分散在不同的目標(biāo)文件,并且自動(dòng)生成一個(gè)索引。其次,對(duì)于每一組需要多重分發(fā)的函數(shù),要建立分層次的分發(fā)表。當(dāng)IsCollided(x,y)被調(diào)用時(shí),先找x的虛函數(shù)表,從中找到IsCollided分發(fā)表的位置,再?gòu)膟的虛函數(shù)表中找出一個(gè)索引。這樣在x的IsColided分發(fā)表里面就可以找到真正函數(shù)的位置。

            如果有3個(gè)參數(shù)x,y,z要?jiǎng)討B(tài)分發(fā),則上面過(guò)程中找到的只是2級(jí)分發(fā)表的位置,還需要結(jié)合z虛函數(shù)表里面的索引值來(lái)找到2級(jí)分發(fā)表表項(xiàng),那個(gè)才是真正的函數(shù)位置。

            這就是為什么連接器需要自動(dòng)生成索引,因?yàn)橐陨系姆职l(fā)表必須在連接時(shí)生成,而且可能跨越不同的目標(biāo)文件。除此之外,還必須在生成虛函數(shù)表和分發(fā)表的時(shí)候允許字段的值到連接時(shí)才確定,因?yàn)椴坏竭B接時(shí)刻索引值都是不確定的。  回復(fù)  更多評(píng)論
              
            亚洲七七久久精品中文国产| 久久亚洲2019中文字幕| 99久久精品无码一区二区毛片| 无码国内精品久久人妻| 人人狠狠综合88综合久久| 7国产欧美日韩综合天堂中文久久久久 | 中文字幕成人精品久久不卡| 国产精品99久久99久久久| 精品久久久久久成人AV| 99久久精品国产麻豆| 91精品国产色综合久久| 国产精品久久波多野结衣| 99久久人妻无码精品系列蜜桃| 国内精品久久久久伊人av| 精品综合久久久久久888蜜芽| 精品久久久久久成人AV| 久久精品国产秦先生| 久久久久黑人强伦姧人妻| 午夜精品久久久久久久无码| 久久伊人色| 久久国产精品77777| 品成人欧美大片久久国产欧美...| 久久久久亚洲爆乳少妇无| 久久久这里有精品| 久久国产乱子伦免费精品| 狠狠色综合久久久久尤物| 久久午夜免费视频| 久久精品国产亚洲AV嫖农村妇女 | 99久久精品国产综合一区| 久久er国产精品免费观看8| 2021国产精品午夜久久| 99久久99这里只有免费费精品| 88久久精品无码一区二区毛片| 亚洲中文字幕伊人久久无码| 蜜臀久久99精品久久久久久小说| 青青草原1769久久免费播放| 三级韩国一区久久二区综合| 久久精品国产亚洲AV麻豆网站| 久久久久无码国产精品不卡| 日韩精品久久久肉伦网站| 久久国产精品波多野结衣AV|