面向對象語言

面向對象語言

面向對象語言(Object-Oriented Language)是一類以對象作為基本程式結構單位的程式設計語言,指用於描述的設計是以對象為核心,而對象是程式運行時刻的基本成分。語言中提供了類、繼承等成分。在靜態類型的語言(如C++,Java)里,父類,子類的method suite的拓撲結構在編譯時就已經確定,所以可以把父類的method suite里的方法合併到子類的method suite中去,方法解析時就不用再搜尋這個method suite的樹或圖了。從一個purist的角度看, dynamic dispatch是唯一應該用來操作已經被subsumption忘掉的屬性和方法的東西。

特殊語言

典型

一般認為,較典型的面向對象語言有:

simula 67,支持單繼承和一定含義的多態和部分動態綁定;

Smalltalk支持單繼承、多態和動態綁定;

EIFFEL,支持多繼承、多態和動態綁定;

C++,支持多繼承、多態和部分動態綁定。

Java,支持單繼承、多態和部分動態綁定。

五種語言涉及概念的含義雖然基本相同,但所用術語有別。

C#,也支持單繼承,與Java和C++等有很多類似之處……

基於類的

基於類的面向對象語言是面向對象世界裡的主流。它包括:

Simula,第一個面向對象語言

Smalltalk,第一個支持動態類型的語言

C++,它的大部分基於類的特性繼承自Simula.等等等等。

與基於類的語言相對應的是基於對象的面向對象語言。這裡“基於對象”的概念和把Visual Basic叫做基於對象的概念是不同的。這裡的“基於對象”是指一個只以對象為中心,沒有類的概念的語言,類似Python之類的語言。

類和對象

先看一個類的定義:

classcell 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本身當作是類型,但接下來,就會發現,那樣做會導致混淆的。

方法解析給出一個方法的調用o.m(……),一個由各個語言自己實現的叫做方法解析的過程負責找到正確的方法的代碼。(者按:是不是想起了vtable了?)。

直觀地看,方法的代碼可以被嵌入各個單個對象中,而且,對於許多面向對象語言,對屬性和方法的相似的語法,也確實給人這種印象。

不過,考慮到節省空間,很少有語言這樣實現。比較普遍的方法是,語言會生成許多method suite,而這些method suite可以被同一個類的對象們所共享。方法解析過程會延著對象內指向method suite的指針找到方法。

在考慮到繼承的情況,方法解析會更加複雜化。Method suite也許會被組成一個樹,而對一個方法的解析也許要查找一系列method suite. 而如果有多繼承的話,method suite甚至可能組成有向圖,或者是環。

方法解析可能發生在編譯時,也可能發生在運行時。

在一些語言中,方法到底是嵌入對象中的,還是存在於method suite中這種細節,對程式設計師是無關緊要的。因為,所有能區分這兩種模式的語言特性一般在基於類的面向對象語言中都不被支持。

比如說,方法並不能象屬性一樣從對象中取出來當作函式使用。方法也不能象屬性一樣在對象中被更新。(也就是說,更新了一個對象的方法,而同一個類的其它對象的該方法保持不變。)

子類和繼承子類和一般的類一樣,也是用來描述對象的結構的。但是,它是通過繼承其它類的結構來漸進式地實現這個目的。

父類的屬性會被隱式地複製到子類,子類也可以添加新的屬性。在一些語言中,子類甚至可以override父類的屬性(通過更改屬性的類型來實現)

父類中的方法可以被複製到子類,也可以被子類override.

一個子類的代碼的示例如下:

subclassreCell 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,事情就有些不同了。什麼是Subsumption呢?請看下面這個例子:

var myCell: InstanceTypeOf(cell) := new cell;

var myReCell: InstanceTypeOf(reCell) := new reCell;

procedure f(x: InstanceTypeOf(cell)) is … end;

再看下面這段代碼:

myCell := myReCell;

f(myReCell);

在這兩行代碼中,頭一行把一個InstanceTypeOf(reCell)類型的變數賦值給一個InstanceTypeOf(cell)的變數。而第二行則用InstanceTypeOf(reCell)類型的變數作為參數傳遞給一個參數類型為InstanceTypeOf(cell)的函式。

這種用法在類似Pascal的語言中是不合法的。而在面向對象的語言中,依據以下的規則,它則是完全正確的用法。該規則通常被叫做subtype polimorphism,即子類型多態(按:其實subtyping應該是OO語言最區別於其它語言的地方了)

如果c’是c的子類,並且o’是c’的一個實例,那么o’也是c的一個實例。

更嚴格地說:

如果c’是c的子類,並且o’: InstanceTypeOf(c’),那么o’: InstanceTypeOf( c ).

仔細分析上面這條規則,可以在InstanceTypeOf的類型之間引入一個滿足自反和傳遞性的子類型關係, 用<;:符號來表示。(按:自反就是說, 對任何a,a 關係 a都成立,比如說,數學裡的相等關係就是自反的。而傳遞性是說,如果a 關係 b,b 關係c,就能推出a 關係c。大於,小於等關係都是具備傳遞性的)

那么上面這條規則可以被拆成兩條規則:

1. 對任何a: A,如果 A <: B,那么 a: B.

2. InstanceTypeOf(c’) <: InstanceTypeOf(c) 若且唯若 c’是c的子類

第一條規則被叫做Subsumption. 它是判斷子類型(注意,是subtype,不是subclass)的唯一標準。

第二條規則可以叫做subclassing-is-subtyping (子類就是子類型,繞嘴吧?)

