Вопрос: Виртуальный вызов участника в конструкторе


Я получаю предупреждение от ReSharper о вызове виртуального члена из моего конструктора объектов.

Зачем это делать?


1127


источник


Ответы:


Когда объект, написанный на C #, строится, происходит то, что инициализаторы выполняются по порядку от самого производного класса к базовому классу, а затем конструкторы выполняются по порядку от базового класса до самого производного класса ( см. блог Эрика Липперта, чтобы узнать, почему это ).

Также в .NET-объектах не изменяются типы по мере их построения, но начинаются как наиболее производные типы, причем таблица методов относится к наиболее производному типу. Это означает, что вызовы виртуальных методов всегда выполняются на самом производном типе.

Когда вы объединяете эти два факта, вы остаетесь с проблемой, что если вы вызываете вызов виртуального метода в конструкторе и не являетесь самым производным типом в своей иерархии наследования, то он будет вызван в класс, конструктор которого не был и поэтому не может находиться в подходящем состоянии, чтобы вызвать этот метод.

Разумеется, эта проблема смягчается, если вы помечаете свой класс как запечатанный, чтобы убедиться, что он является самым производным типом в иерархии наследования - и в этом случае совершенно безопасно вызвать виртуальный метод.


1020



Чтобы ответить на ваш вопрос, рассмотрите этот вопрос: что будет выводить нижеприведенный код, когда Childобъект создается?

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будут брошены, потому что fooнулевой. Базовый конструктор объекта вызывается перед его собственным конструктором , Имея virtualвызовите конструктор объекта, вы представляете, что наследование объектов будет выполнять код до того, как они будут полностью инициализированы.


468



Правила C # сильно отличаются от правил Java и C ++.

Когда вы находитесь в конструкторе для некоторого объекта в C #, этот объект существует в полностью инициализированной (просто не «построенной») форме, как ее полностью производный тип.

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

Это означает, что если вы вызываете виртуальную функцию из конструктора A, она будет разрешена для любого переопределения в B, если таковая предоставляется.

Даже если вы намеренно настроили A и B как это, полностью понимая поведение системы, вы можете быть в шоке позже. Скажем, вы назвали виртуальные функции в конструкторе B, «зная», что они будут обрабатываться B или A, если это необходимо. Затем проходит время, и кто-то другой решает, что им нужно определить C и переопределить некоторые из виртуальных функций там. Внезапный конструктор B заканчивает вызов кода на C, что может привести к совершенно неожиданному поведению.

Вероятно, в любом случае лучше избегать виртуальных функций в конструкторах, поскольку правила находятся поэтому разные между C #, C ++ и Java. Ваши программисты могут не знать, чего ожидать!


152



Причины предупреждения уже описаны, но как вы исправите предупреждение? Вы должны запечатать любого класса или виртуального участника.

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

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

Вы можете закрепить класс A:

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

Или вы можете запечатать метод Foo:

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

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

77



In C#, a base class' constructor runs before the derived class' constructor, so any instance fields that a derived class might use in the possibly-overridden virtual member are not initialized yet.

Do note that this is just a warning to make you pay attention and make sure it's all-right. There are actual use-cases for this scenario, you just have to document the behavior of the virtual member that it can not use any instance fields declared in a derived class below where the constructor calling it is.


16



There are well-written answers above for why you wouldn't want to do that. Here's a counter-example where perhaps you would want to do that (translated into C# from Practical Object-Oriented Design in Ruby by Sandi Metz, p. 126).

Note that GetDependency() isn't touching any instance variables. It would be static if static methods could be virtual.

(To be fair, there are probably smarter ways of doing this via dependency injection containers or object initializers...)

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

10



Yes, it's generally bad to call virtual method in the constructor.

At this point, the objet may not be fully constructed yet, and the invariants expected by methods may not hold yet.


5



Your constructor may (later, in an extension of your software) be called from the constructor of a subclass that overrides the virtual method. Now not the subclass's implementation of the function, but the implementation of the base class will be called. So it doesn't really make sense to call a virtual function here.

However, if your design satisfies the Liskov Substitution principle, no harm will be done. Probably that's why it's tolerated - a warning, not an error.


5



One important aspect of this question which other answers have not yet addressed is that it is safe for a base-class to call virtual members from within its constructor if that is what the derived classes are expecting it to do. In such cases, the designer of the derived class is responsible for ensuring that any methods which are run before construction is complete will behave as sensibly as they can under the circumstances. For example, in C++/CLI, constructors are wrapped in code which will call Dispose on the partially-constructed object if construction fails. Calling Dispose in such cases is often necessary to prevent resource leaks, but Dispose methods must be prepared for the possibility that the object upon which they are run may not have been fully constructed.


5



Because until the constructor has completed executing, the object is not fully instantiated. Any members referenced by the virtual function may not be initialised. In C++, when you are in a constructor, this only refers to the static type of the constructor you are in, and not the actual dynamic type of the object that is being created. This means that the virtual function call might not even go where you expect it to.


4



The warning is a reminder that virtual members are likely to be overridden on derived class. In that case whatever the parent class did to a virtual member will be undone or changed by overriding child class. Look at the small example blow for clarity

The parent class below attempts to set value to a virtual member on its constructor. And this will trigger Re-sharper warning, let see on 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();
    }
}

The child class here overrides the parent property. If this property was not marked virtual the compiler would warn that the property hides property on the parent class and suggest that you add 'new' keyword if it is intentional.

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

Finally the impact on use, the output of the example below abandons the initial value set by parent class constructor. And this is what Re-sharper attempts to to warn you, values set on the Parent class constructor are open to be overwritten by the child class constructor which is called right after the parent class constructor.

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



Beware of blindly following Resharper's advice and making the class sealed! If it's a model in EF Code First it will remove the virtual keyword and that would disable lazy loading of it's relationships.

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

3