[C#解惑] #1 在建構函式內呼叫虛方法

林堯彬發表於2020-04-04

謎題

在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。

8142-20160126002554738-552318815.png

這是什麼原因呢?我們在建構函式中呼叫虛方法,礙著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();這樣的程式碼:

  1. 基類建構函式的執行要早於子類建構函式
  2. 基類建構函式中對於虛方法的呼叫,實際呼叫的是子類中重寫的虛方法

因此,ReSharper會警告我們,這麼做存在隱患。

我們能完全避免這麼做嗎?很遺憾,答案是不能。比如如果專案中使用了NHibernate,框架本身要求ORM實體類中,所有與資料庫列具有對應關係的屬性都必須為虛屬性。這是因為NHibernate為了實現延遲載入,會為每個實體類生成proxy,這些proxy需要重寫實體類中屬性的getter/setter。而有些時候,為了業務需要,我們不得不在實體類的建構函式中對這些屬性進行某些操作(比如初始化)。

我認為這麼做是技術選型所致的必然結果,是完全可以接受的。但我們要注意,在程式碼中保證那些可能會被繼承的實體,在子類中重寫那些虛屬性時,不要依賴於子類自身的建構函式(這幾乎是可以保證的,因為與資料庫列對映的屬性,只能是最簡單的getter/setter)。

轉載於:https://www.cnblogs.com/kirinboy/p/csharp-puzzles-1-virtual-member-call-in-constructor.html

相關文章