it-swarm.com.de

Virtueller Member-Aufruf in einem Konstruktor

Ich erhalte eine Warnung von ReSharper, dass mein Objektkonstruktor ein virtuelles Mitglied aufgerufen hat.

Warum sollte das etwas sein, was man nicht tun sollte?

1244
JasonS

Wenn ein in C # geschriebenes Objekt erstellt wird, werden die Initialisierer der Reihe nach von der am meisten abgeleiteten Klasse zur Basisklasse und die Konstruktoren der Reihe nach von der Basisklasse zur am meisten abgeleiteten Klasse ausgeführt ( siehe Eric Lipperts Blog für Details, warum das so ist ).

Auch in .NET ändern Objekte den Typ nicht, während sie erstellt werden, sondern beginnen als der am meisten abgeleitete Typ, wobei die Methodentabelle für den am meisten abgeleiteten Typ gilt. Dies bedeutet, dass virtuelle Methodenaufrufe immer auf dem am häufigsten abgeleiteten Typ ausgeführt werden.

Wenn Sie diese beiden Fakten kombinieren, bleibt das Problem bestehen, dass ein virtueller Methodenaufruf in einem Konstruktor, der nicht der am häufigsten abgeleitete Typ in seiner Vererbungshierarchie ist, für eine Klasse aufgerufen wird, deren Konstruktor dies nicht war run und daher möglicherweise nicht in einem geeigneten Zustand, um diese Methode aufrufen zu lassen.

Dieses Problem wird natürlich gemildert, wenn Sie Ihre Klasse als versiegelt markieren, um sicherzustellen, dass es sich um den am besten abgeleiteten Typ in der Vererbungshierarchie handelt. In diesem Fall ist es absolut sicher, die virtuelle Methode aufzurufen.

1116
Greg Beech

Stellen Sie sich zur Beantwortung Ihrer Frage die folgende Frage: Wie wird der folgende Code ausgedruckt, wenn das Objekt Child instanziiert wird?

class Parent
{
    public Parent()
    {
        DoSomething();
    }

    protected virtual void DoSomething() 
    {
    }
}

class Child : Parent
{
    private string foo;

    public Child() 
    { 
        foo = "HELLO"; 
    }

    protected override void DoSomething()
    {
        Console.WriteLine(foo.ToLower()); //NullReferenceException!?!
    }
}

Die Antwort ist, dass tatsächlich ein NullReferenceException geworfen wird, weil foo null ist. Der Basiskonstruktor eines Objekts wird vor seinem eigenen Konstruktor aufgerufen. Durch einen virtual -Aufruf im Konstruktor eines Objekts wird die Möglichkeit eingeführt, dass das Erben von Objekten Code ausführt, bevor diese vollständig initialisiert wurden.

593
Matt Howells

Die Regeln von C # unterscheiden sich stark von denen von Java und C++.

Wenn Sie sich im Konstruktor für ein Objekt in C # befinden, ist dieses Objekt in einer vollständig initialisierten (nur nicht "konstruierten") Form als vollständig abgeleiteter Typ vorhanden.

namespace Demo
{
    class A 
    {
      public A()
      {
        System.Console.WriteLine("This is a {0},", this.GetType());
      }
    }

    class B : A
    {      
    }

    // . . .

    B b = new B(); // Output: "This is a Demo.B"
}

Wenn Sie also eine virtuelle Funktion aus dem Konstruktor von A aufrufen, wird diese in eine Überschreibung in B aufgelöst, sofern eine bereitgestellt wird.

Selbst wenn Sie A und B absichtlich so einrichten und das Verhalten des Systems vollständig verstehen, kann es später zu einem Schock kommen. Nehmen wir an, Sie haben im Konstruktor von B virtuelle Funktionen aufgerufen, in dem Wissen, dass sie von B oder A entsprechend behandelt werden. Dann vergeht die Zeit und jemand anderes entscheidet, dass er C definieren und einige der dortigen virtuellen Funktionen überschreiben muss. Plötzlich ruft der Konstruktor von B Code in C auf, was zu einem ziemlich überraschenden Verhalten führen kann.

