C# 構(gòu)造函數(shù)如何調(diào)用虛方法
謎題
在C#中,用virtual關(guān)鍵字修飾的方法(屬性、事件)稱為虛方法(屬性、事件),表示該方法可以由派生類重寫(override)。虛方法是.NET中的重要概念,可以說在某種程度上,虛方法使得多態(tài)成為可能。
然而虛方法的使用卻存在著很大學(xué)問,如果濫用的話勢(shì)必對(duì)程序產(chǎn)生很大的負(fù)面影響。比如下面這個(gè)例子:
public class Puzzle
{
public Puzzle()
{
Name = "Virtual member call in constructor";
Solve();
}
public virtual string Name { get; set; }
public virtual void Solve()
{
}
}
如果您的Visual Studio沒有安裝ReSharper,那么上面的代碼不會(huì)有任何異常。但如果安裝了,在構(gòu)造函數(shù)內(nèi)部給Name賦值和調(diào)用Solve時(shí)就會(huì)在下面產(chǎn)生一個(gè)波浪線,即警告:virtual member call in constructor。

這是什么原因呢?我們?cè)跇?gòu)造函數(shù)中調(diào)用虛方法,礙著ReSharper什么事兒了?
其實(shí)這個(gè)警告就是提醒我們不要在非封閉類型的構(gòu)造函數(shù)內(nèi)調(diào)用虛方法或虛屬性。但為什么這樣做不合適呢?在解惑之前,我們先來了解兩個(gè)概念。
類型的初始化順序
我們先來看這樣一段代碼:
class Base
{
public Base()
{
Console.WriteLine("Base constructor");
}
}
class Derived : Base
{
public Derived()
{
Console.WriteLine("Derived constructor");
}
}
static class Program
{
static void Main()
{
new Derived();
Console.Read();
}
}
猜一猜它的輸出結(jié)果是什么?
你也許已經(jīng)猜到了,它的結(jié)果是:
Base constructor
Derived constructor
我們?cè)诔跏蓟粋€(gè)對(duì)象時(shí),總是會(huì)先執(zhí)行基類的構(gòu)造函數(shù),然后再執(zhí)行子類的構(gòu)造函數(shù)。
虛方法調(diào)用
我們?cè)賮砜匆欢未a:
class Base
{
public void M()
{
Console.WriteLine("Base.M");
}
public virtual void V()
{
Console.WriteLine("Base.V");
}
}
class Derived : Base
{
public new void M()
{
Console.WriteLine("Derived.M");
}
public override void V()
{
Console.WriteLine("Derived.V");
}
}
static class Program
{
static void Main()
{
var d = new Derived();
Base b = d;
b.M();
b.V();
d.M();
d.V();
Console.Read();
}
}
再來猜一猜輸出結(jié)果吧。
貌似應(yīng)該是:
Base.M
Base.V
Derived.M
Derived.V
但運(yùn)行一下會(huì)發(fā)現(xiàn),真正的結(jié)果是這樣的:
Base.M
Derived.V
Derived.M
Derived.V
這是為什么呢?
原來對(duì)于非虛方法調(diào)用,編譯器會(huì)進(jìn)行一些額外的“動(dòng)作”。比如找出所調(diào)用對(duì)象的實(shí)際類型,以訪問正確的方法表(調(diào)用b.V()的時(shí)候就會(huì)找到變量b的實(shí)際類型Derived,從而輸出Derived.V)。
解惑
現(xiàn)在回到我們最初的謎題,virtual member call in constructor。結(jié)合以上兩個(gè)知識(shí)點(diǎn),會(huì)有哪些發(fā)現(xiàn)?
我們稍微改造一下虛方法調(diào)用的那個(gè)例子。
class Foo
{
public Foo(string s)
{
Console.WriteLine(s);
}
public void Bar() { }
}
class Base
{
public Base()
{
V(); // Virtual member call in constructor
}
public virtual void V()
{
Console.WriteLine("Base.V");
}
}
class Derived : Base
{
private Foo foo;
public Derived()
{
foo = new Foo("foo in Derived");
}
public override void V()
{
Console.WriteLine("Derived.V");
foo.Bar(); // will throw NullReferenceException
}
}
在Base的構(gòu)造函數(shù)中調(diào)用虛方法V()時(shí),ReSharper會(huì)給出virtual member call in constructor的警告。這是因?yàn)閂可以在Base的任意子類中被改寫(override),而這種改寫,很有可能使得它依賴于自己的構(gòu)造函數(shù),如上例所示。而由于之前提到的類型初始化順序,在執(zhí)行Base b = new Derived();這樣的代碼時(shí),Base的構(gòu)造函數(shù)要早于Derived的構(gòu)造函數(shù)執(zhí)行,因此在執(zhí)行到foo.Bar()時(shí)foo還是個(gè)空引用。
明白了嗎?我們來簡(jiǎn)單總結(jié)一下。Virtual member call in constructor的警告是因?yàn)椋瑢?duì)于Base b = new Derived();這樣的代碼:
- 基類構(gòu)造函數(shù)的執(zhí)行要早于子類構(gòu)造函數(shù)
- 基類構(gòu)造函數(shù)中對(duì)于虛方法的調(diào)用,實(shí)際調(diào)用的是子類中重寫的虛方法
因此,ReSharper會(huì)警告我們,這么做存在隱患。
我們能完全避免這么做嗎?很遺憾,答案是不能。比如如果項(xiàng)目中使用了NHibernate,框架本身要求ORM實(shí)體類中,所有與數(shù)據(jù)庫(kù)列具有對(duì)應(yīng)關(guān)系的屬性都必須為虛屬性。這是因?yàn)镹Hibernate為了實(shí)現(xiàn)延遲加載,會(huì)為每個(gè)實(shí)體類生成proxy,這些proxy需要重寫實(shí)體類中屬性的getter/setter。而有些時(shí)候,為了業(yè)務(wù)需要,我們不得不在實(shí)體類的構(gòu)造函數(shù)中對(duì)這些屬性進(jìn)行某些操作(比如初始化)。
我認(rèn)為這么做是技術(shù)選型所致的必然結(jié)果,是完全可以接受的。但我們要注意,在代碼中保證那些可能會(huì)被繼承的實(shí)體,在子類中重寫那些虛屬性時(shí),不要依賴于子類自身的構(gòu)造函數(shù)(這幾乎是可以保證的,因?yàn)榕c數(shù)據(jù)庫(kù)列映射的屬性,只能是最簡(jiǎn)單的getter/setter)。
以上就是C# 構(gòu)造函數(shù)如何調(diào)用虛方法的詳細(xì)內(nèi)容,更多關(guān)于C# 構(gòu)造函數(shù)內(nèi)調(diào)用虛方法的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C#/VB.NET實(shí)現(xiàn)HTML轉(zhuǎn)為XML的示例代碼
可擴(kuò)展標(biāo)記語言(XML)文件是一種標(biāo)準(zhǔn)的文本文件,它使用特定的標(biāo)記來描述文檔的結(jié)構(gòu)以及其他特性。本文將利用C#實(shí)現(xiàn)HTML轉(zhuǎn)為XML,需要的可以參考一下2022-06-06
C#使用SqlDataAdapter對(duì)象獲取數(shù)據(jù)的方法
這篇文章主要介紹了C#使用SqlDataAdapter對(duì)象獲取數(shù)據(jù)的方法,結(jié)合實(shí)例形式較為詳細(xì)的分析了SqlDataAdapter對(duì)象獲取數(shù)據(jù)具體步驟與相關(guān)使用技巧,需要的朋友可以參考下2016-02-02
C#實(shí)現(xiàn)漢字轉(zhuǎn)換為拼音縮寫的代碼
這篇文章主要為大家詳細(xì)介紹了C#實(shí)現(xiàn)漢字轉(zhuǎn)換為拼音縮寫的代碼,感興趣的小伙伴們可以參考一下2016-07-07
Unity創(chuàng)建平鋪網(wǎng)格地圖的方法
這篇文章主要為大家詳細(xì)介紹了Unity創(chuàng)建平鋪網(wǎng)格地圖的方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07
C# 制作PictureBox圓形頭像框并從數(shù)據(jù)庫(kù)中讀取頭像
C#提供的PictureBox控鍵默認(rèn)情況下是方形的非常大的影響美觀,怎么解決這一問題呢?下面小編給大家?guī)砹薈# 制作PictureBox圓形頭像框并從數(shù)據(jù)庫(kù)中讀取頭像的操作代碼,感興趣的朋友一起學(xué)習(xí)下吧2021-08-08
C#實(shí)現(xiàn)將PDF轉(zhuǎn)為Excel的方法詳解
通常,PDF格式的文檔能支持的編輯功能不如office文檔多,針對(duì)PDF文檔里面有表格數(shù)據(jù)的,如果想要編輯表格里面的數(shù)據(jù),可以將該P(yáng)DF文檔轉(zhuǎn)為Excel格式。本文將介紹如何利用C#實(shí)現(xiàn)PDF轉(zhuǎn)Excel,需要的可以參考一下2022-04-04
如何獲取C#中方法的執(zhí)行時(shí)間以及其代碼注入詳解
這篇文章主要給大家介紹了關(guān)于如何獲取C#中方法的執(zhí)行時(shí)間以及其代碼注入的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧2018-11-11

