謎題
在C#中,用virtual
關鍵字修飾的方法(屬性、事件)稱為虛方法(屬性、事件),表示該方法可以由派生類重寫(override)。虛方法是.NET中的重要概念,可以說在某種程度上,虛方法使得多態成為可能。
然而虛方法的使用卻存在着很大學問,如果濫用的話勢必對程序產生很大的負面影響。比如下面這個例子:
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,那么上面的代碼不會有任何異常。但如果安裝了,在構造函數內部給Name
賦值和調用Solve
時就會在下面產生一個波浪線,即警告:virtual member call in constructor。
這是什么原因呢?我們在構造函數中調用虛方法,礙着ReSharper什么事兒了?
其實這個警告就是提醒我們不要在非封閉類型的構造函數內調用虛方法或虛屬性。但為什么這樣做不合適呢?在解惑之前,我們先來了解兩個概念。
類型的初始化順序
我們先來看這樣一段代碼:
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();
}
}
猜一猜它的輸出結果是什么?
你也許已經猜到了,它的結果是:
Base constructor
Derived constructor
我們在初始化一個對象時,總是會先執行基類的構造函數,然后再執行子類的構造函數。
虛方法調用
我們再來看一段代碼:
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();
}
}
再來猜一猜輸出結果吧。
貌似應該是:
Base.M
Base.V
Derived.M
Derived.V
但運行一下會發現,真正的結果是這樣的:
Base.M
Derived.V
Derived.M
Derived.V
這是為什么呢?
原來對於非虛方法調用,編譯器會進行一些額外的“動作”。比如找出所調用對象的實際類型,以訪問正確的方法表(調用b.V()
的時候就會找到變量b
的實際類型Derived
,從而輸出Derived.V
)。
解惑
現在回到我們最初的謎題,virtual member call in constructor。結合以上兩個知識點,會有哪些發現?
我們稍微改造一下虛方法調用的那個例子。
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
的構造函數中調用虛方法V()
時,ReSharper會給出virtual member call in constructor的警告。這是因為V
可以在Base
的任意子類中被改寫(override),而這種改寫,很有可能使得它依賴於自己的構造函數,如上例所示。而由於之前提到的類型初始化順序,在執行Base b = new Derived();
這樣的代碼時,Base
的構造函數要早於Derived
的構造函數執行,因此在執行到foo.Bar()
時foo
還是個空引用。
明白了嗎?我們來簡單總結一下。Virtual member call in constructor的警告是因為,對於Base b = new Derived();
這樣的代碼:
- 基類構造函數的執行要早於子類構造函數
- 基類構造函數中對於虛方法的調用,實際調用的是子類中重寫的虛方法
因此,ReSharper會警告我們,這么做存在隱患。
我們能完全避免這么做嗎?很遺憾,答案是不能。比如如果項目中使用了NHibernate,框架本身要求ORM實體類中,所有與數據庫列具有對應關系的屬性都必須為虛屬性。這是因為NHibernate為了實現延遲加載,會為每個實體類生成proxy,這些proxy需要重寫實體類中屬性的getter/setter。而有些時候,為了業務需要,我們不得不在實體類的構造函數中對這些屬性進行某些操作(比如初始化)。
我認為這么做是技術選型所致的必然結果,是完全可以接受的。但我們要注意,在代碼中保證那些可能會被繼承的實體,在子類中重寫那些虛屬性時,不要依賴於子類自身的構造函數(這幾乎是可以保證的,因為與數據庫列映射的屬性,只能是最簡單的getter/setter)。