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)類型的變量作為參數(shù)傳遞給一個參數(shù)類型為InstanceTypeOf(cell)的函數(shù)。
這種用法在類似Pascal的語言中是不合法的。而在面向?qū)ο蟮恼Z言中,依據(jù)以下的規(guī)則,它則是完全正確的用法。該規(guī)則通常被叫做subtype polimorphism, 即子類型多態(tài)(譯者按:其實subtyping應該是OO語言最區(qū)別于其它語言的地方了)
如果c’是c的子類,并且o’是c’的一個實例,那么o’也是c的一個實例。
更嚴格地說:
如果c’是c的子類,并且o’: InstanceTypeOf(c’),那么o’: InstanceTypeOf( c ).
仔細分析上面這條規(guī)則,我們可以在InstanceTypeOf的類型之間引入一個滿足自反和傳遞性的子類型關(guān)系, 我們用<:符號來表示。(譯者按:自反就是說, 對任何a, a 關(guān)系 a都成立,比如說,數(shù)學里的相等關(guān)系就是自反的。而傳遞性是說,如果a 關(guān)系 b, b 關(guān)系c, 就能推出a 關(guān)系c。 大于,小于等關(guān)系都是具備傳遞性的)
那么上面這條規(guī)則可以被拆成兩條規(guī)則:
1. 對任何a: A, 如果 A <: B, 那么 a: B.
2. InstanceTypeOf(c’) <: InstanceTypeOf(c) 當且僅當 c’是c的子類
第一條規(guī)則被叫做Subsumption. 它是判斷子類型(注意,是subtype, 不是subclass)的標準。
第二條規(guī)則可以叫做subclassing-is-subtyping (子類就是子類型,繞嘴吧?)
一般來說,繼承都是和subclassing相關(guān)的,所以這條規(guī)則也可以叫做:inheritance-is-subtyping (繼承就是子類型)
所有的面向?qū)ο笳Z言都支持subsumption (可以說,沒有subsumption, 就不成為面向?qū)ο?。
大部分的基于類的面向?qū)ο笳Z言也并不區(qū)分subclassing和subtyping. 但是,一些最新的面向?qū)ο笳Z言則采取了把subtyping和subclassing分開的方法。也就是說,A是B的子類,但A類的對象卻不可以當作B類的對象來使用。(譯者按:有點象C++里的私有繼承,但內(nèi)容比它豐富)
好吧,關(guān)于區(qū)分subclassing和subtyping, 我們后面會講到。
下面,讓我們重新回頭來看看這個procedure f. 在subsumption的情況下,下面這個代碼的動態(tài)語義是什么呢?
Procedure f(x: InstanceTypeOf(cell)) is
x.set(3);
end;
f(myReCell);
當myReCell被當作InstanceTypeOf(cell)的對象傳入f的時候,x.set(3)究竟是調(diào)用哪一個版本的set方法呢?是定義在cell中的那個set還是定義在reCell中的那個呢?
這時,我們有兩種選擇,
1. Static dispatch (按照編譯時的類型來決定)
2. Dynamic dispatch (按照對象運行時真正類型來決定)
(譯者按,熟悉C++的朋友們一定微笑了,這再簡單不過了。)
static dispatch沒什么可說的。
dynamic dispatch卻有一個有趣的屬性。那就是,subsumption一定不能影響對象的狀態(tài)。如果你在subsumption的時候,改變了這個對象的狀態(tài),比如象C++中的對象切片,那么動態(tài)解析的方法就可能會失敗。
好在,這個屬性無論對語義,還是對效率,都是很有好處的。
(譯者按,C++中的object slicing會把新的對象的vptr初始化成它自己類型的vtable指針, 所以不存在動態(tài)解析的問題。但實際上,對象切片根本不能叫做subsumption。
具體語言實現(xiàn)中,如C++, 雖然subsumption不會改變對象內(nèi)部的狀態(tài),但指針的值卻是可能會變化的。這也是一個讓人討厭的東西,但 C++ vtable的方案卻只能這樣。有一種變種的vtable方法,可以避免指針的變化,也更高效。我們會在另外的文章中闡述這種方法。)
賽翁失馬 (關(guān)于類型信息)
雖然subsumption并不改變對象的狀態(tài),在一些語言里(如Java), 它甚至沒有任何運行時開銷。但是,它卻使我們丟掉了一些靜態(tài)的類型信息。
比如說,我們有一個類型InstanceTypeOf(Object), 而Object類里沒有定義任何屬性和方法。我們又有一個類MyObject, 它繼承自O(shè)bject。那么當我們把MyObject的對象當作InstanceTypeOf(Object)類型來處理的時候,我們就得到了一個什么東西也沒有的沒用的空對象。
當然,如果我們考慮一個不那么極端的情況,比如說,Object類里面定義了一個方法f, 而MyObject對方法f做了重載,那么, 通過dynamic dispatch, 我們還是可以間接地操作MyObject中的屬性和方法的。這也是面向?qū)ο笤O(shè)計和編程的典型方法。
從一個purist的角度看(譯者按,很不幸,我就是一個purist), dynamic dispatch是你應該用來操作已經(jīng)被subsumption忘掉的屬性和方法的東西。它優(yōu)雅,安全,所有的榮耀都歸于dynamic dispatch!??!
不過,讓purist們失望的是,大部分語言還是提供了一些在運行時檢查對象類型,并從而操作被subsumption遺忘的屬性和方法。這種方法一般被叫做RTTI(Run Time Type Identification)。如C++中的dynamic_cast, 或Java中的instanceof.
實事求是地說,RTTI是有用的。(譯者按,典型的存在就是合理的強盜邏輯,氣死我了?。?。但因為一些理論上以及方法論上的原因,它被認為是破壞了面向?qū)ο蟮募儩嵭浴?BR> 首先,它破壞了抽象,使一些本來不應該被使用的方法和屬性被不正確地使用。
其次,因為運行時類型的不確定性,它有效地把程序變得更脆弱。
第三點,也許是最重要的一點,它使你的程序缺乏擴展性。當你加入了一個新的類型時,你也許需要仔細閱讀你的dynamic_cast或instanceof的代碼,必要時改動它們,以保證這個新的類型的加入不會導致問題。
很多人一提到RTTI, 總是側(cè)重于它的運行時的開銷。但是,相比于方法論上的缺點,這點運行時的開銷真是無足輕重的。
而在purist的框架中(譯者按,吸一口氣,目視遠方,做深沉狀),新的子類的加入并不需要改動已有的代碼。
這是一個非常好的優(yōu)點,尤其是當你并不擁有全部源代碼時。
總的來說,雖然RTTI (也叫type case)似乎是不可避免的一種特性,但因為它的方法論上的一些缺點,它必須被非常謹慎的使用。今天面向?qū)ο笳Z言的類型系統(tǒng)中的很多東西就是產(chǎn)生于避免RTTI的各種努力。
比如有些復雜的類型系統(tǒng)中可以在參數(shù)和返回值上使用Self類型來避免RTTI. 這點我們后面會介紹到。
協(xié)變,反協(xié)變和壓根兒不變 (Covarance, Contravariance and Invariance)
在下面的幾個小節(jié)里,我們來介紹一種避免RTTI的類型技術(shù)。在此之前,我們先來介紹“協(xié)變”,“反協(xié)變”和“壓根兒不變”的概念。
協(xié)變
首先,讓我們來看一個Pair類型: A*B
這個類型支持一個getA()的操作以返回這個Pair中的A元素。
給定一個A’ <: A, 那么,我們可以說A’*B <: A*B。
為什么呢?我們可以用Subsumption的屬性加以證明:
假設(shè)我們有一個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是協(xié)變的。
同理,我們也可以證明A*B對于B也是協(xié)變的。
正規(guī)一點說,Covariance是這樣定義的:
給定L(T), 這里,類型L是通過類型T組合成的。那么,
如果 T1 <: T2 能夠推出 L(T1) <: L(T2), 那么我們就說L是對T協(xié)變的。
反協(xié)變
請看一個函數(shù): A f(B b); (用functional language 的定義也許更簡潔, 即f: B->A)
那么,給定一個B’ <: B, 在B->A 和 B’->A之間有什么樣的subtype關(guān)系呢?
可以證明,B->A <: B’->A 。
基于篇幅,我們不再做推導。
所以,函數(shù)的參數(shù)類型是反協(xié)變的。
Contravariance的正規(guī)點的定義是這樣的:
給定L(T), 這里,類型L是通過類型T組合成的。那么,
如果 T1 <: T2 能夠推出 L(T2) <: L(T1), 那么我們就說L是對T反協(xié)變的。
同樣,可以證明,函數(shù)的返回類型是協(xié)變的。
壓根兒不變
那么我們再考慮函數(shù)g: A->A
這里,A既出現(xiàn)在參數(shù)的位置,又出現(xiàn)在返回的位置,可以證明,它既不是協(xié)變的,也不是反協(xié)變的。
對于這種既不是協(xié)變的,也不是反協(xié)變的情況,我們稱之為Invariance (譯者按:“壓根兒不變”是我編的,這么老土的翻譯,各位不必當真)
值得注意的是,對于第一個例子中的Pair類型,如果我們支持setA(A), 那么,Pair就變成Invariance了。
方法特化 (Method Specialization)
在我們前面對subclass的討論中,我們采取了一種最簡單的override的規(guī)則,那就是,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.
從上述的幾個例子來看,似乎子類只是用來從父類借用一些定義,以避免重復。但是,當我們考慮到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)類型的變量作為參數(shù)傳遞給一個參數(shù)類型為InstanceTypeOf(cell)的函數(shù)。
這種用法在類似Pascal的語言中是不合法的。而在面向?qū)ο蟮恼Z言中,依據(jù)以下的規(guī)則,它則是完全正確的用法。該規(guī)則通常被叫做subtype polimorphism, 即子類型多態(tài)(譯者按:其實subtyping應該是OO語言最區(qū)別于其它語言的地方了)
如果c’是c的子類,并且o’是c’的一個實例,那么o’也是c的一個實例。
更嚴格地說:
如果c’是c的子類,并且o’: InstanceTypeOf(c’),那么o’: InstanceTypeOf( c ).
仔細分析上面這條規(guī)則,我們可以在InstanceTypeOf的類型之間引入一個滿足自反和傳遞性的子類型關(guān)系, 我們用<:符號來表示。(譯者按:自反就是說, 對任何a, a 關(guān)系 a都成立,比如說,數(shù)學里的相等關(guān)系就是自反的。而傳遞性是說,如果a 關(guān)系 b, b 關(guān)系c, 就能推出a 關(guān)系c。 大于,小于等關(guān)系都是具備傳遞性的)
那么上面這條規(guī)則可以被拆成兩條規(guī)則:
1. 對任何a: A, 如果 A <: B, 那么 a: B.
2. InstanceTypeOf(c’) <: InstanceTypeOf(c) 當且僅當 c’是c的子類
第一條規(guī)則被叫做Subsumption. 它是判斷子類型(注意,是subtype, 不是subclass)的標準。
第二條規(guī)則可以叫做subclassing-is-subtyping (子類就是子類型,繞嘴吧?)
一般來說,繼承都是和subclassing相關(guān)的,所以這條規(guī)則也可以叫做:inheritance-is-subtyping (繼承就是子類型)
所有的面向?qū)ο笳Z言都支持subsumption (可以說,沒有subsumption, 就不成為面向?qū)ο?。
大部分的基于類的面向?qū)ο笳Z言也并不區(qū)分subclassing和subtyping. 但是,一些最新的面向?qū)ο笳Z言則采取了把subtyping和subclassing分開的方法。也就是說,A是B的子類,但A類的對象卻不可以當作B類的對象來使用。(譯者按:有點象C++里的私有繼承,但內(nèi)容比它豐富)
好吧,關(guān)于區(qū)分subclassing和subtyping, 我們后面會講到。
下面,讓我們重新回頭來看看這個procedure f. 在subsumption的情況下,下面這個代碼的動態(tài)語義是什么呢?
Procedure f(x: InstanceTypeOf(cell)) is
x.set(3);
end;
f(myReCell);
當myReCell被當作InstanceTypeOf(cell)的對象傳入f的時候,x.set(3)究竟是調(diào)用哪一個版本的set方法呢?是定義在cell中的那個set還是定義在reCell中的那個呢?
這時,我們有兩種選擇,
1. Static dispatch (按照編譯時的類型來決定)
2. Dynamic dispatch (按照對象運行時真正類型來決定)
(譯者按,熟悉C++的朋友們一定微笑了,這再簡單不過了。)
static dispatch沒什么可說的。
dynamic dispatch卻有一個有趣的屬性。那就是,subsumption一定不能影響對象的狀態(tài)。如果你在subsumption的時候,改變了這個對象的狀態(tài),比如象C++中的對象切片,那么動態(tài)解析的方法就可能會失敗。
好在,這個屬性無論對語義,還是對效率,都是很有好處的。
(譯者按,C++中的object slicing會把新的對象的vptr初始化成它自己類型的vtable指針, 所以不存在動態(tài)解析的問題。但實際上,對象切片根本不能叫做subsumption。
具體語言實現(xiàn)中,如C++, 雖然subsumption不會改變對象內(nèi)部的狀態(tài),但指針的值卻是可能會變化的。這也是一個讓人討厭的東西,但 C++ vtable的方案卻只能這樣。有一種變種的vtable方法,可以避免指針的變化,也更高效。我們會在另外的文章中闡述這種方法。)
賽翁失馬 (關(guān)于類型信息)
雖然subsumption并不改變對象的狀態(tài),在一些語言里(如Java), 它甚至沒有任何運行時開銷。但是,它卻使我們丟掉了一些靜態(tài)的類型信息。
比如說,我們有一個類型InstanceTypeOf(Object), 而Object類里沒有定義任何屬性和方法。我們又有一個類MyObject, 它繼承自O(shè)bject。那么當我們把MyObject的對象當作InstanceTypeOf(Object)類型來處理的時候,我們就得到了一個什么東西也沒有的沒用的空對象。
當然,如果我們考慮一個不那么極端的情況,比如說,Object類里面定義了一個方法f, 而MyObject對方法f做了重載,那么, 通過dynamic dispatch, 我們還是可以間接地操作MyObject中的屬性和方法的。這也是面向?qū)ο笤O(shè)計和編程的典型方法。
從一個purist的角度看(譯者按,很不幸,我就是一個purist), dynamic dispatch是你應該用來操作已經(jīng)被subsumption忘掉的屬性和方法的東西。它優(yōu)雅,安全,所有的榮耀都歸于dynamic dispatch!??!
不過,讓purist們失望的是,大部分語言還是提供了一些在運行時檢查對象類型,并從而操作被subsumption遺忘的屬性和方法。這種方法一般被叫做RTTI(Run Time Type Identification)。如C++中的dynamic_cast, 或Java中的instanceof.
實事求是地說,RTTI是有用的。(譯者按,典型的存在就是合理的強盜邏輯,氣死我了?。?。但因為一些理論上以及方法論上的原因,它被認為是破壞了面向?qū)ο蟮募儩嵭浴?BR> 首先,它破壞了抽象,使一些本來不應該被使用的方法和屬性被不正確地使用。
其次,因為運行時類型的不確定性,它有效地把程序變得更脆弱。
第三點,也許是最重要的一點,它使你的程序缺乏擴展性。當你加入了一個新的類型時,你也許需要仔細閱讀你的dynamic_cast或instanceof的代碼,必要時改動它們,以保證這個新的類型的加入不會導致問題。
很多人一提到RTTI, 總是側(cè)重于它的運行時的開銷。但是,相比于方法論上的缺點,這點運行時的開銷真是無足輕重的。
而在purist的框架中(譯者按,吸一口氣,目視遠方,做深沉狀),新的子類的加入并不需要改動已有的代碼。
這是一個非常好的優(yōu)點,尤其是當你并不擁有全部源代碼時。
總的來說,雖然RTTI (也叫type case)似乎是不可避免的一種特性,但因為它的方法論上的一些缺點,它必須被非常謹慎的使用。今天面向?qū)ο笳Z言的類型系統(tǒng)中的很多東西就是產(chǎn)生于避免RTTI的各種努力。
比如有些復雜的類型系統(tǒng)中可以在參數(shù)和返回值上使用Self類型來避免RTTI. 這點我們后面會介紹到。
協(xié)變,反協(xié)變和壓根兒不變 (Covarance, Contravariance and Invariance)
在下面的幾個小節(jié)里,我們來介紹一種避免RTTI的類型技術(shù)。在此之前,我們先來介紹“協(xié)變”,“反協(xié)變”和“壓根兒不變”的概念。
協(xié)變
首先,讓我們來看一個Pair類型: A*B
這個類型支持一個getA()的操作以返回這個Pair中的A元素。
給定一個A’ <: A, 那么,我們可以說A’*B <: A*B。
為什么呢?我們可以用Subsumption的屬性加以證明:
假設(shè)我們有一個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是協(xié)變的。
同理,我們也可以證明A*B對于B也是協(xié)變的。
正規(guī)一點說,Covariance是這樣定義的:
給定L(T), 這里,類型L是通過類型T組合成的。那么,
如果 T1 <: T2 能夠推出 L(T1) <: L(T2), 那么我們就說L是對T協(xié)變的。
反協(xié)變
請看一個函數(shù): A f(B b); (用functional language 的定義也許更簡潔, 即f: B->A)
那么,給定一個B’ <: B, 在B->A 和 B’->A之間有什么樣的subtype關(guān)系呢?
可以證明,B->A <: B’->A 。
基于篇幅,我們不再做推導。
所以,函數(shù)的參數(shù)類型是反協(xié)變的。
Contravariance的正規(guī)點的定義是這樣的:
給定L(T), 這里,類型L是通過類型T組合成的。那么,
如果 T1 <: T2 能夠推出 L(T2) <: L(T1), 那么我們就說L是對T反協(xié)變的。
同樣,可以證明,函數(shù)的返回類型是協(xié)變的。
壓根兒不變
那么我們再考慮函數(shù)g: A->A
這里,A既出現(xiàn)在參數(shù)的位置,又出現(xiàn)在返回的位置,可以證明,它既不是協(xié)變的,也不是反協(xié)變的。
對于這種既不是協(xié)變的,也不是反協(xié)變的情況,我們稱之為Invariance (譯者按:“壓根兒不變”是我編的,這么老土的翻譯,各位不必當真)
值得注意的是,對于第一個例子中的Pair類型,如果我們支持setA(A), 那么,Pair就變成Invariance了。
方法特化 (Method Specialization)
在我們前面對subclass的討論中,我們采取了一種最簡單的override的規(guī)則,那就是,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.