基本概念
1.1 類與對象的初探
要我說,無論是面向過程的語言也好,面向對象的語言也罷,我首先要給他講的都是類和對象!--------“這個世界是由什麼組成的?”這個問題如果 讓不同的人來回答會得到不同的答案。如果是一個化學家,他也許會告訴你“還用問嘛?這個世界是由分子、原子、離子等等的化學物質組成的”。如果是一個畫家 呢?他也許會告訴你,“這個世界是由不同的顏色所組成的”。……呵呵,眾說紛紜吧!但如果讓一個分類學家來考慮問題就有趣的多了,他會告訴你“這個世界是 由不同類型的物與事所構成的”好!作為面向對象的程式設計師來說,我們要站在分類學家的角度去考慮問題!是的,這個世界是由動物、植物等組成的。動物又分為單 細胞動物、多細胞動物、哺乳動物等等,哺乳動物又分為人、大象、老虎……就這樣的分下去了!
站在抽象的角度,我們給“類”下個定義吧!我的意思是,站在抽象的角度,你回答我“什麼是人類?”首先讓我們來看看人類所具有的一些特徵,這個 特徵包括屬性(一些參數,數值)以及方法(一些行為,他能幹什麼!)。每個人都有身高、體重、年齡、血型等等一些屬性。人會勞動、人都會直立行走、人都會 用自己的頭腦去創造工具等等這些方法!人之所以能區別於其它類型的動物,是因為每個人都具有人這個群體的屬性與方法。“人類”只是一個抽象的概念,它僅僅 是一個概念,它是不存在的實體!但是所有具備“人類”這個群體的屬性與方法的對象都叫人!這個對象“人”是實際存在的實體!每個人都是人這個群體的一個對 象。老虎為什麼不是人?因為它不具備人這個群體的屬性與方法,老虎不會直立行走,不會使用工具等等!所以說老虎不是人!
由此可見-------類描述了一組有相同特性(屬性)和相同行為(方法)的對象。在程式中,類實際上就是數據類型!例如:整數,小數等等。整數也有 一組特性和行為。面向過程的語言與面相對象的語言的區別就在於,面向過程的語言不允許程式設計師自己定義數據類型,而只能使用程式中內置的數據類型!而為了模 擬真實世界,為了更好的解決問題,往往我們需要創建解決問題所必需的數據類型!面向對象編程為我們提供了解決方案。
1.2 內置數據類型與函式:
電腦程式在存儲數據時必須跟蹤3個基本屬性為:
1. 信息存儲在何處;
2. 存儲的值是多少;
3. 存儲的信息是什麼類型的;
讓我們來看看程式語言的內置數據類型都有哪些!(呵呵,這個不大好說,因為每門語言都有自己獨特的數據類型,但這畢竟是少數,比如在JAVA中有 byte類型的數據,而在C++中就沒有,希望你能舉一反三!)比如整數”int ”,浮點類型的數據”float”!字元串”String”,以及數組還有結構體等等。然而在寫程式的時候,根據需要我們會創建一個類型的變數或常量,例 如:由於我們需要創建一個整形的變數i為5,我們就可以這樣做,int i = 5;而根據需要我很有可能改變i的值,也就是從新給它賦值,比如讓它等與6,就可以在所需的地方改成i = 6;由此我們知道,在“值”上可以發生變化的量就叫變數。不會發生變化的量就叫做常量了,在C++中用count關鍵字來聲明,而在JAVA中則使用 final關鍵字來聲明。由於不同語言的聲明格式不一樣,這裡就不做一一介紹了,詳細的內容清查閱相關書籍!
在這裡我們主要討論一下函式,我們可以把函式想像成一個“實現某種特定功能的黑匣子”-------這個功能是由你來設定的,舉個例子來說:我問 你“2+3等於多少”?我相信你能很快的回答我等於5。讓我們來分析分析這句話包含什麼信息!首先我要把你的大腦想像成是一個黑匣子,我並不知道也沒有必 要知道你的大腦是如何工作的(也就是怎么運算的),我關心的只是我傳給你的是什麼信息?你對信息做了哪些處理? 以及你返回給我的是什麼信息?需要提醒你一下的是每個方法都會返回一個信息給調用者的,除了構造函式外(稍候我會作詳細的介紹)。我現需要把自己當作是 一名程式設計師,而你呢?當然就是計算機了!計算機可沒有人那么聰明,它只會按事先約好的特定的格式運行,我想讓它具有如上所述的功能,我就要先定義這個黑匣 子!首先我要告訴這個黑匣子會有兩個整數值給你(這就是所謂的參數,是程式設計師需要給黑匣子的信息),然後就要定義這個黑匣子內部實現這兩個整數相加的運算 (這就是黑匣子對數據所做的加工,根據需要,你可以做任何的加工。)。最後再標註它返回給我一個同樣是整型的數值(這是黑匣子返回給程式設計師的信息)。一個 函式就這樣定義完了,讓我們來看看書寫格式:
int addnum(int x,int y){
return x+y;
}
具體的含義是這樣的:
int /*返回值類型*/ addnum /*方法(黑匣子)名稱*/ (int x,int y/*傳入的參數*/){
return x+y; /*內部是想方法(實現相加運算,)並用return返回給調用者結果*/
}
首先請注意上明的“return”語句!return 關鍵字的含義是向調用者返回緊跟在它後面的信息!就像上面一樣,因為我問你,你才會回答我,如果我不問你,你就不用回答我的!在計算機中也一樣,定義好這 個函式在哪裡調用呢?我只能告訴你,哪裡需要就在哪裡調用!當然,你可以根據需要去更改參數、返回值以及內部實現,具體到如何定義如何調用你只好去參考相 關的資料了!在這裡我只是給你一個思想!
有時你會遇到這樣的問題,我讓你記住,我的年齡是20歲!從字面上理解,你並沒有給我返回信息!然而事實上,你確實給我返回了信息,信息的內容是“無信息,也就是無返回值類型void”。具體的程式如下:int myAge = 0;
int a=20;
void remAge(int a){
myAge=a;
}
具體的函式說明如下:
int myAge =0; //定義並初始化我的年齡為0;
int a=20; /*定義變數a等於20*/
void /*返回值類型為無返回值類型*/ remAge /*函式名稱*/(int a /*傳入的參數*/){
myAge=a; //內部實現方法,注意,沒有return返回!!!
}
關於函式的話題還有很多很多,這裡就不一一介紹了,我的目的是讓你知道函式是怎么一會事兒!為下面的討論作鋪墊!
1.3 指針以及引用:
指針及引用是在C++中有的,JAVA中沒有。JAVA中取消了對記憶體的操作,隨之而來的事也取消了操作符重載的操作。不過在稍候我還是會介紹一些操 作符重載的功能等。引用主要還是用在函式參數的傳遞上。所以我在這裡就不做過多的介紹了。他們很實用,有興趣的同學可以參閱C++相關書籍。
類和對
class cell is
var contents: Integer :=0;
method get(): Integer is
return self.contents;
end;
method set(n:Integer) is
self.contents := n;
end;
end;
一個類是用來描述所有屬於這個類的對象的共同結構的。這個cell類表示的對象擁有一個叫做contents的整數屬性(attribute),這個屬性被初始化成0。它還描述了兩個操作contents的方法。Get和set. 這兩個方法的內容都是很直觀的。Self變數表示這個對象自己。
對象的動態語義可以這樣理解:
一個對象在內部被表示為一個指向一組屬性的指針。任何對這個對象的操作都會經過這個指針操作對象的屬性和方法。而當對象被賦值或被當作參數傳遞的時候,所傳遞的只是指針,這樣一來,同一組屬性就可以被共享。
(注, 有些語言如C++,明確區分指向屬性組的指針和屬性組本身,而一些其它的語言則隱藏了這種區別)
對象可以用new從一個類中實例化。準確地說,new C分配了一組屬性,
並返回指向這組屬性的指針。這組屬性被賦予了初始值,並包括了類C所定義的方法的代碼。
下面我們來考慮類型。對一個new C所生成的對象,我們把它的類型記為InstanceTypeOf(c). 一個例子是:
var myCell: InstanceTypeOf(cell) := new cell;
這裡,通過引入InstanceTypeOf(cell),我們開始把class和type區分開來了。我們也可以把cell本身當作是類型,但接下來,你就會發現,那樣做會導致混淆的。
面向對象的程式設計(OOP)是結構化語言的自然延伸。OOP的先進編程方法,會產生一個清晰而又容易擴展及維護的程式。一旦您為您的程式建立了一個對象,您和其他的程式設計師可以在其他的程式中使用這個對象,完全不必重新編制繁複的代碼。對象的重複使用可以大大地節省開發時間,切實地提高您和其他人的工作效率。
方法解析
給出一個方法的調用o.m(……),一個由各個語言自己實現的叫做方法解析的過程負責找到正確的方法的代碼。(譯者按:是不是想起了vtable了?)。
直觀地看,方法的代碼可以被嵌入各個單個對象中,而且,對於許多面向對象語言,對屬性和方法的相似的語法,也確實給人這種印象。
不過,考慮到節省空間,很少有語言這樣實現。比較普遍的方法是,語言會生成許多method suite,而這些method suite可以被同一個類的對象們所共享。方法解析過程會延著對象內指向method suite的指針找到方法。
在考慮到繼承的情況,方法解析會更加複雜化。Method suite也許會被組成一個樹,而對一個方法的解析也許要查找一系列method suite. 而如果有多繼承的話,method suite甚至可能組成有向圖,或者是環。
方法解析可能發生在編譯時,也可能發生在運行時。
在一些語言中,方法到底是嵌入對象中的,還是存在於method suite中這種細節,對程式設計師是無關緊要的。因為,所有能區分這兩種模式的語言特性一般在基於類的面向對象語言中都不被支持。
比如說,方法並不能象屬性一樣從對象中取出來當作函式使用。方法也不能象屬性一樣在對象中被更新。(也就是說,你更新了一個對象的方法,而同一個類的其它對象的該方法保持不變。)
子類和繼承
子類和一般的類一樣,也是用來描述對象的結構的。但是,它是通過繼承其它類的結構來漸進式地實現這個目的。
父類的屬性會被隱式地複製到子類,子類也可以添加新的屬性。在一些語言中,子類甚至可以override父類的屬性(通過更改屬性的類型來實現)
父類中的方法可以被複製到子類,也可以被子類override.
一個子類的代碼的示例如下:
subclass reCell of cell is
var backup: Integer := 0;
override set(n: Integer) is
self.backup := self.contents;
super.set(n);
end;
method restore() is
self.contents := self.backup;
end;
end;
對有subclass的方法解析,根據語言是靜態類型還是動態類型而有所不同。
在靜態類型的語言(如C++,Java)里,父類,子類的method suite的拓撲結構在編譯時就已經確定,所以可以把父類的method suite里的方法合併到子類的method suite中去,方法解析時就不用再搜尋這個method suite的樹或圖了。(譯者按:C++的vtable就是這種方法)
而對於動態類型的語言,(也就是說,父子類的關係是在運行時決定的),method suite就無法合併了。所以,方法解析時,就要沿著這個動態生成的樹或有向圖搜尋直到找到合適的方法。而如果語言支持多繼承,這個搜尋就更複雜了。
類型信息
雖然subsumption並不改變對象的狀態,在一些語言裡(如Java),它甚至沒有任何運行時開銷。但是,它卻使我們丟掉了一些靜態的類型信息。
比如說,我們有一個類型InstanceTypeOf(Object),而Object類里沒有定義任何屬性和方法。我們又有一個類MyObject,它繼承自Object。那么當我們把MyObject的對象當作InstanceTypeOf(Object)類型來處理的時候,我們就得到了一個什麼東西也沒有的沒用的空對象。
當然,如果我們考慮一個不那么極端的情況,比如說,Object類裡面定義了一個方法f,而MyObject對方法f做了重載,那么,通過dynamic dispatch,我們還是可以間接地操作MyObject中的屬性和方法的。這也是面向對象設計和編程的典型方法。
從一個purist的角度看(譯者按,很不幸,我就是一個purist),dynamic dispatch是唯一你應該用來操作已經被subsumption忘掉的屬性和方法的東西。它優雅,安全,所有的榮耀都歸於dynamic dispatch!!! (譯者按,這句話是我說的)
不過,讓purist們失望的是,大部分語言還是提供了一些在運行時檢查對象類型,並從而操作被subsumption遺忘的屬性和方法。這種方法一般被叫做RTTI(Run Time Type Identification)。如C++中的dynamic_cast,或Java中的instanceof.
實事求是地說,RTTI是有用的。(譯者按,典型的存在就是合理的強盜邏輯,氣死我了!)。但因為一些理論上以及方法論上的原因,它被認為是破壞了面向對象的純潔性。
首先,它破壞了抽象,使一些本來不應該被使用的方法和屬性被不正確地使用。
其次,因為運行時類型的不確定性,它有效地把程式變得更脆弱。
第三點,也許是最重要的一點,它使你的程式缺乏擴展性。當你加入了一個新的類型時,你也許需要仔細閱讀你的dynamic_cast或instanceof的代碼,必要時改動它們,以保證這個新的類型的加入不會導致問題。而在這個過程中,編譯器將不會給你任何幫助。
很多人一提到RTTI,總是側重於它的運行時的開銷。但是,相比於方法論上的缺點,這點運行時的開銷真是無足輕重的。
而在purist的框架中(譯者按,吸一口氣,目視遠方,做深沉狀),新的子類的加入並不需要改動已有的代碼。
這是一個非常好的優點,尤其是當你並不擁有全部原始碼時。
總的來說,雖然RTTI (也叫type case)似乎是不可避免的一種特性,但因為它的方法論上的一些缺點,它必須被非常謹慎的使用。今天面向對象語言的類型系統中的很多東西就是產生於避免RTTI的各種努力。
比如有些複雜的類型系統中可以在參數和返回值上使用Self類型來避免RTTI. 這點我們後面會介紹到。
方法特化
在我們前面對subclass的討論中,我們採取了一種最簡單的override的規則,那就是,overriding的方法必須和overriden的方法有相同的signature.
但是,從類型安全的角度來說,這並不是必須的。套用我們前面討論的協變和反協變的知識,我們完全可以讓方法的返回類型協變,讓方法的參數類型反協變。
這樣,只要A <: A’,B’ <: B,下面的代碼就是合法的:
class c is
method m(x:A):B is … end;
method m1(x1:A1):B1 is … end;
end;
subclass c’ of c is
override m(x: A’):B’ is … end;
end;
我們暫時不允許屬性的協變。因為只有immutable的屬性才是協變的。允許對屬性的修改使得屬性都是invariant的。
特殊變數self這裡有一個有趣的屬性,它是一個參數,但它卻是協變的。這種特殊特性是由於self變數只能隱式地由編譯器傳入,所以避免了協變參數的不安全性。
還有一點有趣的地方是,上面的協變發生在override的時候,也就是,子類要改寫父類的方法的時候。但是,在繼承時,參數和返回類型的變化規律就又是另一回事了。
比如說,下面這個例子:
class c is
method m(x:A):B is … end;
method m1(x1:A1):B1 is … end;
end;
subclass c’ of c is
inherit m(x: A’):B’;
//這裡,方法m的代碼被繼承,子類只是重定義方法m的接口signature
end;
那么,這裡,參數就是協變的,而返回類型卻是反協變的了。
這裡,從另一個側面,我們看到了subtyping (通過override),和subclassing (通過inheritance) 的本質上的區別。