Виртуальный член вызова в конструкторе - PullRequest
1226 голосов
/ 23 сентября 2008

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

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

Ответы [ 17 ]

1102 голосов
/ 23 сентября 2008

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

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

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

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

569 голосов
/ 23 сентября 2008

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

157 голосов
/ 23 сентября 2008

Правила 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. Ваши программисты могут не знать, чего ожидать!

84 голосов
/ 23 сентября 2008

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

  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();
    }
  }
17 голосов
/ 23 сентября 2008

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

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

11 голосов
/ 28 декабря 2012

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

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

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

5 голосов
/ 23 сентября 2008

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

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

5 голосов
/ 26 октября 2012

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

5 голосов
/ 23 сентября 2008

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

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