一般來說,繼承都是和subclassing相關的,所以這條規則也可以叫做:inheritance-is-subtyping (繼承就是子類型)

所有的面向對象語言都支持subsumption (可以說,沒有subsumption,就不成為面向對象)。

大部分的基於類的面向對象語言也並不區分subclassing和subtyping. 但是,一些最新的面向對象語言則採取了把subtyping和subclassing分開的方法。也就是說,A是B的子類,但A類的對象卻不可以當作B類的對象來使用。(按:有點象C++里的私有繼承,但內容比它豐富)

好吧,關於區分subclassing和subtyping,後面會講到。

下面,重新回頭來看看這個procedure f. 在subsumption的情況下,下面這個代碼的動態語義是什麼呢?

Procedure f(x: InstanceTypeOf(cell)) is

x.set(3);

end;

f(myReCell);

當myReCell被當作InstanceTypeOf(cell)的對象傳入f的時候,x.set(3)究竟是調用哪一個版本的set方法呢?是定義在cell中的那個set還是定義在reCell中的那個呢?

這時,有兩種選擇,

1. Static dispatch (按照編譯時的類型來決定)

2. Dynamic dispatch (按照對象運行時真正類型來決定)

(按,熟悉C++的朋友們一定微笑了,這再簡單不過了。)

static dispatch沒什麼可說的。

dynamic dispatch卻有一個有趣的屬性。那就是,subsumption一定不能影響對象的狀態。如果在subsumption的時候,改變了這個對象的狀態,比如象C++中的對象切片,那么動態解析的方法就可能會失敗。

好在,這個屬性無論對語義,還是對效率,都是很有好處的。

(按,C++中的object slicing會把新的對象的vptr初始化成它自己類型的vtable指針,所以不存在動態解析的問題。但實際上,對象切片根本不能叫做subsumption。

具體語言實現中,如C++,雖然subsumption不會改變對象內部的狀態,但指針的值卻是可能會變化的。這也是一個讓人討厭的東西,但 C++ vtable的方案卻只能這樣。有一種變種的vtable方法,可以避免指針的變化,也更高效。會在另外的文章中闡述這種方法。)

關於類型信息

雖然subsumption並不改變對象的狀態,在一些語言裡(如Java),它甚至沒有任何運行時開銷。但是,它卻使丟掉了一些靜態的類型信息。

比如說,有一個類型InstanceTypeOf(Object),而Object類里沒有定義任何屬性和方法。又有一個類MyObject,它繼承自Object。那么當把MyObject的對象當作InstanceTypeOf(Object)類型來處理的時候,就得到了一個什麼東西也沒有的沒用的空對象。

當然,如果考慮一個不那么極端的情況,比如說,Object類裡面定義了一個方法f,而MyObject對方法f做了重載,那么,通過dynamic dispatch,還是可以間接地操作MyObject中的屬性和方法的。這也是面向對象設計和編程的典型方法。

從一個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. 這點後面會介紹到。

協變,反協變和壓根兒不變 (Covarance,Contravariance and Invariance)

在下面的幾個小節里,來介紹一種避免RTTI的類型技術。在此之前,先來介紹“協變”,“反協變”和“壓根兒不變”的概念。

協變

首先,來看一個Pair類型:A*B

這個類型支持一個getA()的操作以返回這個Pair中的A元素。

給定一個A’ <: A,那么,可以說A’*B <: A*B。

為什麼呢?可以用Subsumption的屬性加以證明:

假設有一個A’*B類型的對象a’*b,這裡,a’:A’,b:B,a’*b <: A’*B

那么,因為,A’ <: A, 從subsumption,可以知道a’:A,getA():A 所以, a’*b<: A*B

這樣,就定義A*B這個類型對於A是協變的。

同理,也可以證明A*B對於B也是協變的。

正規一點說,Covariance是這樣定義的:

給定L(T),這裡,類型L是通過類型T組合成的。那么,

如果 T1 <: T2 能夠推出 L(T1) <: L(T2),那么就說L是對T協變的。

反協變

請看一個函式:A f(B b); (用functional language 的定義也許更簡潔, 即f: B->A)

那么,給定一個B’ <: B,在B->A 和 B’->A之間有什麼樣的subtype關係呢?

可以證明,B->A <: B’->A。

基於篇幅,不再做推導。

所以,函式的參數類型是反協變的。

Contravariance的正規點的定義是這樣的:

給定L(T),這裡,類型L是通過類型T組合成的。那么,

如果 T1 <: T2 能夠推出 L(T2) <: L(T1),那么就說L是對T反協變的。

同樣,可以證明,函式的返回類型是協變的。

壓根兒不變

那么再考慮函式g: A->A

這裡,A既出現在參數的位置,又出現在返回的位置,可以證明,它既不是協變的,也不是反協變的。

對於這種既不是協變的,也不是反協變的情況,稱之為Invariance

值得注意的是,對於第一個例子中的Pair類型,如果支持setA(A),那么,Pair就變成Invariance了。

方法特化 (Method Specialization)

在前面對subclass的討論中,採取了一種最簡單的override的規則,那就是,overriding的方法必須和overriden的方法有相同的signature.

但是,從類型安全的角度來說,這並不是必須的。

這樣,只要A <: A’,B’ <: B,下面的代碼就是合法的:

classc is

method m(x:A):B is … end;

method m1(x1:A1):B1 is … end;

end;

subclassc’ of c is.

相關詞條

相關搜尋

熱門詞條

聯絡我們