• <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  評論-2670  文章-0  trackbacks-0

            面向對象這個抽象的特例總是有說不完的話題,更糟糕的是很多語言都錯誤地實現了面向對象——class居然可以當一個變量類型什么的這只是讓人們寫代碼寫的更糟糕而已。當然這個話題第三篇文章已經說過了,現在來談談人們喜歡拿來裝逼的另一個話題——消息發送。

            按照慣例先來點題外話。說到消息發送,有些人喜歡跳出來說,objective-c的消息做得多優雅啊,代碼都可以寫成一句話[golang screw:you you:suck]之類的。其實這個還做得不夠徹底。在幾年前易語言曾經火了一陣,但是為什么大家這么討厭他呢?其實顯然不是因為每個token都是漢字,而是因為他做的一點都不像中文,誰會說話的時候帶那么多符號呀。其實objective-c也一樣,沒人會因為想在一句英語里面用冒號來分割短語的。

            當我還在讀大三的時候,我由于受到了Apple Script(也是蘋果做的)的啟發,試圖發明一門語言,讓他可以盡量寫起來像自然語言——當然他仍然是嚴格的編程語言。但是這門語言因為其奇特的語法結構,我只好自己想出了一個兩遍parse地方法。第一遍parse出所有函數頭,讓后用這些函數頭臨時組成一個parser,用它來parse語句的部分。后面整個也實現出來了,然后我就去做了一下調查,發現大家不喜歡,原因是要輸入的東西太多了。不過我這里還是貼一下當初是怎么設計的:

            phrase print(content) is
                external function "writeln"
            end phrase
            
            phrase first (count) items of fibonacci sequence is
                if count equals to 1 then
                    result is [1]
                else if count equals to 2 then
                    result is [1,1]
                else
                    let list be [1,1]
                    repeat with i from 3 to count
                        let list be list joins with item length of list - 1 of list + item length of list - 2 of list
                    end
                    result is list
                end
            end phrase
            
            phrase (number) is odd is
                result is number mod 2 is 0
            end phrase alias odd number
            
            phrase append (item) after (list) is
                let 0 element of list from length of list be [item]
            end phrase
            
            phrase ((item) is validated) in (list) is
                let filtered list be []
                repeat with item in list
                    append item after filtered list if item is validated
                end
                result is filtered list
            end phrase
            
            phrase main is
                print odd number in first 10 items of fibonacci sequence
            end phrase

            倒數第二個函數聲明甚至連函數指針的聲明也如此的優雅(我自己認為的),整個程序組織起來,我們要輸出斐波那契數列里面前10個數字中間的奇數,于是就寫成了

            print odd number in first 10 items of fibonacci sequence

            看起來比objective-c要漂亮把。其實如果想把所有的東西換成中文,算法也不需要變化。現在用空格來分割一個一個的詞,中文直接用字符就好了,剩下的都一樣。要parse這個程序根本沒辦法用流行的那些方法來parse。當然我知道大家也不會關心這些特別復雜的問題,于是題外話就到這里結束了,這個語言的實現的代碼你們大概也永遠都不會看到的,啊哈哈哈哈。

            為什么要提這件事情呢?我主要是想告訴大家,就算你在用面向對象語言,想在程序里面給一個對象發送一條消息,這個對象并不是非得寫在最前面的。為什么呢?有的時候對象不止一個——這個東西叫multiple dispatching,著名的問題就是如何給一堆面向對象的幾何體類做他們的求交函數——用面向對象的慣用做法做起來會特別的難受。不過現在我們先來看一下普通的消息發送是什么樣子的。

            對于一個我們知道他是什么類型的對象來說,發送一個消息就跟直接調用一個函數一樣,因為你不需要去resolve一下這個函數到底是誰。譬如說下面的代碼:

            class Language
            {
            public:
                void YouSuck(){ ... }
            };
            
            Language golang;
            golang.YouSuck();

            最終翻譯出來的結果會近似

            struct Language
            {
            };
            
            void Language_YouSuck(Language* const this)
            {
                ...
            }
            
            Language golang;
            Language_YouSuck(&golang);

            很多人其實并不能在學習面向對象語言的時候就直接意識到這一點。其實我也是在高中的時候玩delphi突然就在網上看見了這么一篇文章,然后我才明白的。看起來這個過渡并不是特別的自然是不是。

            當你要寫一個獨立的class,不繼承自任何東西的時候,這個class的作用只有兩個。第一個是封裝,這個第三篇文章已經提到過了。第二個作用就是給里面的那些函數做一個匿名的namespace。這是什么意思呢?就像上面的代碼一樣,你寫golang.YouSuck(),編譯器會知道golang是一個Language,然后去調用Language::YouSuck()。如果你調用lisp.YouSuck()的時候,說不定lisp是另一個叫做BetterThanGolangLanguage的類型,然后他就去里面找了YouSuck。這里并不會因為兩個YouSuck的名字一樣,編譯器就把它搞混了。這個東西這跟重載也差不多,我就曾經在Microsoft Research里面看見過一個人做了一個語言(主要是用來驗證語言本身的正確性的),其中a.b(c, d)是b(a, c, d)的語法糖,這個“.”毫無特別之處。

            有一天,情況變了。專門開發蹩腳編譯器的AMD公司看見golang很符合他們的口味,于是也寫了一個golang的實現。那這個事情應該怎么建模呢?因為golang本身是一套標準,你可也可以稱呼他為協議,然后下面有若干個實現。所以Language本身作為一個category也只好跟著golang變成interface了。為了程序簡單我們只看其中的一個小片段:

            class IGolang
            {
            public:
                virtual void YouSuck()=0;
            };
            
            class GoogleGolang : public IGolang
            {
            public:
                void YouSuck()override{ /*1*/ }
            };
            
            class AmdGolang : public IGolang
            {
            public:
                void YouSuck()override{ /*2*/ }
            };
            
            IGolang* golang = new GoogleGolang;
            golang->YouSuck();

            我很喜歡VC++的專有關鍵字override,他可以在我想override但是不小心寫錯了一點的時候提示我,避免了我大量的錯誤的發生。當然這個東西別的編譯器不支持,所以我在我的代碼的靠前的地方寫了一個宏,發現不是VC++再編譯,我就把override給#define成空的。反正我的程序里面不會用關鍵字來當變量名的。

            看著這個程序,已經不能單純的用GoogleGolang_YouSuck(golang)來代替這個消息發送了,因為類型是IGolang的話說不定下面是一個AmdGolang。所以在這里我們就要引入虛函數表了。一旦引入了虛函數表,代碼就會瞬間變得復雜起來。我見過很多人問,虛函數表那么大,要是每一個類的實例都帶一個表的話豈不是很浪費內存?這種人就應該先去看《Inside the C++ Object Model》,然后再反省一下自己的問題有多么的——呃——先看帶有虛函數表的程序長什么樣子好了:

            struct vtable_IGolang
            {
                void (*YouSuck)(IGolang* const this);
            };
            
            struct IGolang
            {
                vtable_IGolang* vtable;
            };
            
            //---------------------------------------------------
            
            vtable_IGolang vtable_GoogleGolang;
            vtable_GoogleGolang.YouSuck = &vtable_GoogleGolang_YouSuck;
            
            struct GoogleGolang
            {
                IGolang parent;
            };
            
            void vtable_GoogleGolang_YouSuck(IGolang* const this)
            {
                int offset=(int)(&((GoogleGolang*)0)->parent);
                GoogleGolang_YouSuck((GoogleGolang*)((char*)this-offset));
            }
            
            void GoogleGolang_YouSuck(GoogleGolang* const this)
            {
                /*1*/
            }
            
            void GoogleGolang_ctor(GoogleGolang* const this)
            {
                this->parent->vtable = &vtable_GoogleGolang;
            }
            
            //---------------------------------------------------
            // AmdGolang略,長得都一樣
            //---------------------------------------------------
            
            GoogleGolang* tmp = (GoogleGolang*)malloc(sizeof(GoogleGolang));
            GoogleGolang_ctor(tmp);
            IGolang* golang = &tmp->parent;
            golang->vtable->YouSuck(golang);

            基本上已經面目全非了。當然實際上C++生成的代碼比這個要復雜得多。我這里只是不想把那些細節牽引進來,針對我們的那個例子寫了個可能的實現。面向對象的語法糖多么的重要啊,盡管你也可以在需要的時候用C語言把這些東西寫出來(就跟那個愚蠢的某著名linux GUI框架一樣),但是可讀性已經完全喪失了吧。明明那么幾行就可以表達出來的東西,我們為了達到同樣的性能,用C寫要把代碼寫成屎。東西一多,名字用完了,都只好對著代碼發呆了,決定把C扔了,完全用C++來寫。萬一哪天用到了virtual繼承——在某些情況下其實是相當好用的,譬如說第三篇文章講的,在C++里面用interface,而且也很常見——那用C就只能呵呵呵了,寫出來的代碼再也沒法讀了,沒法再把OOP實踐下去了。

            好了,消息發送的簡單的實現大概也就講到這里了。只要不是C++,其他語言譬如說只有單根繼承的Delphi,實現OOP大概也就是上面這個樣子。于是我們圍繞著消息發送的語法糖玩了很久,終于遇到了兩大終極問題。這兩個問題說白了都是開放和封閉的矛盾。我們用基類和一大堆子類的結構來寫程序的時候,需要把邏輯都封裝在虛函數里面,不然的話你就得cast了,cast是將程序最終導向失控的根源之一。這個時候我們對類型擴展是開放的,而對邏輯擴展是封閉的。這是什么意思呢?讓我們來看下面這個例子:

            class Shape
            {
            public:
                virtual double GetArea()=0;
                virtual bool HitTest(Point p)=0;
            };
            
            class Circle : public Shape ...;
            class Rectangle : public Shape ... ;

            我們每當添加一個新形狀的時候,只要實現GetArea和HitTest,那么事情就做完了。所以你可以無限的添加新形狀——所以類型擴展是開放的。但是你卻永遠只能做GetArea和HitTest——對邏輯擴展是封閉的。你如果想做除了GetArea和HitTest以外的更多的事情的話,這個時候你就被迫做cast了。那么在類型相對穩定的情況下有沒有別的方法呢?設計模式告訴我們,我們可以用Visitor來把情況扭轉過來——做成對類型擴展封閉,而對邏輯擴展開放的:

            class IShapeVisitor
            {
            public:
                virtual void Visit(Circle* shape)=0;
                virtual void Visit(Rectangle* shape)=0;
            };
            
            class Shape
            {
            public:
                virtual void Accept(IShapeVisitor* visitor)=0;
            };
            
            class Circle : public Shape
            {
            public:
                ...
            
                void Accept(IShapeVIsitor* visitor)override
                {
                    visitor->Visit(this);  // 因為重載的關系,會調用到第一個Visit函數
                }
            };
            
            class Rectangle : public Shape
            {
            public:
                ...
            
                void Accept(IShapeVIsitor* visitor)override
                {
                    visitor->Visit(this);  // 因為重載的關系,會調用到第二個Visit函數
                }
            };
            
            //------------------------------------------
            
            class GetAreaVisitor : public IShapeVisitor
            {
            public:
                double result;
            
                void Visit(Circle* shape)
                {
                    result = ...;
                }
            
                void Visit(Rectangle* shape)
                {
                    result = ...;
                }
            };
            
            class HitTestVisitor : public IShapeVisitor ...;

            這個時候GetArea可能調用起來就不是那么方便了,不過我們總是可以把它寫成一個函數:

            double GetArea(Shape* shape)
            {
                GetAreaVisitor visitor;
                shape->Accept(&visitor);
                return visitor.result;
            }

            這個時候你可以隨意的做新的事情了,但是一旦需要添加新類型的時候,你需要改動很多東西,首先是Visitor的接口,其實是讓所有的邏輯都支持新類型,這樣你就不能僅僅通過添加新代碼來擴展新類型了。所以這就是對邏輯擴展開放,而對類型擴展封閉了。

            所以第一個問題就是:能不能做成類型擴展也開放,邏輯擴展也開放呢?在回答這個問題之前,我們先來看下一個問題。我們要對兩個Shape進行求交,看看他們是不是有重疊在一起的部分。但是每一個具體的Shape,譬如Circle啊Rectangle啊,定義都是不一樣的,沒辦法有通用的處理辦法,所以我們只能寫3個函數了(RR, CC, CR)。如果有3各類型,那么我們就需要6個函數。如果有4個類型,那我們就需要有10個函數——才能處理所有情況。公式倒是可以一下子看出來,函數數量就等于1+2+ … +n,n等于類型的數量。

            這看起來好像是一個類型擴展開放的問題是吧,但是實際上他只能用邏輯擴展的方法來做。為什么呢?你看我們的一個visitor其實很像是我們對一個一個的具體類型都試一下看看shape是不是這個類型,從而做出正確的處理。不過這跟我們直接用if地方法相比有兩個優點:1、快;2、編譯器替你查錯有保證。

            那實際上應該怎么做呢?想想,我們這里有兩次“if type”。第一次針對第一個參數,第二次針對第二個參數。所以我們一共需要n+1=3個visitor。寫的方法倒是不復雜,首先我們得準備好RR,CC,CR三個邏輯,然后用visitor去識別類型然后調用它們:

            bool IntersectCC(Circle* s1, Circle* s2){ ... }
            bool IntersectCR(Circle* s1, Rectangle* s2){ ... }
            bool IntersectRR(Rectangle* s1, Rectangle* s2){ ... }
            // RC和CR是一樣的
            
            class IntersectWithCircleVisitor : public IShapeVisitor
            {
            public:
                Circle* s1;
                bool result;
            
                void Visit(Circle* shape)
                {
                    result=IntersectCC(s1, shape);
                }
            
                void Visit(Rectangle* shape)
                {
                    result=IntersectCR(s1, shape);
                }
            };
            
            class IntersectWithRectangleVisitor : public IShapeVisitor
            {
            public:
                Rectangle* s1;
                bool result;
            
                void Visit(Circle* shape)
                {
                    result=IntersectCR(shape, s1);
                }
            
                void Visit(Rectangle* shape)
                {
                    result=IntersectRR(s1, shape);
                }
            };
            
            class IntersectVisitor : public IShapeVisitor
            {
            public:
                bool result;
                IShape* s2;
            
                void Visit(Circle* shape)
                {
                    IntersectWithCircleVisitor visitor;
                    visitor.s1=shape;
                    s2->Accept(&visitor);
                    result=visitor.result;
                }
            
                void Visit(Rectangle* shape)
                {
                    IntersectWithRectangleVisitor visitor;
                    visitor.s1=shape;
                    s2->Accept(&visitor);
                    result=visitor.result;
                }
            };
            
            bool Intersect(Shape* s1, Shape* s2)
            {
                IntersectVisitor visitor;
                visitor.s2=s2;
                s1->Accept(&visitor);
                return visitor.result;
            }

            我覺得你們現在心里的想法肯定是:“我屮艸芔茻。”嗯,這種事情在物理引擎里面是經常要碰到的。然后當你需要添加一個新的形狀的時候,呵呵呵呵呵呵呵呵。不過這也是沒辦法的,誰讓現在的要求運行時性能的面向對象語言都這么做呢?

            當然,如果在不要求性能的情況下,我們可以用ruby和它的mixin來做。至于說怎么辦,其實你們應該發現了,添加一個Visitor和添加一個虛函數的感覺是差不多的。所以只要把Visitor當成虛函數的樣子,讓Ruby給mixin一堆新的函數進各種類型就好了。不過只有支持運行時mixin的語言才能做到這一點。強類型語言我覺得是別想了。

            Mixin地方法倒是很直接,我們只要把每一個Visitor里面的Visit函數都給加進去就好了,大概感覺上就類似于:

            class Shape
            {
            public:
                // Mixin的時候等價于給每一個具體的Shape類都添加下面三個虛函數的重寫
                virtual bool Intersect(Shape* s2)=0;
                virtual bool IntersectWithCircle(Circle* s1)=0;
                virtual bool IntersectWithRectangle(Rectangle* s1)=0;
            };
            
            //--------------------------------------------
            
            bool Circle::Intersect(Shape* s2)
            {
                return s2->IntersectWithCircle(this);
            }
            
            bool Rectangle::Intersect(Shape* s2)
            {
                return s2->IntersectWithRectangle(this);
            }
            
            //--------------------------------------------
            
            bool Circle::IntersectWithCircle(Circle* s1)
            {
                return IntersectCC(s1, this);
            }
            
            bool Rectangle::IntersectWithCircle(Circle* s1)
            {
                return IntersectCR(s1, this);
            }
            
            //--------------------------------------------
            
            bool Circle::IntersectWithRectangle(Rectangle* s1)
            {
                return IntersectCR(this, s1);
            }
            
            bool Rectangle::IntersectWithRectangle(Rectangle* s1)
            {
                return IntersectRR(s1, this);
            }

            這下子應該看出來為什么我說這種方法只能用Visitor了吧,否則就要把所有類型都寫進Shape,就會很奇怪了。如果這樣的邏輯一多,類型也有四五個的話,那每加一個邏輯就得添加一批虛函數,Shape類很快就會被玩壞了。而代表邏輯的Visitor是可以放在不同的地方的,互相之間是隔離的,維護起來就會比較容易。

            那現在我們就要有第二個問題了:在擁有兩個“this”的情況下,我們要如何做才能把邏輯做成類型擴展也開放,邏輯擴展也開放呢?然后參考我們的第一個問題:能不能做成類型擴展也開放,邏輯擴展也開放呢?你應該心里有數了吧,答案當然是——不能做。

            這就是語言的極限了。面向對象才用的single dispatch的方法,能做到的東西是很有限的。情況稍微復雜那么一點點——就像上面對兩個形狀求交這種正常的問題——寫起來都這么難受。

            那呼應一下標題,如果我們要設計一門語言,來支持上面這種multiple dispatch,那可以怎么修改語法呢?這里面分為兩種,第一種是像C++這樣運行時load dll不增加符號的,第二種是像C#這樣運行時load dll會增加符號的。對于前一種,其實我們可以簡單的修改一下語法:

            bool Intersect(switch Shape* s1, switch Shape* s2);
            
            bool Intersect(case Circle* s1, case Circle* s2){ ... }
            bool Intersect(case Circle* s1, case Rectangle* s2){ ... }
            bool Intersect(case Rectangle* s1, case Circle* s2){ ... }
            bool Intersect(case Rectangle* s1, case Rectangle* s2){ ... }

            然后修改一下編譯器,把這些東西翻譯成虛函數塞回原來的Shape類里面就行了。對于第二種嘛,其實就相當于Intersect的根節點、Circle和CC寫在dll1,Rectangle和CR、RC、RR寫在dll2,然后dll1運行時把dll2給動態地load了進來,再之后調用Intersect的時候就好像“虛函數已經進去了”一樣。至于要怎么做,這個大家回去慢慢思考一下吧,啊哈哈哈。

            posted on 2013-05-24 19:08 陳梓瀚(vczh) 閱讀(11461) 評論(5)  編輯 收藏 引用 所屬分類: 啟示

            評論:
            # re: 如何設計一門語言(五)&mdash;&mdash;面向對象和消息發送 2013-05-24 20:29 | n00b
            SICP Exercise 2.76是類似的問題  回復  更多評論
              
            # re: 如何設計一門語言(五)&mdash;&mdash;面向對象和消息發送 2013-05-25 00:07 | rink1969
            這個說的就是Expression Problem嘛
            兩種方法其實就是代數方法和共代數方法。
            編程語言擴展的根本問題就是新的方法和數據類型在反序列化過程中如何保證整個類型系統不出問題。
            要求不嚴格的話,用宏隨便搞搞,夠用就行了。  回復  更多評論
              
            # re: 如何設計一門語言(五)&mdash;&mdash;面向對象和消息發送 2013-05-25 03:58 | 溪流
            學習了  回復  更多評論
              
            # re: 如何設計一門語言(五)&mdash;&mdash;面向對象和消息發送 2013-05-25 22:36 | Marvin
            面向對象很簡單,只是語言給做復雜了  回復  更多評論
              
            # re: 如何設計一門語言(五)&mdash;&mdash;面向對象和消息發送 2013-05-26 05:58 | cscope
            哼哼,CLOS——————multimethod  回復  更多評論
              
            97超级碰碰碰碰久久久久| 狠狠久久亚洲欧美专区 | 99久久综合国产精品免费| 亚洲国产精品久久久久婷婷软件 | 精品久久久久久国产潘金莲 | 久久综合九色综合久99| 久久91精品国产91久久麻豆| 久久国产免费观看精品3| 久久久婷婷五月亚洲97号色| 无码精品久久久久久人妻中字| 亚洲AV日韩精品久久久久久久 | 久久久久亚洲AV无码观看| 国产精品久久久久免费a∨| 久久久久99这里有精品10 | 久久一日本道色综合久久| 久久亚洲AV成人无码国产| 色婷婷综合久久久久中文| 成人综合伊人五月婷久久| 青青国产成人久久91网| 国产免费久久精品99久久| 性高湖久久久久久久久AAAAA| 国内精品人妻无码久久久影院导航| 久久夜色精品国产亚洲| 国产成人精品免费久久久久| 久久免费高清视频| 亚洲精品美女久久久久99小说 | 91精品国产乱码久久久久久| 国产欧美久久久精品| 久久天天日天天操综合伊人av| 久久只这里是精品66| 人妻精品久久无码专区精东影业 | 91久久成人免费| 伊人色综合久久天天人守人婷| 性色欲网站人妻丰满中文久久不卡| 狠狠色丁香婷综合久久| 久久婷婷五月综合97色直播| 日韩精品无码久久久久久| 精品人妻伦一二三区久久| 亚洲精品乱码久久久久久按摩 | 久久久精品人妻一区二区三区四| 国产成人久久777777|