面試的時(shí)候遇到有這么一題:您在什么情況下會(huì )用到虛方法(虛函數)?它與接口有什么不同?
當不同的人面對這個(gè)問(wèn)題的時(shí)候應該是有不同的反應,因為每個(gè)人對以上提到的知識點(diǎn)的理解程度不同。絕對有人迷惑,也有人似乎明白,有人不屑的撇撇嘴。迷惑的人因為不知道面試官想問(wèn)什么,虛方法和接口在不同的討論范圍真是有點(diǎn)風(fēng)馬牛不相及;明白的人似乎知道有這么幾個(gè)東西,并侃侃而談:“由于Java不支持多繼承,而有可能某個(gè)類(lèi)或對象要使用分別在幾個(gè)類(lèi)或對象里面的方法或屬性,現有的單繼承機制就不能滿(mǎn)足要求。與繼承相比,接口有更高的靈活性,因為接口中沒(méi)有任何實(shí)現代碼。當一個(gè)類(lèi)實(shí)現了接口以后,該類(lèi)要實(shí)現接口里面所有的方法和屬性,并且接口里面的屬性在默認狀態(tài)下面都是public static,所有方法默認情況下是public.一個(gè)類(lèi)可以實(shí)現多個(gè)接口。”這時(shí)候面試官微笑著(zhù)點(diǎn)點(diǎn)頭,應聘者擦汗慶祝。
這就是所謂的答案嗎?我們往下看。
1、 什么是虛函數
C++書(shū)中介紹為了指明某個(gè)成員函數具有多態(tài)性,用關(guān)鍵字virtual來(lái)標志其為虛函數。傳統的多態(tài)實(shí)際上就是由虛函數(Virtual Function)利用虛表(Virtual Table)實(shí)現的也就是說(shuō),虛函數應為多態(tài)而生??吹教摵瘮?,我就想到了多態(tài)。
2、 什么時(shí)候用到虛函數
既然虛函數虛函數應為多態(tài)而生,那么簡(jiǎn)單的說(shuō)當我們在C++和C#中要想實(shí)現多態(tài)的方法之一就是使用到虛函數。復雜點(diǎn)說(shuō),那就是因為OOP的核心思想就是用程序語(yǔ)言描述客觀(guān)世界的對象,從而抽象出一個(gè)高內聚、低偶合,易于維護和擴展的模型。
但是在抽象過(guò)程中我們會(huì )發(fā)現很多事物的特征不清楚,或者很容易發(fā)生變動(dòng),怎么辦呢?比如飛禽都有飛這個(gè)動(dòng)作,但是對于不同的鳥(niǎo)類(lèi)它的飛的動(dòng)作方式是不同的,有的是滑行,有的要顫抖翅膀,雖然都是飛的行為,但具體實(shí)現卻是千差萬(wàn)別,在我們抽象的模型中不可能把一個(gè)個(gè)飛的動(dòng)作都考慮到,那么怎樣為以后留下好的擴展,怎樣來(lái)處理各個(gè)具體飛禽類(lèi)千差萬(wàn)別的飛行動(dòng)作呢?比如我現在又要實(shí)現一個(gè)類(lèi)“鶴”,它也有飛禽的特征(比如飛這個(gè)行為),如何使我可以只用簡(jiǎn)單地繼承 “飛禽”,而不去修改“飛禽”這個(gè)抽象模型現有的代碼,從而達到方便地擴展系統呢?
因此面向對象的概念中引入了虛函數來(lái)解決這類(lèi)問(wèn)題。
使用虛函數就是在父類(lèi)中把子類(lèi)中共有的但卻易于變化或者不清楚的特征抽取出來(lái),作為子類(lèi)需要去重新實(shí)現的操作(override)。而虛函數也是OOP中實(shí)現多態(tài)的關(guān)鍵之一。
下面引舉一個(gè)例子:(用C#描述)
class 飛禽
{
public string wing; // 翅膀
public string feather; // 羽毛
…… // 其它屬性和行為
public virtual bool Fly() // 利用關(guān)鍵字virtual來(lái)定義為虛函數,這是一個(gè)熱點(diǎn)
{
// 空下來(lái)讓子類(lèi)去實(shí)現
}
}
class 麻雀 : 飛禽 // 麻雀從飛禽繼承而來(lái)
{
…… // 定義麻雀自己特有的屬性和行為
public override bool Fly() // 利用關(guān)鍵字override重載飛翔動(dòng)作,實(shí)現自己的飛翔
{
…… // 實(shí)現麻雀飛的動(dòng)作
}
}
class 鶴 : 飛禽 // 鶴從飛禽繼承而來(lái)
{
…… // 定義鶴自己的特有的屬性和行為
public override bool Fly() // 利用關(guān)鍵字override重載實(shí)現鶴的飛翔
{
…… // 實(shí)現鶴飛的動(dòng)作
}
}
這樣我們只需要在抽象模型“飛禽”里定義Fly()這個(gè)行為,表示所有由此“飛禽”派生出去的子類(lèi)都會(huì )有Fly()這個(gè)行為,而至于Fly()到底具體是怎么實(shí)現的,那么就由具體的子類(lèi)去實(shí)現就好了,不會(huì )再影響“飛禽”這個(gè)抽象模型了。
比如現在我們要做一個(gè)飛禽射擊訓練的系統,我們就可以這樣來(lái)使用上面定義的類(lèi):
// 如何來(lái)使用虛函數,這里同時(shí)也是一個(gè)多態(tài)的例子.
// 定義一個(gè)射擊飛禽的方法
// 注意這里聲明傳入一個(gè)“飛禽”類(lèi)作為參數,而不是某個(gè)具體的“鳥(niǎo)類(lèi)”。好處就是以后不管再出現多少
// 種鳥(niǎo)類(lèi),只要是從飛禽繼承下來(lái)的,都照打不誤:)(多態(tài)的方式)
void ShootBird(飛禽 bird)
{
// 當鳥(niǎo)在飛就開(kāi)始射擊
if(bird.Fly())
{
…… // 射擊動(dòng)作
}
}
static void main()
{
/ /打麻雀
ShootBird(new 麻雀());
// 打鶴
ShootBird(new 鶴());
// 都是打鳥(niǎo)的過(guò)程,我只要實(shí)現了具體某個(gè)鳥(niǎo)類(lèi)(從“飛禽”派生而來(lái))的定義,就可以對它
// 進(jìn)行射擊,而不用去修改ShootBird函數和飛禽基類(lèi)
ShootBird(new 其它的飛禽());
}
虛函數從C#的程序編譯的角度來(lái)看,它和其它一般的函數有什么區別呢?一般函數在編譯時(shí)采用先期綁定(詳見(jiàn)我的文章:深刻剖析經(jīng)典面試題之四:OOP的三個(gè)核心本質(zhì)之多態(tài))編譯到了執行文件中,其相對地址在程序運行期間是不發(fā)生變化的。而虛函數在編譯期間采用的是后期綁定,它的相對地址是不確定的,它會(huì )根據運行時(shí)期對象實(shí)例來(lái)動(dòng)態(tài)判斷要調用的函數,其中那個(gè)聲明時(shí)定義的類(lèi)叫聲明類(lèi),那個(gè)執行時(shí)實(shí)例化的類(lèi)叫實(shí)例類(lèi)。
( 如:飛禽 bird = new 麻雀();
那么飛禽就是聲明類(lèi),麻雀是實(shí)例類(lèi)。 )
具體的檢查的流程如下:
(1)當調用一個(gè)對象的函數時(shí),系統會(huì )直接去檢查這個(gè)對象聲明定義的類(lèi),即聲明類(lèi),看所調用的函數是否為虛函數;
(2)如果不是虛函數,那么它就直接執行該函數。而如果有virtual關(guān)鍵字,也就是一個(gè)虛函數,那么這個(gè)時(shí)候它就不會(huì )立刻執行該函數了,而是轉去檢查對象的實(shí)例類(lèi)。
(3)在這個(gè)實(shí)例類(lèi)里,他會(huì )檢查這個(gè)實(shí)例類(lèi)的定義中是否重新實(shí)現了該虛函數(通過(guò)override關(guān)鍵字),如果是,則執行該實(shí)例類(lèi)中的這個(gè)重新實(shí)現的函數。而如果沒(méi)有的話(huà),系統就會(huì )不停地往上找實(shí)例類(lèi)的父類(lèi),并對父類(lèi)重復剛才在實(shí)例類(lèi)里的檢查,直到找到第一個(gè)重寫(xiě)了該虛函數的父類(lèi)為止,然后執行該父類(lèi)里重寫(xiě)后的函數。
知道這點(diǎn),就可以理解下面代碼的運行結果了:
class A
{
protected virtual Func() // 注意virtual,表明這是一個(gè)虛函數
{
Console.WriteLine("Func In A");
}
}
class B : A // 注意B是從A類(lèi)繼承,所以A是父類(lèi),B是子類(lèi)
{
protected override Func() // 注意override ,表明重新實(shí)現了虛函數
{
Console.WriteLine("Func In B");
}
}
class C : B // 注意C是從A類(lèi)繼承,所以B是父類(lèi),C是子類(lèi)
{
}
class D : A // 注意D是從A類(lèi)繼承,所以A是父類(lèi),D是子類(lèi)
{
protected new Func() // 注意new ,表明覆蓋父類(lèi)里的同名類(lèi),而不是重新實(shí)現
{
Console.WriteLine("Func In D");
}
}
static void main()
{
A a; // 定義一個(gè)a這個(gè)A類(lèi)的對象.這個(gè)A就是a的聲明類(lèi)
A b; // 定義一個(gè)b這個(gè)A類(lèi)的對象.這個(gè)A就是b的聲明類(lèi)
A c; // 定義一個(gè)c這個(gè)A類(lèi)的對象.這個(gè)A就是b的聲明類(lèi)
A d; // 定義一個(gè)d這個(gè)A類(lèi)的對象.這個(gè)A就是b的聲明類(lèi)
a = new A(); // 實(shí)例化a對象,A是a的實(shí)例類(lèi)
b = new B(); // 實(shí)例化b對象,B是b的實(shí)例類(lèi)
c = new C(); // 實(shí)例化b對象,C是b的實(shí)例類(lèi)
d = new D(); // 實(shí)例化b對象,D是b的實(shí)例類(lèi)
a.Func() ;
// 執行a.Func:1.先檢查聲明類(lèi)A 2.檢查到是虛方法 3.轉去檢查實(shí)例類(lèi)A,就為本身 4.執行實(shí)例類(lèi)A中的方法 5.輸出結果 Func In A
b.Func() ;
// 執行b.Func:1.先檢查聲明類(lèi)A 2.檢查到是虛方法 3.轉去檢查實(shí)例類(lèi)B,有重載的 4.執行實(shí)例類(lèi)B中的方法 5.輸出結果 Func In B
c.Func() ;
// 執行c.Func:1.先檢查聲明類(lèi)A 2.檢查到是虛方法 3.轉去檢查實(shí)例類(lèi)C,無(wú)重載的 4.轉去檢查類(lèi)C的父類(lèi)B,有重載的 5.執行父類(lèi)B中的Func方法 5.輸出結果 Func In B
d.Func();
// 執行d.Func:1.先檢查聲明類(lèi)A 2.檢查到是虛方法 3.轉去檢查實(shí)例類(lèi)D,無(wú)重載的(這個(gè)地方要注意了,雖然D里有實(shí)現Func(),但沒(méi)有使用override關(guān)鍵字,所以不會(huì )被認為是重載) 4.轉去檢查類(lèi)D的父類(lèi)A,就為本身 5.執行父類(lèi)A中的Func方法 5.輸出結果 Func In A
D d1 = new D()
d1.Func(); // 執行D類(lèi)里的Func(),輸出結果 Func In D}
3、 再論虛函數與接口
如果非得要把這兩者相提并論,還真能找出一絲的聯(lián)系。這一絲的聯(lián)系還得從多態(tài)的種類(lèi)說(shuō)起。多態(tài)的種類(lèi)有兩種,一為基類(lèi)繼承多態(tài)(Base Class Polymorphism),二為接口繼承多態(tài)(Interface Polymorphism)。虛函數的使用實(shí)現的是基類(lèi)繼承多態(tài),從設計模式的角度來(lái)說(shuō)基類(lèi)繼承體系描述的是Is-A的問(wèn)題。比如飛禽就是基類(lèi)(父類(lèi)),麻雀和鶴為子類(lèi)繼承了飛禽這個(gè)類(lèi)。麻雀和鶴“Is-A”飛禽。除了基類(lèi)繼承多態(tài),我們還有一種接口繼承多態(tài)。顧名思義,這種多態(tài)是通過(guò)繼承(更確切的說(shuō)是“實(shí)現”)接口而產(chǎn)生繼承體系的。從設計模式的角度來(lái)說(shuō)接口繼承體系描述的是Is-Like-A(或者叫Can-do)的問(wèn)題(詳見(jiàn)博客上另一篇文章《從設計模式看抽象類(lèi)與接口的區別》)。比如一個(gè)具有報警功能的門(mén),我們要實(shí)現“報警門(mén)”這么一個(gè)類(lèi),“報警門(mén)”“Is-A”門(mén),而不是一個(gè)報警器,只是“Is-Like-A”報警器而已。所以“報警門(mén)”的報警功能要通過(guò)實(shí)現報警器這個(gè)接口來(lái)實(shí)現報警功能。
4、 虛函數和接口哭了
它們哭著(zhù)說(shuō):“面試官,強扭的瓜不甜····”。
后記:老實(shí)說(shuō)我是懷著(zhù)一種忐忑不安的心情在總結這個(gè)問(wèn)題,或許我的總結不是最后的答案。但是我希望這些都化做上升的一種過(guò)程,歡迎大家的批駁與交流。有句成語(yǔ)講的好,叫“從善如流”。