C# 構(gòu)造函數(shù)如何調(diào)用虛方法
謎題
在C#中,用virtual關(guān)鍵字修飾的方法(屬性、事件)稱為虛方法(屬性、事件),表示該方法可以由派生類重寫(override)。虛方法是.NET中的重要概念,可以說在某種程度上,虛方法使得多態(tài)成為可能。
然而虛方法的使用卻存在著很大學(xué)問,如果濫用的話勢必對程序產(chǎn)生很大的負(fù)面影響。比如下面這個例子:
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,那么上面的代碼不會有任何異常。但如果安裝了,在構(gòu)造函數(shù)內(nèi)部給Name賦值和調(diào)用Solve時就會在下面產(chǎn)生一個波浪線,即警告:virtual member call in constructor。
這是什么原因呢?我們在構(gòu)造函數(shù)中調(diào)用虛方法,礙著ReSharper什么事兒了?
其實這個警告就是提醒我們不要在非封閉類型的構(gòu)造函數(shù)內(nèi)調(diào)用虛方法或虛屬性。但為什么這樣做不合適呢?在解惑之前,我們先來了解兩個概念。
類型的初始化順序
我們先來看這樣一段代碼:
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
我們在初始化一個對象時,總是會先執(zhí)行基類的構(gòu)造函數(shù),然后再執(zhí)行子類的構(gòu)造函數(shù)。
虛方法調(diào)用
我們再來看一段代碼:
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
但運行一下會發(fā)現(xiàn),真正的結(jié)果是這樣的:
Base.M
Derived.V
Derived.M
Derived.V
這是為什么呢?
原來對于非虛方法調(diào)用,編譯器會進(jìn)行一些額外的“動作”。比如找出所調(diào)用對象的實際類型,以訪問正確的方法表(調(diào)用b.V()的時候就會找到變量b的實際類型Derived,從而輸出Derived.V)。
解惑
現(xiàn)在回到我們最初的謎題,virtual member call in constructor。結(jié)合以上兩個知識點,會有哪些發(fā)現(xiàn)?
我們稍微改造一下虛方法調(diào)用的那個例子。
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()時,ReSharper會給出virtual member call in constructor的警告。這是因為V可以在Base的任意子類中被改寫(override),而這種改寫,很有可能使得它依賴于自己的構(gòu)造函數(shù),如上例所示。而由于之前提到的類型初始化順序,在執(zhí)行Base b = new Derived();這樣的代碼時,Base的構(gòu)造函數(shù)要早于Derived的構(gòu)造函數(shù)執(zhí)行,因此在執(zhí)行到foo.Bar()時foo還是個空引用。
明白了嗎?我們來簡單總結(jié)一下。Virtual member call in constructor的警告是因為,對于Base b = new Derived();這樣的代碼:
- 基類構(gòu)造函數(shù)的執(zhí)行要早于子類構(gòu)造函數(shù)
- 基類構(gòu)造函數(shù)中對于虛方法的調(diào)用,實際調(diào)用的是子類中重寫的虛方法
因此,ReSharper會警告我們,這么做存在隱患。
我們能完全避免這么做嗎?很遺憾,答案是不能。比如如果項目中使用了NHibernate,框架本身要求ORM實體類中,所有與數(shù)據(jù)庫列具有對應(yīng)關(guān)系的屬性都必須為虛屬性。這是因為NHibernate為了實現(xiàn)延遲加載,會為每個實體類生成proxy,這些proxy需要重寫實體類中屬性的getter/setter。而有些時候,為了業(yè)務(wù)需要,我們不得不在實體類的構(gòu)造函數(shù)中對這些屬性進(jìn)行某些操作(比如初始化)。
我認(rèn)為這么做是技術(shù)選型所致的必然結(jié)果,是完全可以接受的。但我們要注意,在代碼中保證那些可能會被繼承的實體,在子類中重寫那些虛屬性時,不要依賴于子類自身的構(gòu)造函數(shù)(這幾乎是可以保證的,因為與數(shù)據(jù)庫列映射的屬性,只能是最簡單的getter/setter)。
以上就是C# 構(gòu)造函數(shù)如何調(diào)用虛方法的詳細(xì)內(nèi)容,更多關(guān)于C# 構(gòu)造函數(shù)內(nèi)調(diào)用虛方法的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
如何利用C#通過sql語句操作Sqlserver數(shù)據(jù)庫教程
ado.net提供了豐富的數(shù)據(jù)庫操作,下面這篇文章主要給大家介紹了關(guān)于如何利用C#通過sql語句操作Sqlserver數(shù)據(jù)庫教程的相關(guān)資料,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-10-10C#自定義導(dǎo)出數(shù)據(jù)到Excel的類實例
這篇文章主要介紹了C#自定義導(dǎo)出數(shù)據(jù)到Excel的類,實例分析了C#操作Excel的技巧,非常具有實用價值,需要的朋友可以參考下2015-03-03C#設(shè)計模式之Observer觀察者模式解決牛頓童鞋成績問題示例
這篇文章主要介紹了C#設(shè)計模式之Observer觀察者模式解決牛頓童鞋成績問題,簡單講述了觀察者模式的原理并結(jié)合具體實例形式分析了使用觀察者模式解決牛頓童鞋成績問題的具體步驟相關(guān)操作技巧,并附帶demo源碼供讀者下載參考,需要的朋友可以參考下2017-09-09淺談C#在網(wǎng)絡(luò)波動時防重復(fù)提交的方法
這篇文章主要介紹了淺談C#在網(wǎng)絡(luò)波動時防重復(fù)提交的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04