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

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

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

Ответ 1

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

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

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

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

Ответ 2

Чтобы ответить на ваш вопрос, рассмотрите этот вопрос: что будет выводить нижеприведенный код при создании экземпляра объекта 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!?!
    }
}

Ответ заключается в том, что на самом NullReferenceException будет NullReferenceException, поскольку foo имеет значение NULL. Конструктор базы объектов вызывается перед его собственным конструктором. Имея virtual вызов в конструкторе объекта, вы представляете, что наследование объектов будет выполнять код до того, как они будут полностью инициализированы.

Ответ 3

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

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

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, что может привести к довольно неожиданному поведению.

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

Ответ 4

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

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

Ответ 5

В С# конструктор базового класса работает до конструктора производного класса, поэтому все поля экземпляров, которые производный класс может использовать в возможно переопределенном виртуальном члене, еще не инициализированы.

Обратите внимание, что это всего лишь предупреждение , чтобы вы обратили внимание и убедитесь, что все в порядке. Существуют фактические варианты использования для этого сценария, вам просто нужно документировать поведение виртуального участника, что он не может использовать любые поля экземпляров, объявленные в производном классе ниже, где его вызывает конструктор.

Ответ 6

Есть хорошо написанные ответы выше, почему вы не хотели бы этого делать. Здесь встречный пример, где, возможно, вы захотите это сделать (переведен на С# из Практический объектно-ориентированный дизайн в Ruby от Sandi Metz, p. 126).

Обратите внимание, что GetDependency() не касается каких-либо переменных экземпляра. Это было бы статическим, если бы статические методы могли быть виртуальными.

(Чтобы быть справедливым, возможно, более разумные способы сделать это через контейнеры инъекций зависимостей или инициализаторы объектов...)

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

Ответ 7

Да, обычно плохо вызвать виртуальный метод в конструкторе.

В этот момент объект еще не может быть полностью построен, а инварианты, ожидаемые с помощью методов, могут еще не выполняться.

Ответ 8

Ваш конструктор может (позже, в расширении вашего программного обеспечения) вызываться из конструктора подкласса, который переопределяет виртуальный метод. Теперь не реализация подкласса функции, а реализация базового класса будет вызвана. Поэтому нет смысла называть виртуальную функцию здесь.

Однако, если ваш проект удовлетворяет принципу замены Лискова, никакого вреда не будет. Наверное, поэтому он терпел - предупреждение, а не ошибка.

Ответ 9

Один важный аспект этого вопроса, который еще не затронул другие ответы, заключается в том, что для базового класса безопасно вызывать виртуальных членов внутри своего конструктора, если это то, что ожидают производные классы. В таких случаях разработчик производного класса несет ответственность за то, что все методы, которые выполняются до завершения строительства, будут вести себя так же разумно, насколько это возможно в сложившихся обстоятельствах. Например, в С++/CLI конструкторы завернуты в код, который вызовет Dispose на частично построенном объекте, если построение завершится с ошибкой. Вызов Dispose в таких случаях часто необходим для предотвращения утечек ресурсов, но методы Dispose должны быть подготовлены к возможности того, что объект, на котором они выполняются, возможно, не был полностью сконструирован.

Ответ 10

Потому что, пока конструктор не завершит выполнение, объект не будет полностью создан. Любые участники, на которые ссылается виртуальная функция, не могут быть инициализированы. В С++, когда вы находитесь в конструкторе, this относится только к статическому типу конструктора, в котором вы находитесь, а не к фактическому динамическому типу создаваемого объекта. Это означает, что вызов виртуальной функции может даже не идти туда, где вы ожидаете.

Ответ 11

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

Родительский класс ниже пытается установить значение для виртуального элемента в его конструкторе. И это вызовет предупреждение Re-sharper, давайте посмотрим на код:

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

Здесь класс child переопределяет родительское свойство. Если это свойство не было отмечено виртуальным, компилятор предупредит, что свойство скрывает свойство родительского класса и предлагает добавить ключевое слово 'new', если оно намеренно.

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

Наконец, влияние на использование, выход из приведенного ниже примера оставляет исходное значение, заданное конструктором родительского класса. И это то, что Re-sharper пытается предупредить вас, значения, установленные в конструкторе класса Parent, могут быть перезаписаны конструктором дочернего класса, который вызывается сразу после конструктора родительского класса.

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

Ответ 12

Остерегайтесь слепо следовать совету Resharper и сделать класс запечатанным! Если это модель в EF Code First, она удалит ключевое слово virtual, и это приведет к отключению ленивой загрузки его отношений.

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

Ответ 13

Один важный недостающий бит - это правильный способ решить эту проблему?

Как пояснил Грег, основная проблема заключается в том, что конструктор базового класса будет ссылаться на виртуальный член до того, как будет создан производный класс.

Следующий код, взятый из руководства по дизайну конструктора MSDN, демонстрирует эту проблему.

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

Когда создается новый экземпляр DerivedFromBad, конструктор базового класса вызывает DisplayState и показывает BadBaseClass потому что это поле еще не было обновлено производным конструктором.

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

Улучшенная реализация удаляет виртуальный метод из конструктора базового класса и использует метод Initialize. Создание нового экземпляра DerivedFromBetter отображает ожидаемый "DerivedFromBetter",

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

Ответ 14

В этом конкретном случае существует разница между С++ и С#. В С++ объект не инициализируется, и поэтому небезопасно вызывать вирутальную функцию внутри конструктора. В С# при создании объекта класса все его члены инициализируются нулем. В конструкторе можно вызвать виртуальную функцию, но если вы можете получить доступ к тем членам, которые все еще ноль. Если вам не нужен доступ к элементам, вполне безопасно вызвать виртуальную функцию на С#.

Ответ 15

Просто чтобы добавить свои мысли. Если вы всегда инициализируете частное поле при его определении, эту проблему следует избегать. По крайней мере, ниже кода работает как шарм:

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

Ответ 16

Еще одна интересная вещь, которую я обнаружил, заключается в том, что ошибка ReSharper может быть "удовлетворена", делая что-то вроде ниже, которое мне неинтересно (однако, как упоминалось многими ранее, по-прежнему не рекомендуется вызывать виртуальные prop/методы в т е р.

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

}

Ответ 17

Я бы просто добавил метод Initialize() в базовый класс, а затем вызвал это из производных конструкторов. Этот метод вызовет любые виртуальные/абстрактные методы/свойства ПОСЛЕ всех конструкторов:)