如何设计一门语言(五)——面向对象和消息发送
面向?qū)ο筮@個抽象的特例總是有說不完的話題,更糟糕的是很多語言都錯誤地實(shí)現(xiàn)了面向?qū)ο蟆猚lass居然可以當(dāng)一個變量類型什么的這只是讓人們寫代碼寫的更糟糕而已。當(dāng)然這個話題第三篇文章已經(jīng)說過了,現(xiàn)在來談?wù)勅藗兿矚g拿來裝逼的另一個話題——消息發(fā)送。
按照慣例先來點(diǎn)題外話。說到消息發(fā)送,有些人喜歡跳出來說,objective-c的消息做得多優(yōu)雅啊,代碼都可以寫成一句話[golang screw:you you:suck]之類的。其實(shí)這個還做得不夠徹底。在幾年前易語言曾經(jīng)火了一陣,但是為什么大家這么討厭他呢?其實(shí)顯然不是因?yàn)槊總€token都是漢字,而是因?yàn)樗龅囊稽c(diǎn)都不像中文,誰會說話的時候帶那么多符號呀。其實(shí)objective-c也一樣,沒人會因?yàn)橄朐谝痪溆⒄Z里面用冒號來分割短語的。
當(dāng)我還在讀大三的時候,我由于受到了Apple Script(也是蘋果做的)的啟發(fā),試圖發(fā)明一門語言,讓他可以盡量寫起來像自然語言——當(dāng)然他仍然是嚴(yán)格的編程語言。但是這門語言因?yàn)槠淦嫣氐恼Z法結(jié)構(gòu),我只好自己想出了一個兩遍parse地方法。第一遍parse出所有函數(shù)頭,讓后用這些函數(shù)頭臨時組成一個parser,用它來parse語句的部分。后面整個也實(shí)現(xiàn)出來了,然后我就去做了一下調(diào)查,發(fā)現(xiàn)大家不喜歡,原因是要輸入的東西太多了。不過我這里還是貼一下當(dāng)初是怎么設(shè)計的:
phrase print(content) isexternal function "writeln" end phrasephrase first (count) items of fibonacci sequence isif count equals to 1 thenresult is [1]else if count equals to 2 thenresult is [1,1]elselet list be [1,1]repeat with i from 3 to countlet list be list joins with item length of list - 1 of list + item length of list - 2 of listendresult is listend end phrasephrase (number) is odd isresult is number mod 2 is 0 end phrase alias odd numberphrase append (item) after (list) islet 0 element of list from length of list be [item] end phrasephrase ((item) is validated) in (list) islet filtered list be []repeat with item in listappend item after filtered list if item is validatedendresult is filtered list end phrasephrase main isprint odd number in first 10 items of fibonacci sequence end phrase倒數(shù)第二個函數(shù)聲明甚至連函數(shù)指針的聲明也如此的優(yōu)雅(我自己認(rèn)為的),整個程序組織起來,我們要輸出斐波那契數(shù)列里面前10個數(shù)字中間的奇數(shù),于是就寫成了
print odd number in first 10 items of fibonacci sequence看起來比objective-c要漂亮把。其實(shí)如果想把所有的東西換成中文,算法也不需要變化?,F(xiàn)在用空格來分割一個一個的詞,中文直接用字符就好了,剩下的都一樣。要parse這個程序根本沒辦法用流行的那些方法來parse。當(dāng)然我知道大家也不會關(guān)心這些特別復(fù)雜的問題,于是題外話就到這里結(jié)束了,這個語言的實(shí)現(xiàn)的代碼你們大概也永遠(yuǎn)都不會看到的,啊哈哈哈哈。
為什么要提這件事情呢?我主要是想告訴大家,就算你在用面向?qū)ο笳Z言,想在程序里面給一個對象發(fā)送一條消息,這個對象并不是非得寫在最前面的。為什么呢?有的時候?qū)ο蟛恢挂粋€——這個東西叫multiple dispatching,著名的問題就是如何給一堆面向?qū)ο蟮膸缀误w類做他們的求交函數(shù)——用面向?qū)ο蟮膽T用做法做起來會特別的難受。不過現(xiàn)在我們先來看一下普通的消息發(fā)送是什么樣子的。
對于一個我們知道他是什么類型的對象來說,發(fā)送一個消息就跟直接調(diào)用一個函數(shù)一樣,因?yàn)槟悴恍枰esolve一下這個函數(shù)到底是誰。譬如說下面的代碼:
class Language { public:void YouSuck(){ ... } };Language golang; golang.YouSuck();最終翻譯出來的結(jié)果會近似于
struct Language { };void Language_YouSuck(Language* const this) {... }Language golang; Language_YouSuck(&golang);很多人其實(shí)并不能在學(xué)習(xí)面向?qū)ο笳Z言的時候就直接意識到這一點(diǎn)。其實(shí)我也是在高中的時候玩delphi突然就在網(wǎng)上看見了這么一篇文章,然后我才明白的。看起來這個過渡并不是特別的自然是不是。
當(dāng)你要寫一個獨(dú)立的class,不繼承自任何東西的時候,這個class的作用只有兩個。第一個是封裝,這個第三篇文章已經(jīng)提到過了。第二個作用就是給里面的那些函數(shù)做一個匿名的namespace。這是什么意思呢?就像上面的代碼一樣,你寫golang.YouSuck(),編譯器會知道golang是一個Language,然后去調(diào)用Language::YouSuck()。如果你調(diào)用lisp.YouSuck()的時候,說不定lisp是另一個叫做BetterThanGolangLanguage的類型,然后他就去里面找了YouSuck。這里并不會因?yàn)閮蓚€YouSuck的名字一樣,編譯器就把它搞混了。這個東西這跟重載也差不多,我就曾經(jīng)在Microsoft Research里面看見過一個人做了一個語言(主要是用來驗(yàn)證語言本身的正確性的),其中a.b(c, d)是b(a, c, d)的語法糖,這個“.”毫無特別之處。
有一天,情況變了。專門開發(fā)蹩腳編譯器的AMD公司看見golang很符合他們的口味,于是也寫了一個golang的實(shí)現(xiàn)。那這個事情應(yīng)該怎么建模呢?因?yàn)間olang本身是一套標(biāo)準(zhǔn),你可也可以稱呼他為協(xié)議,然后下面有若干個實(shí)現(xiàn)。所以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++的專有關(guān)鍵字override,他可以在我想override但是不小心寫錯了一點(diǎn)的時候提示我,避免了我大量的錯誤的發(fā)生。當(dāng)然這個東西別的編譯器不支持,所以我在我的代碼的靠前的地方寫了一個宏,發(fā)現(xiàn)不是VC++再編譯,我就把override給#define成空的。反正我的程序里面不會用關(guān)鍵字來當(dāng)變量名的。
看著這個程序,已經(jīng)不能單純的用GoogleGolang_YouSuck(golang)來代替這個消息發(fā)送了,因?yàn)轭愋褪荌Golang的話說不定下面是一個AmdGolang。所以在這里我們就要引入虛函數(shù)表了。一旦引入了虛函數(shù)表,代碼就會瞬間變得復(fù)雜起來。我見過很多人問,虛函數(shù)表那么大,要是每一個類的實(shí)例都帶一個表的話豈不是很浪費(fèi)內(nèi)存?這種人就應(yīng)該先去看《Inside the C++ Object Model》,然后再反省一下自己的問題有多么的——呃——先看帶有虛函數(shù)表的程序長什么樣子好了:
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);基本上已經(jīng)面目全非了。當(dāng)然實(shí)際上C++生成的代碼比這個要復(fù)雜得多。我這里只是不想把那些細(xì)節(jié)牽引進(jìn)來,針對我們的那個例子寫了個可能的實(shí)現(xiàn)。面向?qū)ο蟮恼Z法糖多么的重要啊,盡管你也可以在需要的時候用C語言把這些東西寫出來(就跟那個愚蠢的某著名linux GUI框架一樣),但是可讀性已經(jīng)完全喪失了吧。明明那么幾行就可以表達(dá)出來的東西,我們?yōu)榱诉_(dá)到同樣的性能,用C寫要把代碼寫成屎。東西一多,名字用完了,都只好對著代碼發(fā)呆了,決定把C扔了,完全用C++來寫。萬一哪天用到了virtual繼承——在某些情況下其實(shí)是相當(dāng)好用的,譬如說第三篇文章講的,在C++里面用interface,而且也很常見——那用C就只能呵呵呵了,寫出來的代碼再也沒法讀了,沒法再把OOP實(shí)踐下去了。
好了,消息發(fā)送的簡單的實(shí)現(xiàn)大概也就講到這里了。只要不是C++,其他語言譬如說只有單根繼承的Delphi,實(shí)現(xiàn)OOP大概也就是上面這個樣子。于是我們圍繞著消息發(fā)送的語法糖玩了很久,終于遇到了兩大終極問題。這兩個問題說白了都是開放和封閉的矛盾。我們用基類和一大堆子類的結(jié)構(gòu)來寫程序的時候,需要把邏輯都封裝在虛函數(shù)里面,不然的話你就得cast了,cast是將程序最終導(dǎo)向失控的根源之一。這個時候我們對類型擴(kuò)展是開放的,而對邏輯擴(kuò)展是封閉的。這是什么意思呢?讓我們來看下面這個例子:
class Shape { public:virtual double GetArea()=0;virtual bool HitTest(Point p)=0; };class Circle : public Shape ...; class Rectangle : public Shape ... ;我們每當(dāng)添加一個新形狀的時候,只要實(shí)現(xiàn)GetArea和HitTest,那么事情就做完了。所以你可以無限的添加新形狀——所以類型擴(kuò)展是開放的。但是你卻永遠(yuǎn)只能做GetArea和HitTest——對邏輯擴(kuò)展是封閉的。你如果想做除了GetArea和HitTest以外的更多的事情的話,這個時候你就被迫做cast了。那么在類型相對穩(wěn)定的情況下有沒有別的方法呢?設(shè)計模式告訴我們,我們可以用Visitor來把情況扭轉(zhuǎn)過來——做成對類型擴(kuò)展封閉,而對邏輯擴(kuò)展開放的:
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); // 因?yàn)橹剌d的關(guān)系,會調(diào)用到第一個Visit函數(shù) } };class Rectangle : public Shape { public:...void Accept(IShapeVIsitor* visitor)override{visitor->Visit(this); // 因?yàn)橹剌d的關(guān)系,會調(diào)用到第二個Visit函數(shù) } };//------------------------------------------class GetAreaVisitor : public IShapeVisitor { public:double result;void Visit(Circle* shape){result = ...;}void Visit(Rectangle* shape){result = ...;} };class HitTestVisitor : public IShapeVisitor ...;這個時候GetArea可能調(diào)用起來就不是那么方便了,不過我們總是可以把它寫成一個函數(shù):
double GetArea(Shape* shape) {GetAreaVisitor visitor;shape->Accept(&visitor);return visitor.result; }這個時候你可以隨意的做新的事情了,但是一旦需要添加新類型的時候,你需要改動很多東西,首先是Visitor的接口,其實(shí)是讓所有的邏輯都支持新類型,這樣你就不能僅僅通過添加新代碼來擴(kuò)展新類型了。所以這就是對邏輯擴(kuò)展開放,而對類型擴(kuò)展封閉了。
所以第一個問題就是:能不能做成類型擴(kuò)展也開放,邏輯擴(kuò)展也開放呢?在回答這個問題之前,我們先來看下一個問題。我們要對兩個Shape進(jìn)行求交,看看他們是不是有重疊在一起的部分。但是每一個具體的Shape,譬如Circle啊Rectangle啊,定義都是不一樣的,沒辦法有通用的處理辦法,所以我們只能寫3個函數(shù)了(RR, CC, CR)。如果有3各類型,那么我們就需要6個函數(shù)。如果有4個類型,那我們就需要有10個函數(shù)——才能處理所有情況。公式倒是可以一下子看出來,函數(shù)數(shù)量就等于1+2+ … +n,n等于類型的數(shù)量。
這看起來好像是一個類型擴(kuò)展開放的問題是吧,但是實(shí)際上他只能用邏輯擴(kuò)展的方法來做。為什么呢?你看我們的一個visitor其實(shí)很像是我們對一個一個的具體類型都試一下看看shape是不是這個類型,從而做出正確的處理。不過這跟我們直接用if地方法相比有兩個優(yōu)點(diǎn):1、快;2、編譯器替你查錯有保證。
那實(shí)際上應(yīng)該怎么做呢?想想,我們這里有兩次“if type”。第一次針對第一個參數(shù),第二次針對第二個參數(shù)。所以我們一共需要n+1=3個visitor。寫的方法倒是不復(fù)雜,首先我們得準(zhǔn)備好RR,CC,CR三個邏輯,然后用visitor去識別類型然后調(diào)用它們:
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; }我覺得你們現(xiàn)在心里的想法肯定是:“我屮艸芔茻?!编?#xff0c;這種事情在物理引擎里面是經(jīng)常要碰到的。然后當(dāng)你需要添加一個新的形狀的時候,呵呵呵呵呵呵呵呵。不過這也是沒辦法的,誰讓現(xiàn)在的要求運(yùn)行時性能的面向?qū)ο笳Z言都這么做呢?
當(dāng)然,如果在不要求性能的情況下,我們可以用ruby和它的mixin來做。至于說怎么辦,其實(shí)你們應(yīng)該發(fā)現(xiàn)了,添加一個Visitor和添加一個虛函數(shù)的感覺是差不多的。所以只要把Visitor當(dāng)成虛函數(shù)的樣子,讓Ruby給mixin一堆新的函數(shù)進(jìn)各種類型就好了。不過只有支持運(yùn)行時mixin的語言才能做到這一點(diǎn)。強(qiáng)類型語言我覺得是別想了。
Mixin地方法倒是很直接,我們只要把每一個Visitor里面的Visit函數(shù)都給加進(jìn)去就好了,大概感覺上就類似于:
class Shape { public:// Mixin的時候等價于給每一個具體的Shape類都添加下面三個虛函數(shù)的重寫 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); }這下子應(yīng)該看出來為什么我說這種方法只能用Visitor了吧,否則就要把所有類型都寫進(jìn)Shape,就會很奇怪了。如果這樣的邏輯一多,類型也有四五個的話,那每加一個邏輯就得添加一批虛函數(shù),Shape類很快就會被玩壞了。而代表邏輯的Visitor是可以放在不同的地方的,互相之間是隔離的,維護(hù)起來就會比較容易。
那現(xiàn)在我們就要有第二個問題了:在擁有兩個“this”的情況下,我們要如何做才能把邏輯做成類型擴(kuò)展也開放,邏輯擴(kuò)展也開放呢?然后參考我們的第一個問題:能不能做成類型擴(kuò)展也開放,邏輯擴(kuò)展也開放呢?你應(yīng)該心里有數(shù)了吧,答案當(dāng)然是——不能做。
這就是語言的極限了。面向?qū)ο蟛庞玫膕ingle dispatch的方法,能做到的東西是很有限的。情況稍微復(fù)雜那么一點(diǎn)點(diǎn)——就像上面對兩個形狀求交這種正常的問題——寫起來都這么難受。
那呼應(yīng)一下標(biāo)題,如果我們要設(shè)計一門語言,來支持上面這種multiple dispatch,那可以怎么修改語法呢?這里面分為兩種,第一種是像C++這樣運(yùn)行時load dll不增加符號的,第二種是像C#這樣運(yùn)行時load dll會增加符號的。對于前一種,其實(shí)我們可以簡單的修改一下語法:
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){ ... }然后修改一下編譯器,把這些東西翻譯成虛函數(shù)塞回原來的Shape類里面就行了。對于第二種嘛,其實(shí)就相當(dāng)于Intersect的根節(jié)點(diǎn)、Circle和CC寫在dll1,Rectangle和CR、RC、RR寫在dll2,然后dll1運(yùn)行時把dll2給動態(tài)地load了進(jìn)來,再之后調(diào)用Intersect的時候就好像“虛函數(shù)已經(jīng)進(jìn)去了”一樣。至于要怎么做,這個大家回去慢慢思考一下吧,啊哈哈哈。
from: http://www.cnblogs.com/geniusvczh/archive/2013/05/25/3098496.html
總結(jié)
以上是生活随笔為你收集整理的如何设计一门语言(五)——面向对象和消息发送的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何设计一门语言(四)——什么是坑(操作
- 下一篇: 如何设计一门语言(六)——excepti