Вопрос

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

Почему этого нельзя делать?

Это было полезно?

Решение

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

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

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

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

Другие советы

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

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

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

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

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

Опломбировать класс А можно:

  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++, когда вы находитесь в конструкторе, this относится только к статическому типу конструктора, в котором вы находитесь, а не к фактическому динамическому типу создаваемого объекта.Это означает, что вызов виртуальной функции может даже не пойти туда, куда вы ожидаете.

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

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

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

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

Родительский класс ниже пытается установить значение виртуального члена в своем конструкторе.И это вызовет предупреждение 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();
    }
}

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

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

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

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 Code First, она удалит ключевое слово 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 можно «удовлетворить», выполнив что-то вроде приведенного ниже, что для меня глупо (однако, как упоминалось многими ранее, вызывать виртуальные опоры/методы в 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() в базовый класс, а затем вызывал его из производных конструкторов.Этот метод будет вызывать любые виртуальные/абстрактные методы/свойства ПОСЛЕ выполнения всех конструкторов :)

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top