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

1232

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

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

  • 26
    @ m.edmondson, Серьезно .. ваш комментарий должен быть ответом здесь. Хотя объяснение Грега верное, я не понял его, пока не прочитал твой блог.
  • 6
    @ m.edmondson срок действия вашего домена истек.
Показать ещё 1 комментарий
Теги:
constructor
warnings
virtual-functions
resharper

17 ответов

1064
Лучший ответ

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

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

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

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

  • 133
    Грег, пожалуйста, скажи мне, почему у кого-то есть класс SEALED (который не может быть НАСЛЕДОВАННЫМ), если в нем есть члены VIRTUAL [то есть переопределяемые в классах DERIVED]?
  • 108
    Если вы хотите убедиться, что производный класс не может быть получен в дальнейшем, вполне приемлемо его запечатать.
Показать ещё 17 комментариев
532

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

  • 17
    Лучший ответ, чем отмеченный.
  • 6
    Это более понятно, чем приведенный выше ответ. Пример кода стоит тысячи слов.
Показать ещё 1 комментарий
162

Правила С# сильно отличаются от правил Java и С++.

Когда вы находитесь в конструкторе для некоторого объекта в С#, этот объект существует в полностью инициализированной (просто не "сконструированной" ) форме, как ее полностью производный тип.

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

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

  • 48
    Ответ Грега Бича, хотя, к сожалению, не проголосовал так высоко, как мой ответ, я считаю, что это лучший ответ. В нем, безусловно, есть еще несколько ценных, объяснительных деталей, которые я не потратил на это времени.
  • 3
    На самом деле правила в Java одинаковы.
Показать ещё 3 комментария
85

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

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

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

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

11

Есть хорошо написанные ответы выше, почему вы не хотели бы этого делать. Здесь встречный пример, где, возможно, вы захотите это сделать (переведен на С# из Практический объектно-ориентированный дизайн в 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 { }
  • 0
    Я хотел бы использовать фабричные методы для этого.
  • 0
    Мне бы хотелось, чтобы .NET Framework вместо того, чтобы включать в себя, как правило, бесполезный Finalize как элемент по умолчанию для Object , использовал этот слот vtable для ManageLifetime(LifetimeStatus) который вызывался бы, когда конструктор возвращается в код клиента, когда конструктор броски, или когда объект найден оставленным. Большинство сценариев, которые влекут за собой вызов виртуального метода из конструктора базового класса, лучше всего обрабатывать с использованием двухэтапного построения, но двухэтапное построение должно вести себя как деталь реализации, а не как требование, чтобы клиенты вызывали второй этап.
Показать ещё 2 комментария
5

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

5

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

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

5

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

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

4

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

3

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

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

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

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

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

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

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

Следующий код, взятый из руководства по дизайну конструктора 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);
    }
}
  • 4
    Я думаю, что конструктор DerivedFromBetter вызывает простоту конструктора BetterBaseClass. приведенный выше код должен быть эквивалентен общедоступному DerivedFromBetter (): base (), поэтому intialize будет вызываться дважды
  • 0
    Вы можете определить защищенный конструктор в классе BetterBaseClass, у которого есть дополнительный параметр bool initialize , который определяет, вызывается ли Initialize в базовом конструкторе. Производный конструктор затем вызовет base(false) чтобы избежать вызова Initialize дважды.
Показать ещё 2 комментария
1

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

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());
    }
}
  • 0
    Я почти никогда не делаю этого, потому что это затрудняет отладку, если вы хотите войти в конструктор.
1

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

  • 0
    Запрещено вызывать виртуальную функцию внутри конструктора в C ++.
  • 0
    Тот же аргумент верен для C ++: если вам не нужен доступ к членам, вам все равно, они не были инициализированы ...
Показать ещё 1 комментарий
0

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

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

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

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

}

  • 0
    Вы не должны найти обходной путь, но решить актуальную проблему.
  • 2
    Я согласен @alzaimar! Я пытаюсь оставить варианты для лиц, сталкивающихся с подобной проблемой, и тех, кто не хочет реализовывать решения, представленные выше, возможно, из-за некоторых ограничений. В связи с этим (как я уже упоминал в моем обходном пути выше), я также хочу подчеркнуть, что ReSharper, по возможности, должен иметь возможность пометить этот обходной путь как ошибку. Однако в настоящее время этого не происходит, что может привести к двум вещам - они забыли об этом сценарии или хотели сознательно исключить его из-за какого-либо действительного варианта использования, о котором сейчас нельзя думать.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню