簡體   English   中英

在 C# 中的基類構造函數中覆蓋和調用相同的方法

[英]Overriding and calling same method in Base class constructor in C#

我的煩惱是:在下面提供的代碼中,它應該顯示A然后B 但它顯示B然后B 為什么會這樣?

我的感覺是,在創建B對象時,首先執行A構造函數。 那樣的話, B的方法就不會命中了吧? 所以它應該是A.Display()並且應該導致A 此外,然后a.Display()應該返回B因為我們有覆蓋。

所以我希望A然后B 因為它不是重載而是覆蓋。 我知道這些東西的定義,我希望了解這種行為的原因以及它在內部如何運作,因為我不相信BB而是AB


代碼

class A
{
    public A()
    {
        this.Display();
    }
    public virtual void Display()
    {
        Console.WriteLine("A");
    }
}

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

class C
{
    static void Main()
    {
        A a = new B();
        a.Display();
        Console.WriteLine();
        Console.ReadLine();
    }
}

輸出

1) 在派生類中重寫Display方法會產生以下結果:

A a = new A(); // ---> AA
B a = new B(); // ---> BB // I expect AB.
A a = new B(); // ---> BB // I expect AB.

2) 在派生類的Display方法中使用NEW關鍵字會產生以下結果:

B a = new B(); // ---> AB // I Expect AA here.
A a = new B(); // ---> AA
A a = new A(); // ---> AA

3)更有趣的發現是:

當我在派生構造函數中使用base.Display()並覆蓋派生類中的基方法時,它給了我BAB

至少在這方面我看不到任何邏輯。 因為,它應該給BBB

我的感覺是,在創建 B 的對象時,首先執行 A 的構造函數。

正確。

那樣的話,B中的方法就不會被命中了吧?

這是不正確的。

在 C++ 中的類似代碼中,您是正確的。 在 C++ 中,有一條規則是在構建對象時構建虛函數調度表。 也就是說,當進入“A”構造函數時,vtable被“A”中的方法填充。 當控制權轉到“B”構造函數時,vtable 就會被 B 的方法填充。

在 C# 中不是這種情況。 在 C# 中,當對象從內存分配器中出來時,在執行任何一個 ctor 之前,vtable 被填充,並且在此之后它不會改變。 方法的 vtable 槽總是包含最派生的方法。

因此,像您在這里所做的那樣在 ctor 中調用虛擬方法是一個非常糟糕的主意 一個虛方法可以被調用,其中實現是在一個尚未運行 ctor 的類上! 因此,它可能取決於尚未初始化的狀態。

請注意,字段初始值設定項在所有 ctor 主體之前運行,因此幸運的是,對更派生類的覆蓋將始終在覆蓋類的字段初始值設定項之后運行。

這個故事的寓意是:不要那樣做。 永遠不要在ctor中調用虛擬方法。 在 C++ 中,您可能會得到與預期不同的方法,而在 C# 中,您可能會得到使用未初始化狀態的方法。 避免,避免,避免。

為什么我們不應該在ctor內部調用虛擬方法? 是因為我們總是在 vtable 中只得到(僅最新派生的)結果嗎?

是的。 讓我用一個例子來說明:

class Bravo
{
    public virtual void M() 
    {
        Console.WriteLine("Bravo!");
    }
    public Bravo()
    {
        M(); // Dangerous!
    }
}
class Delta : Bravo:
{
    DateTime creation;
    public override void M() 
    {
        Console.WriteLine(creation);
    }
    public Delta() 
    {
        creation = DateTime.Now;
    }
}

好的,所以這個程序的預期行為是當M在任何Delta上調用時,它會打印出創建實例的時間。 但是new Delta()上的事件順序是:

  • Bravo ctor 運行
  • Bravo ctor 稱之為this.M
  • M是虛擬的, this是運行時類型Delta所以Delta.M運行
  • Delta.M打印出未初始化的字段,該字段設置為默認時間,而不是當前時間。
  • M返回
  • Bravo ctor 回來了
  • Delta ctor 設置字段

現在你明白我說的覆蓋方法可能依賴於尚未初始化的狀態的意思了嗎? M任何其他用法中,這會很好,因為Delta ctor 已經完成了。 但是這里M甚至在Delta ctor 開始之前就被調用了!

您創建了一個對象B的實例。 它使用在類A上定義的構造函數的代碼,因為您沒有在B覆蓋它。 但實例仍然是B ,所以其他方法調用構造函數是在中定義的B ,而不是A 因此,您會看到在類B定義的Display()結果。

根據問題的更新進行更新

我會試着解釋你得到的“奇怪”的結果。

覆蓋時

B a = 新 B(); // ---> BB // 我希望是 AB。

A a = new B(); // ---> BB // 我希望是 AB。

這在上面有介紹。 當您覆蓋子類上的方法時,如果您使用的是子類的實例,則會使用此方法。 這是一個基本規則,即使用的方法由變量實例的類決定,而不是由用於聲明變量的類決定。

為方法使用new修飾符時(隱藏繼承的方法)