Es ist wahrscheinlich eine gute Idee, virtuelle Funktionen in Konstruktoren sowieso zu vermeiden, da die Regeln sind zwischen C #, C++ und Java so unterschiedlich sind. Ihre Programmierer wissen möglicherweise nicht, was Sie erwartet!

157
Lloyd

Die Gründe für die Warnung sind bereits beschrieben, aber wie würden Sie die Warnung beheben? Sie müssen entweder eine Klasse oder ein virtuelles Mitglied besiegeln.

  class B
  {
    protected virtual void Foo() { }
  }

  class A : B
  {
    public A()
    {
      Foo(); // warning here
    }
  }

Sie können Klasse A versiegeln:

  sealed class A : B
  {
    public A()
    {
      Foo(); // no warning
    }
  }

Oder Sie können Methode Foo versiegeln:

  class A : B
  {
    public A()
    {
      Foo(); // no warning
    }

    protected sealed override void Foo()
    {
      base.Foo();
    }
  }
84
Ilya Ryzhenkov

In C # wird der Konstruktor einer Basisklasse vor dem Konstruktor der abgeleiteten Klasse ausgeführt, sodass alle Instanzfelder, die eine abgeleitete Klasse in der möglicherweise überschriebenen virtuellen Klasse verwenden kann Mitglied sind noch nicht initialisiert.

Beachten Sie, dass dies nur ein Warnung ist, um Sie aufmerksam zu machen und sicherzustellen, dass alles in Ordnung ist. Es gibt tatsächliche Anwendungsfälle für dieses Szenario. Sie müssen lediglich das Verhalten dokumentieren des virtuellen Members angeben, dass es keine Instanzfelder verwenden kann, die in einer abgeleiteten Klasse deklariert sind, in der der Konstruktor sie aufruft.

17
Alex Lyman

Es gibt oben gut geschriebene Antworten, warum Sie das nicht wollen . Hier ist ein Gegenbeispiel, in dem Sie dies vielleicht tun möchten (übersetzt in C # von Praktisches objektorientiertes Entwerfen in Ruby von Sandi Metz, S. 126).

Beachten Sie, dass GetDependency() keine Instanzvariablen berührt. Es wäre statisch, wenn statische Methoden virtuell sein könnten.

(Um fair zu sein, es gibt wahrscheinlich intelligentere Möglichkeiten, dies über Abhängigkeitsinjektionscontainer oder Objektinitialisierer zu tun ...)

public class MyClass
{
    private IDependency _myDependency;

    public MyClass(IDependency someValue = null)
    {
        _myDependency = someValue ?? GetDependency();
    }

    // If this were static, it could not be overridden
    // as static methods cannot be virtual in C#.
    protected virtual IDependency GetDependency() 
    {
        return new SomeDependency();
    }
}

public class MySubClass : MyClass
{
    protected override IDependency GetDependency()
    {
        return new SomeOtherDependency();
    }
}

public interface IDependency  { }
public class SomeDependency : IDependency { }
public class SomeOtherDependency : IDependency { }
11
Josh Kodroff

Ja, es ist im Allgemeinen schlecht, eine virtuelle Methode im Konstruktor aufzurufen.

Zu diesem Zeitpunkt ist das Objekt möglicherweise noch nicht vollständig konstruiert, und die von den Methoden erwarteten Invarianten sind möglicherweise noch nicht gültig.

6
David Pierre

Ihr Konstruktor kann (später in einer Erweiterung Ihrer Software) vom Konstruktor einer Unterklasse aufgerufen werden, die die virtuelle Methode überschreibt. Nun wird nicht die Implementierung der Funktion der Unterklasse, sondern die Implementierung der Basisklasse aufgerufen. Es ist also nicht sinnvoll, hier eine virtuelle Funktion aufzurufen.

Wenn Ihr Entwurf jedoch das Liskov-Substitutionsprinzip erfüllt, wird kein Schaden angerichtet. Wahrscheinlich ist das der Grund, warum es toleriert wird - eine Warnung, kein Fehler.

5
xtofl

Ein wichtiger Aspekt dieser Frage, den andere noch nicht beantwortet haben, ist, dass es für eine Basisklasse sicher ist, virtuelle Member aus ihrem Konstruktor heraus aufzurufen wenn die abgeleiteten Klassen dies erwarten . In solchen Fällen ist der Designer der abgeleiteten Klasse dafür verantwortlich, dass alle Methoden, die vor Abschluss der Konstruktion ausgeführt werden, sich unter den gegebenen Umständen so vernünftig wie möglich verhalten. In C++/CLI werden Konstruktoren beispielsweise in Code eingeschlossen, der Dispose für das teilweise konstruierte Objekt aufruft, wenn die Konstruktion fehlschlägt. Der Aufruf von Dispose ist in solchen Fällen häufig erforderlich, um Ressourcenlecks zu verhindern. Es müssen jedoch Dispose - Methoden vorbereitet werden, damit das Objekt, auf dem sie ausgeführt werden, möglicherweise nicht vollständig erstellt wurde.

5
supercat

Denn bis der Konstruktor die Ausführung abgeschlossen hat, ist das Objekt nicht vollständig instanziiert. Von der virtuellen Funktion referenzierte Mitglieder dürfen nicht initialisiert werden. Wenn Sie sich in C++ in einem Konstruktor befinden, bezieht sich this nur auf den statischen Typ des Konstruktors, in dem Sie sich befinden, und nicht auf den tatsächlichen dynamischen Typ des Objekts, das erstellt wird. Dies bedeutet, dass der virtuelle Funktionsaufruf möglicherweise nicht an die von Ihnen erwartete Stelle gelangt.

5

Ein wichtiges fehlendes Bit ist, wie dieses Problem richtig behoben werden kann.

Wie Greg erklärte , besteht das Grundproblem darin, dass ein Basisklassenkonstruktor das virtuelle Member aufruft, bevor die abgeleitete Klasse erstellt wurde.

Der folgende Code aus Konstruktor-Entwurfsrichtlinien von MSDN veranschaulicht dieses Problem.

public class BadBaseClass
{
    protected string state;

    public BadBaseClass()
    {
        this.state = "BadBaseClass";
        this.DisplayState();
    }

    public virtual void DisplayState()
    {
    }
}

public class DerivedFromBad : BadBaseClass
{
    public DerivedFromBad()
    {
        this.state = "DerivedFromBad";
    }

    public override void DisplayState()
    {   
        Console.WriteLine(this.state);
    }
}

Wenn eine neue Instanz von DerivedFromBad erstellt wird, ruft der Basisklassenkonstruktor DisplayState auf und zeigt BadBaseClass an, da das Feld vom abgeleiteten Konstruktor noch nicht aktualisiert wurde.

public class Tester
{
    public static void Main()
    {
        var bad = new DerivedFromBad();
    }
}

Eine verbesserte Implementierung entfernt die virtuelle Methode aus dem Basisklassenkonstruktor und verwendet eine Initialize -Methode. Beim Erstellen einer neuen Instanz von DerivedFromBetter wird das erwartete "DerivedFromBetter" angezeigt.

public class BetterBaseClass
{
    protected string state;

    public BetterBaseClass()
    {
        this.state = "BetterBaseClass";
        this.Initialize();
    }

    public void Initialize()
    {
        this.DisplayState();
    }

    public virtual void DisplayState()
    {
    }
}

public class DerivedFromBetter : BetterBaseClass
{
    public DerivedFromBetter()
    {
        this.state = "DerivedFromBetter";
    }

    public override void DisplayState()
    {
        Console.WriteLine(this.state);
    }
}
3
Gustavo Mori

Die Warnung erinnert daran, dass virtuelle Mitglieder in abgeleiteten Klassen wahrscheinlich überschrieben werden. In diesem Fall wird alles, was die übergeordnete Klasse einem virtuellen Mitglied angetan hat, durch Überschreiben der untergeordneten Klasse rückgängig gemacht oder geändert. Schauen Sie sich zur Verdeutlichung das kleine Beispiel an

Die übergeordnete Klasse unten versucht, einen Wert für ein virtuelles Element in seinem Konstruktor festzulegen. Und dies löst eine Wiederholungswarnung aus, siehe Code:

public class Parent
{
    public virtual object Obj{get;set;}
    public Parent()
    {
        // Re-sharper warning: this is open to change from 
        // inheriting class overriding virtual member
        this.Obj = new Object();
    }
}

Die untergeordnete Klasse überschreibt hier die übergeordnete Eigenschaft. Wenn diese Eigenschaft nicht als virtuell markiert wurde, warnt der Compiler, dass die Eigenschaft die Eigenschaft in der übergeordneten Klasse verbirgt, und schlägt vor, dass Sie das Schlüsselwort 'new' hinzufügen, wenn dies beabsichtigt ist.

public class Child: Parent
{
    public Child():base()
    {
        this.Obj = "Something";
    }
    public override object Obj{get;set;}
}

Aufgrund der Auswirkungen auf die Verwendung wird in der Ausgabe des folgenden Beispiels der vom übergeordneten Klassenkonstruktor festgelegte Anfangswert verworfen. nd das ist es, was Re-sharper versucht, Sie zu warnen, Werte, die im Konstruktor für übergeordnete Klassen festgelegt wurden, können vom Konstruktor für untergeordnete Klassen überschrieben werden, der als rechts bezeichnet wird nach dem übergeordneten Klassenkonstruktor .

public class Program
{
    public static void Main()
    {
        var child = new Child();
        // anything that is done on parent virtual member is destroyed
        Console.WriteLine(child.Obj);
        // Output: "Something"
    }
} 
3
BTE

Hüten Sie sich davor, den Ratschlägen von Resharper blindlings zu folgen und die Klasse zu versiegeln! Wenn es sich um ein Modell in EF Code First handelt, wird das virtuelle Schlüsselwort entfernt, und das verzögerte Laden der Beziehungen wird deaktiviert.

    public **virtual** User User{ get; set; }
3
typhon04

Nur um meine Gedanken hinzuzufügen. Wenn Sie das private Feld beim Definieren immer initialisieren, sollte dieses Problem vermieden werden. Zumindest der folgende Code wirkt wie ein Zauber:

class Parent
{
    public Parent()
    {
        DoSomething();
    }
    protected virtual void DoSomething()
    {
    }
}

class Child : Parent
{
    private string foo = "HELLO";
    public Child() { /*Originally foo initialized here. Removed.*/ }
    protected override void DoSomething()
    {
        Console.WriteLine(foo.ToLower());
    }
}
1
Jim Ma

In diesem speziellen Fall gibt es einen Unterschied zwischen C++ und C #. In C++ ist das Objekt nicht initialisiert und daher ist es unsicher, eine virutale Funktion innerhalb eines Konstruktors aufzurufen. In C # werden beim Erstellen eines Klassenobjekts alle seine Elemente mit Null initialisiert. Es ist möglich, eine virtuelle Funktion im Konstruktor aufzurufen, aber wenn Sie auf Member zugreifen möchten, die noch Null sind. Wenn Sie nicht auf Mitglieder zugreifen müssen, ist es ziemlich sicher, eine virtuelle Funktion in C # aufzurufen.

1
Yuval Peled

Eine andere interessante Sache, die ich gefunden habe, ist, dass der ReSharper-Fehler "befriedigt" werden kann, indem man so etwas wie das Folgende macht, was für mich dumm ist (jedoch ist es, wie bereits von vielen erwähnt, immer noch keine gute Idee, virtuelle Requisiten/Methoden in CTOR aufzurufen.

public class ConfigManager
{

   public virtual int MyPropOne { get; private set; }
   public virtual string MyPropTwo { get; private set; }

   public ConfigManager()
   {
    Setup();
   }

   private void Setup()
   {
    MyPropOne = 1;
    MyPropTwo = "test";
   }

}

0
adityap