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


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

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

17 1166

17 ответов:

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

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

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

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

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

правила 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 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 { }

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

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

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

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

один важный аспект этого вопроса, который другие ответы еще не рассматривали, заключается в том, что базовый класс может безопасно вызывать виртуальные члены из своего конструктора если это то, что производные классы ожидают от него. В таких случаях разработчик производного класса отвечает за то, чтобы любые методы, которые выполняются до завершения построения, вели себя настолько разумно, насколько это возможно в данных обстоятельствах. Например, в 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"
    }
} 

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

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

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

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

следующий код, взятый из руководство по проектированию конструктора 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 может быть "удовлетворена", сделав что-то вроде ниже, что для меня глупо (однако, как упоминалось многими ранее, по-прежнему не рекомендуется называть virtual prop/methods в 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 () в базовый класс, а затем вызвал его из производных конструкторов. Этот метод будет вызывать любые виртуальные / абстрактные методы / свойства после выполнения всех конструкторов:)