程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> C#的虛函數解析機制

C#的虛函數解析機制

編輯:關於C#

前言

這篇文章出自我個人對C#虛函數特性的研究和理解,未參考、查閱第三方資料,因此很可能存在謬誤之處。我在這裡只是為了將我的理解呈現給大家,也希望大家在看到我犯了錯誤後告訴我。

用詞約定

“方法的簽名”包括返回類型、方法名、參數列表,這三者共同標識了一個方法。

“聲明方法”,即指出該方法的簽名。“定義方法”,則是指定調用方法時執行的代碼。

“同名方法”是指方法的簽名相同的兩個方法。

“重寫”一個方法,意味著子類想繼承父類對方法的聲明,卻想重新定義該方法。

單獨使用“使用”一詞時,包括“顯式”或“隱式”兩種使用方式:前者是指在代碼中指明,後者是根據語句的上下文推斷。

某個類的方法,包括了在該類中定義的方法,以及由繼承得到的直接父類的方法。注意這條規則的遞歸性質。 

理論部分

在父類與子類裡,除了類之間的繼承鏈,還存在方法之間的繼承鏈。

C#裡,在一個類中聲明一個方法時,有四個和方法的繼承性有關的關鍵字:new、virtual、sealed、override。

virtual 表示允許子類的同名方法與其①建立繼承鏈。

override 表示其①與父類的同名方法之間建立了繼承鏈,並隱式使用 virtual 關鍵字。

new 表示其切斷了其①與父類的同名方法之間的繼承鏈。

sealed 表示將其①與父類的同名方法建立繼承鏈(注意這個就是 override 關鍵字的特性),並且不允許子類的同名方法與其建立繼承鏈。在使用 sealed 關鍵字時,必須同時顯式使用 override 關鍵字。

以及:

在定義方法時,若不使用以上關鍵字,方法就會具有new關鍵字的特性。對於這一點,如果父類中沒有同名方法,則沒有任何影響;如果父類中存在一個同名方法,編譯器會給出一個警告,詢問你是否是想隱藏父類的同名方法,並推薦你顯式地為其指定new關鍵字。

①其:指代正在進行聲明的方法。

依照上述的說明,在調用類上的某個方法時,可以為該方法構建出一個或多個“方法繼承鏈”。首先列出從子類②一直到父類③的類繼承鏈,並列出這些類對該方法的最初定義或重定義。然後從父類到子類,逐個檢查每個類對該方法的定義,按以下規則構造方法繼承鏈:

任何一個沒有使用 override 或 sealed 關鍵字的方法定義都將成為繼承鏈的開端;

如果該類在定義方法時使用了 virtual 關鍵字,則會被附加到繼承鏈中。

繼承鏈的結束取決於兩個因素:若子類中存在使用了 new 關鍵字的同名方法,則之前的繼承鏈立刻結束(該方法不會被添加到繼承鏈中);若子類中存在使用了 sealed 關鍵字的同名方法,則在將該方法添加到繼承鏈後,然後結束繼承鏈。

當你拿到一個子類②的實例,卻使用父類③的對象引用調用一個方法時(例如“A instanceRef = new C(); instanceRef.Foo1()”,這時類型A的引用就指向了類型C的對象),C#會先檢查該方法是否為一個虛方法(使用了 virtual 關鍵字):如果不是,則簡單地調用該方法的父類③版本即可;如果是,則沿著方法的繼承鏈向下尋找,找到位於繼承鏈底部的那個方法。

②子類:指該實例的實際類型。

③父類:指在調用方法時,使用的對象引用的類型;該類型必然是子類的父類型。

實踐部分

我定義了以下四個類:

類定義

public class A
{
  public virtual void Foo1()
  {
    Console.WriteLine("A.Foo1() was invoked.");
  }

  public void Foo2()
  {
    Console.WriteLine("A.Foo2() was invoked.");
  }
}

public class B : A
{
  public override void Foo1()
  {
    Console.WriteLine("B.Foo1() was invoked.");
  }

  public new virtual void Foo2()
  {
    Console.WriteLine("B.Foo2() was invoked");
  }
}

public class C : B
{
  public new void Foo1()
  {
    Console.WriteLine("C.Foo1() was invoked.");
  }
}

public class D : C
{
  public override sealed void Foo2()
  {
    Console.WriteLine("D.Foo2() was invoked.");
  }
}

當運行如下代碼時,會打印出什麼?

運行這些代碼

C aD = new D();
A aC = new C();

aD.Foo1();
aD.Foo2();
aC.Foo1();
aC.Foo2();

結果是:

打印出的結果C.Foo1() was invoked.
D.Foo2() was invoked.
B.Foo1() was invoked.
A.Foo2() was invoked.

例子很簡單,依照之前的規則,可以畫出如下一幅圖。圖中圓形的末端表示封閉、中斷繼承鏈;菱形的末端表示開放、允許構建繼承鏈;類描述中的等式,表示從該類型的對象引用調用對應方法(等號左邊的斜體)時,實際執行的代碼體是在何處(等號右邊的正常字體)定義的。

其實,為了確認這裡描述出來的方法的繼承鏈,甚至都不需要實地運行此代碼。將代碼放在Visual Studio裡,使用“重構”(Refactor)菜單中的“重命名”(Rename)修改方法名稱,待完成後就會發現在方法繼承鏈的中斷處,自動修改符號名稱的動作也中止了。

補充

對於 this 關鍵字,上述的規則也適用。只需要將 this 依照當前的代碼上下文翻譯為對應的類型引用,就可以依照之前敘述的方法確定最終調用的代碼了。例如在C中的Foo1方法裡假如有這麼一條語句:“this.Foo2()”。當在外部運行“D.Foo2()”時,就會就會解析到“C.Foo1()”,這時,C.Foo1()方法的內部在解析“this.Foo2()”時就會解析到D.Foo2()。

對於 base 關鍵字,則比較簡單,只是在基類的方法(這裡“基類的方法”一詞,請參見“用詞約定”的第6條。)中找到同名方法,然後調用,不存在解析虛函數的過程。

對於被委托對象包裝的方法指針,在調用委托時,仍會按照上述規則解析到正確的方法。

本文示例中使用了“Foo”開頭的方法名,而這個習慣借鑒自一些別的文章。這裡是有個典故還是怎麼?

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved