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

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

Почему это было бы что-то не делать?

17 ответов


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

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

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

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


чтобы ответить на ваш вопрос, рассмотрите этот вопрос: что будет ниже кода распечатать, когда


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

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


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

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

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

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


есть хорошо написанные ответы выше, почему вы не хочу сделать это. Вот встречный пример, где, возможно, вы б хотите сделать это (переведено на C# из практический объектно-ориентированный дизайн в Ruby Сэнди Мец, р. 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 { }

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

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


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

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


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


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


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

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

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

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

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

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

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

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

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

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

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

следующий код, взятый из рекомендации по проектированию конструктора 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);
    }
}

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


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

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

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

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

}


Я бы просто добавил метод Initialize () в базовый класс, а затем вызвал его из производных конструкторов. Этот метод вызовет любые виртуальные / абстрактные методы / свойства после выполнения всех конструкторов:)