B a = 新 B(); // ---> AB // 我希望這里是 AA。

現在這里有兩種不同的行為:

  • 當使用構造函數時,它使用類A的構造函數。 由於繼承的方法隱藏在子類中,構造函數使用類A Display()方法,因此您會看到 A 打印出來。

  • 當您稍后直接調用Display() ,變量的實例為B 出於這個原因,它使用在類B上定義的方法打印 B。


初步聲明

我將從基本代碼開始,我已經對其進行了調整以在LINQPad 中運行(我也將其更改為Write而不是WriteLine因為無論如何我都不會在解釋中保留新行)。

class A
{
    public A()
    {
        this.Display();
    }

    public virtual void Display()
    {
        Console.Write("A"); //changed to Write
    }
}

class B :A
{
    public override void Display()
    {
        Console.Write("B"); //changed to Write
    }
}

static void Main()
{
    A a = new B();
    a.Display();
}

輸出是:

BB

在你最初的問題中,你說你期待:

AB

這里發生的事情(正如Szymon 試圖解釋的那樣)是您正在創建類型B的對象,而類B覆蓋了類A的方法Display 因此,每當您對該對象調用Display時,它都會是派生類 ( B ) 的方法,甚至來自A的構造函數。


我會檢查你提到的所有情況。 我想鼓勵你仔細閱讀它 此外,請保持開放的態度,因為這與某些其他語言中發生的情況不符。


1) 在派生類中重寫 Display 方法

這是您覆蓋該方法的情況,即:

public override void Display()
{
    Console.Write("B"); //changed to Write
}

當您覆蓋時,對於所有實際用途,將使用的方法是派生類的方法。 將覆蓋視為替換

案例一

A a = new A(); // ---> AA

我們沒問題。

案例2

B a = new B(); // ---> BB // I expect AB.

如上所述,在對象上調用Display將始終是派生類上的方法。 因此,對Display兩次調用都會產生B

案例3

A a = new B(); // ---> BB // I expect AB.

這是相同混淆的變體。 該對象顯然屬於B類型,即使您將它放在A類型的變量中。 請記住,在 C# 中,類型是對象的屬性而不是變量的屬性。 所以,結果和上面一樣。


注意:您仍然可以使用base.Display()來訪問被替換的方法。


2)在派生類的Display方法中使用NEW關鍵字

這是您隱藏方法的情況,即:

public new void Display()
{
    Console.Write("B"); //changed to Write
}

當您隱藏方法時,意味着原始方法仍然可用。 您可以將其視為不同的方法(恰好具有相同的名稱和簽名)。 也就是說:派生類不會 取代 覆蓋該方法。

因此,當您對對象進行(虛擬)調用時,在編譯時決定它將使用基類的方法......派生類的方法不被考慮(實際上,它的作用就像它不是虛擬調用一樣)。

可以這樣想:如果您使用基類的變量調用該方法......代碼不知道存在隱藏該方法的派生類,並且該特定調用可以使用這些對象之一執行。 相反,它將使用基類的方法,無論如何。

案例一

B a = new B(); // ---> AB // I Expect AA here.

您會看到,在編譯時構造函數中的調用被設置為使用基類的方法。 那個給A 但是由於變量的類型為B ,編譯器知道該方法在第二次調用時是隱藏的。

案例2

A a = new B(); // ---> AA

在這里,無論是在構造函數中還是在第二次調用中,它都不會使用新方法。 它不知道。

案例3

A a = new A(); // ---> AA

我認為這一點很清楚。


3) 使用 base.Display()

這是您執行此操作的代碼的變體:

public new void Display()
{
    base.Display();
    Console.Write("B"); //changed to Write
}

base.Display()將成為基類 ( A ) 中的方法,無論如何。


進一步閱讀

你說你想了解這在內部是如何運作的。

你可以通過閱讀微軟關於虛擬方法的 C# 規范來更深入

然后閱讀 Eric Lippert 的在 C# 中實現虛擬方法模式( 第 1 部分第 2 部分第 3 部分

您可能還感興趣:


網上對虛擬方法的其他解釋:

您可以通過命名類會混淆自己a在實例作為類B 如果您想調用虛擬方法,您可以使用base關鍵字。 下面的代碼寫AB

class A
{
    public A()
    {
        //this.Display();
    }
    public virtual void Display()
    {
        Console.WriteLine("A");
    }
}

class B : A
{
    public override void Display()
    {
        base.Display();
        Console.WriteLine("B");
    }
}

class C
{
    static void Main(string[] args)
    {
        A a = new B();
        a.Display();
        Console.WriteLine();
        Console.ReadLine();
    }
}

另請注意,您可以通過在開頭設置斷點然后逐行執行代碼 (F11) 來了解代碼顯示BB的原因。

我的理解是,在虛擬方法的情況下,父對象和子對象之間共享相同的方法槽。

如果是這樣,那么我認為當一個對象虛擬方法被調用時,編譯器會以某種方式用適當的方法地址更新方法槽,以便在 c# 中執行確切的方法。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM