Frage Virtual Member-Aufruf in einem Konstruktor


Ich bekomme eine Warnung von ReSharper über einen Aufruf an ein virtuelles Mitglied von meinem Objektkonstruktor.

Warum sollte das etwas nicht sein?


1146
2017-09-23 07:11


Ursprung


Antworten:


Wenn ein in C # geschriebenes Objekt erstellt wird, werden die Initialisierer in der Reihenfolge von der am weitesten abgeleiteten Klasse zur Basisklasse ausgeführt, und dann werden die Konstruktoren in der Reihenfolge von der Basisklasse zur abgeleiteten Klasse ausgeführt (Details dazu finden Sie in Eric Lipperts Blog).

Auch in .NET-Objekten ändert sich der Typ nicht, wie sie konstruiert sind, sondern als der am weitesten abgeleitete Typ, wobei die Methodentabelle für den am weitesten abgeleiteten Typ ist. Dies bedeutet, dass virtuelle Methodenaufrufe immer mit dem am weitesten abgeleiteten Typ ausgeführt werden.

Wenn Sie diese beiden Fakten kombinieren, bleibt das Problem, dass wenn Sie einen virtuellen Methodenaufruf in einem Konstruktor machen und es nicht der am meisten abgeleitete Typ in seiner Vererbungshierarchie ist, dass er für eine Klasse aufgerufen wird, deren Konstruktor nicht existiert run, und daher möglicherweise nicht in einem geeigneten Zustand, um diese Methode aufgerufen zu haben.

Dieses Problem wird natürlich gemildert, wenn Sie Ihre Klasse als versiegelt markieren, um sicherzustellen, dass es der am weitesten abgeleitete Typ in der Vererbungshierarchie ist - in diesem Fall ist es vollkommen sicher, die virtuelle Methode aufzurufen.


1036
2017-09-23 07:21



Um Ihre Frage zu beantworten, überlegen Sie sich diese Frage: Was wird der untenstehende Code ausdrucken, wenn der Child Objekt wird instanziiert?

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());
    }
}

Die Antwort ist, dass tatsächlich ein NullReferenceException wird geworfen, weil foo ist Null. Der Basiskonstruktor eines Objekts wird vor seinem eigenen Konstruktor aufgerufen. Durch ein a virtual Wenn Sie den Konstruktor eines Objekts aufrufen, führen Sie die Möglichkeit ein, dass erbende Objekte Code ausführen, bevor sie vollständig initialisiert wurden.


478
2017-09-23 07:17



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

Wenn Sie sich im Konstruktor für ein Objekt in C # befinden, existiert dieses Objekt in einem vollständig initialisierten (nur nicht "konstruierten") Formular als vollständig abgeleiteten Typ.

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"
}

Das heißt, wenn Sie eine virtuelle Funktion vom Konstruktor von A aufrufen, wird sie in eine beliebige Überschreibung in B aufgelöst, falls eine solche Funktion vorhanden ist.

Selbst wenn Sie A und B absichtlich so einrichten, dass Sie das Verhalten des Systems vollständig verstehen, könnten Sie später einen Schock erleben. Nehmen wir an, Sie haben in Bs Konstruktor virtuelle Funktionen aufgerufen, die "wissen", dass sie von B oder A entsprechend gehandhabt werden. Dann vergeht die Zeit, und jemand anders entscheidet, dass sie C definieren und einige der virtuellen Funktionen dort überschreiben müssen. Plötzlich ruft Bs Konstruktor Code in C auf, was zu einem ziemlich überraschenden Verhalten führen könnte.

Es ist wahrscheinlich eine gute Idee, virtuelle Funktionen in Konstruktoren sowieso zu vermeiden, da die Regeln sind so verschieden zwischen C #, C ++ und Java. Ihre Programmierer wissen vielleicht nicht, was Sie erwarten können!


154
2017-09-23 07:36



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 versiegeln.

  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 die Methode Foo versiegeln:

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

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

77
2017-09-23 13:20



In C # wird der Konstruktor einer Basisklasse ausgeführt Vor der Konstruktor der abgeleiteten Klasse, so dass alle Instanzfelder, die eine abgeleitete Klasse möglicherweise in dem möglicherweise überschriebenen virtuellen Element verwendet, noch nicht initialisiert sind.

Beachten Sie, dass dies nur eine ist Warnung um dich aufmerksam zu machen und sicherzustellen, dass alles in Ordnung ist. Es gibt tatsächliche Anwendungsfälle für dieses Szenario, Sie müssen nur dokumentieren Sie das Verhalten des virtuellen Members, das keine Instanzfelder verwenden darf, die in einer abgeleiteten Klasse unterhalb des Konstruktors deklariert sind.


16
2017-09-23 07:21



Es gibt gut geschriebene Antworten oben für warum Sie würde nicht will das machen. Hier ist ein Gegenbeispiel, wo Sie vielleicht sind würde möchte das machen (übersetzt in C # von Praktisches Objektorientiertes Design in Ruby von Sandi Metz, p. 126).

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

(Um fair zu sein, gibt es wahrscheinlich klügere Wege, dies über Dependency-Injection-Container 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
2017-12-28 01:19



Ja, es ist normalerweise schlecht, die virtuelle Methode im Konstruktor aufzurufen.

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


5
2017-09-23 07:15



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 macht also keinen Sinn, hier eine virtuelle Funktion aufzurufen.

Wenn Ihr Entwurf jedoch dem Liskov-Substitutionsprinzip entspricht, wird kein Schaden verursacht. Wahrscheinlich wird es deshalb geduldet - eine Warnung, kein Fehler.


5
2017-09-23 07:25



Ein wichtiger Aspekt dieser Frage, den andere Antworten noch nicht angesprochen haben, ist, dass es für eine Basisklasse sicher ist, virtuelle Mitglieder innerhalb ihres Konstruktors aufzurufen Wenn dies von den abgeleiteten Klassen erwartet wird. In solchen Fällen ist der Designer der abgeleiteten Klasse dafür verantwortlich, dass alle Methoden, die vor der Fertigstellung der Konstruktion ausgeführt werden, sich so vernünftig verhalten, wie es unter den gegebenen Umständen möglich ist. In C ++ / CLI beispielsweise sind Konstruktoren in Code eingebettet, der aufgerufen wird Dispose auf dem teilweise konstruierten Objekt, wenn die Konstruktion fehlschlägt. Berufung Dispose In solchen Fällen ist es oft notwendig, Ressourcenlecks zu vermeiden, aber Dispose Methoden müssen auf die Möglichkeit vorbereitet sein, dass das Objekt, auf dem sie ausgeführt werden, möglicherweise nicht vollständig konstruiert wurde.


5
2017-10-25 20:33



Denn bis der Konstruktor die Ausführung abgeschlossen hat, wird das Objekt nicht vollständig instanziiert. Alle Mitglieder, auf die von der virtuellen Funktion verwiesen wird, werden möglicherweise nicht initialisiert. In C ++, wenn Sie in einem Konstruktor sind, this bezieht sich nur auf den statischen Typ des Konstruktors, in dem Sie sich befinden, und nicht auf den tatsächlichen dynamischen Typ des Objekts, das gerade erstellt wird. Dies bedeutet, dass der virtuelle Funktionsaufruf möglicherweise nicht an den erwarteten Ort gelangt.


4
2017-09-23 07:14