4. Subsumption和Dynamic Dispatch (譯者按:呵呵,黔驢技窮,找不到合適的翻譯了)
從上述的幾個例子來看,似乎子類只是用來從父類借用一些定義,以避免重復。但是,當我們考慮到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方法,可以避免指針的變化,也更高效。我們會在另外的文章中闡述這種方法。)
5. 賽翁失馬 (關於類型信息)
雖然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. 這點我們後面會介紹到。
6.協變,反協變和壓根兒不變 (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了。
7.方法特化 (Method Specialization)
在我們前面對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