c# - конструктор - вызов виртуальной функции в деструкторе




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

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

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

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

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


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


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

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


Для этого есть хорошо написанные ответы, почему вы не хотели бы этого делать. Вот контрпример, где, возможно, вы захотите это сделать (переведено на C # из Практического объектно-ориентированного дизайна в Ruby by Sandi Metz, стр. 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 #, строится, происходит то, что инициализаторы выполняются по порядку от самого производного класса к базовому классу, а затем конструкторы выполняются по порядку от базового класса до самого производного класса ( подробности см. В блоге Эрика Липперта почему это так ).

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

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

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


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


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

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

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


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

Родительский класс ниже пытается установить значение для виртуального участника в его конструкторе. И это вызовет предупреждение 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 здесь переопределяет родительское свойство. Если это свойство не было отмечено виртуальным, компилятор предупредил, что свойство скрывает свойство в родительском классе и предлагает добавить ключевое слово «новое», если оно является намеренным.

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

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

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

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


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





virtual-functions