運行時動態綁定被調過程的“虛擬方法”(virtual method)機制是實現多態的關鍵技術。C++、Java和C#(按出生年月排列,上同,下同)作爲三種主流的支持對面向對象的程序設計語言,自然都提供了這種動態的方法綁定機制,在這個問題上三兄弟沒有誰是含糊的。但當“虛擬”(virtual)和“私有”(private)碰在一起時,這幾種語言在處理上卻有所不同,本文的故事就是從一段小程序說起的:
我們來看下面這段短小的C++程序
程序很簡單,我們在基類“Base”中,通過公有方法f調用了虛擬的私有方法g,而繼承自Base的Derived類中覆寫(override)了私有的方法g。
先不考慮其他,語法上有問題嗎?沒有;編譯能通過嗎?能。那運行結果是什麼呢?既然編譯能通過,那麼結果也就能猜到了,應該是:
Hi, MorningStar! I am g() of Derived.
沒錯,VC6.0,VC.net 2003和gcc都是這個結果。從結果來看,對私有方法g的調用在運行時綁定到了對象實際類型(Derived)的方法上,這符合虛擬函數的語義。
我們知道,在Java中,沒有“virtual”這麼個關鍵字,默認就是virtual,不可被覆寫的才需要加關鍵字“final”。Java中,對已被覆寫的方法如果想調用其父類的方法,則需通過特殊的關鍵字“super”調用。好了,不多說Java語法了,我們看下面一段類似的Java程序:
使用命令行
javac Derived.java
編譯,然後
Java Derived
運行,應該不會有什麼問題(偶是在Eclipse裏頭直接運行的:P)。結果爲:
Hi, MorningStar!, I am g() of Base.
雖然程序類似,但這兩中不同的語言產生的結果卻不一樣。
翻了翻C++98標準(ISO/IEC 14882)的相關章節,沒有找到任何關於此種情況的特殊描述,惟一涉及到虛擬函數訪問的第11.6節:Access to virtual functions,說的又不是這種情況。由此可見,C++語言中僅僅分別規定了public,protected和private三者的訪問控制問題和virtual方法的動態綁定問題,而對於他們的組合,沒加任何限制,換句話說,前後是兩種無關的機制,訪問控制專門負責訪問權限的問題,不管方法是不是虛擬;而虛擬只管延遲綁定的問題,不管公有、保護還是私有。於是,前面的C++程序輸出那個結果也就不足爲怪了。
然而,從面向對象的角度考慮,基類中私有的東西對外界、對繼承類都是不可見的,繼承類根本不應該知道基類中任何私有的東西,於是繼承、覆寫也應無從談起纔對,即使方法重名,那也應該僅僅看作一種巧合,Java就是這麼做的。
在Java中,private天生就是final的,我想Java的設計者至少有過上面的考慮:既然是私有,就不可見,見都見不到,何談覆寫?所以,在Java中,我們討論繼承和多態都是針對類的對外接口,包括puclic方法、protected方法和默認的friendly方法,而private是不被納入考量的,從某種意義上,Java中的private方法純粹是爲方法而方法,是一種組織代碼使之更清晰,更易維護的手段而已。
但如果僅從語法語義上考慮,而不是從面向對象的理論上考慮,或許C++更厚道一些——既然沒有限制,那麼private也可以virtual,既然可以virtual,那也就應該享受virtual級的待遇。而Java也沒什麼virtual不virtual,沒有任何語法痕跡便悶聲地規定:public/protected/friendly的是這樣,而private的是那樣。
最後,讓我們來看看時代的新寵,C#是怎麼做的呢?我們使用Visual Studio.net 2003新建Console應用程序,添加C#源文件,然後也可以寫類似的代碼:
編譯一下。完蛋了,編譯通不過!
.../Test.cs(14): “Test.Base.g()” : 虛擬成員或抽象成員不能是私有的
.../Test.cs(22): “Test.Derived.g()” : 虛擬成員或抽象成員不能是私有的
看來還是C#夠狠,語法上拒絕虛擬私有——那就沒啥輸出結果可以拷貝過來了。
C#從C++那裏繼承了“virtual”關鍵字,而這一點跟Java只是形式上的不同,C#中非virtual方法在很大程度上就相當於Java中的final方法。只是Java中final還用於指定類本身可否被繼承,C#使用另外一個關鍵字“sealed”做這件事。
相對來講,還是C++最靈活,它至少沒有限制這樣一種可能:基類允許派生類重新實現某個方法,卻不允許派生類直接調用基類的實現,即:我可以直接用你的(運行時綁定),但你不可以直接用我的——這一點跟protect/protected不一樣。而“重寫”與“調用”畢竟還是兩碼事,既然是兩碼事,分開考慮就不能說沒有必要。
總結:
C++從語法到語義上都支持可被覆寫的虛擬私有成員;
Java語法上看不出什麼痕跡,但語義上不存在可被覆蓋的私有成員;
C#從語法上直接拒絕私有方法成爲虛擬(可被覆寫)。