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

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

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

23.09.2008 07:11:30
@ m.edmondson, серьезно .. ваш комментарий должен быть ответом здесь. Хотя объяснение Грега верное, я не понял его, пока не прочитал твой блог.
Rosdi Kasim 17.04.2013 06:50:50
Вы можете найти статью от @ m.edmondson здесь и сейчас: codeproject.com/Articles/802375/…
SpeziFish 21.12.2017 10:25:47
18 ОТВЕТОВ
РЕШЕНИЕ

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

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

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

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

1161
10.01.2020 13:26:43
Грег, пожалуйста, скажи мне, почему у кого-то есть класс SEALED (который не может быть НАСЛЕДОВАННЫМ), когда в нем есть члены VIRTUAL [то есть переопределяемые в классах DERIVED]?
Paul Pacurar 15.02.2011 15:27:06
Если вы хотите убедиться, что производный класс не может быть получен в дальнейшем, вполне приемлемо его запечатать.
Øyvind 15.02.2011 19:40:42
@Paul - Дело в том, что мы закончили извлечение виртуальных членов базового класса [es] и, таким образом, помечаем класс как полностью производный, как вы этого хотите.
ljs 25.02.2011 16:37:20
@Greg Если поведение виртуального метода не имеет ничего общего с переменными экземпляра, разве это не нормально? Кажется, может быть, мы должны быть в состоянии объявить, что виртуальный метод не будет изменять переменные экземпляра? (статический?) Например, если вы хотите иметь виртуальный метод, который можно переопределить для создания экземпляра более производного типа. Это кажется мне безопасным и не гарантирует этого предупреждения.
Dave Cousineau 7.10.2011 22:29:11
@PaulPacurar - если вы хотите вызвать виртуальный метод в самом производном классе, вы все равно получите предупреждение, хотя знаете, что это не вызовет проблемы. В этом случае вы можете поделиться своими знаниями с системой, запечатав этот класс.
Revolutionair 26.02.2013 09:16:02

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

5
23.09.2008 07:14:10

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

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

7
23.09.2008 07:15:32

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

644
13.09.2018 15:45:08
Это более понятно, чем приведенный выше ответ. Пример кода стоит тысячи слов.
Novice in.NET 28.06.2018 00:05:12

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

1
23.09.2008 07:58:58
Не запрещено вызывать виртуальную функцию внутри конструктора в C ++.
qbeuek 23.09.2008 07:32:57
То же самое относится и к C ++: если вам не нужен доступ к членам, вам все равно, что они не были инициализированы ...
David Pierre 23.09.2008 09:17:39
Нет. Когда вы вызываете виртуальный метод в конструкторе в C ++, он вызывает не самую глубокую переопределенную реализацию, а версию, связанную с текущим типом. Он вызывается виртуально, но как для типа текущего класса - у вас нет доступа к методам и членам производного класса.
qbeuek 26.09.2008 09:01:31

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

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

18
23.09.2008 07:21:04

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

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

5
23.09.2008 07:25:01

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

161
19.08.2011 08:56:36
Ответ Грега Бича, хотя, к сожалению, не проголосовал так высоко, как мой ответ, я считаю, что это лучший ответ. У него, конечно, есть еще несколько ценных, объяснительных деталей, которые я не нашел времени, чтобы включить.
Lloyd 23.09.2008 18:17:31
На самом деле правила в Java одинаковы.
OlegYch 17.11.2011 22:02:16
@ JoãoPortela C ++ очень сильно отличается. Вызовы виртуальных методов в конструкторах (и деструкторах!) Разрешаются с использованием создаваемого в данный момент типа (и vtable), а не самого производного типа, как Java и C # оба. Вот соответствующая запись FAQ .
Jacek Sieka 28.05.2012 08:26:52
@JacekSieka вы абсолютно правы. Прошло много времени с тех пор, как я написал код на C ++, и я как-то запутал все это. Должен ли я удалить комментарий, чтобы не запутать кого-либо еще?
João Portela 28.05.2012 09:34:10
Существует значительное отличие C # от Java и VB.NET; в C # поля, которые инициализируются в точке объявления, будут обрабатываться до инициализации базового конструктора; это было сделано с целью обеспечения возможности использования объектов производного класса из конструктора, но, к сожалению, такая возможность работает только для функций производного класса, инициализация которых не контролируется никакими параметрами производного класса.
supercat 24.03.2015 18:15:14

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

  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();
    }
  }
87
23.09.2008 13:20:22

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

5
25.10.2012 20:33:34

Выше приведены хорошо написанные ответы о том, почему вы не хотите этого делать. Вот контрпример , где , возможно , вы бы хотели , чтобы сделать это ( в переводе на C # от Практический объектно-ориентированного дизайна в Ruby , по Санди 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 { }
11
28.12.2012 01:19:13
Я хотел бы использовать фабричные методы для этого.
Ian Ringrose 17.03.2015 09:32:28
Мне бы хотелось, чтобы .NET Framework вместо того, чтобы включать в себя Finalizeв качестве элемента по умолчанию Object, который используется в основном бесполезно , использовал этот слот vtable для ManageLifetime(LifetimeStatus)метода, который вызывался бы, когда конструктор возвращается в код клиента, когда конструктор выдает объект или когда объект найден заброшенным. Большинство сценариев, которые влекут за собой вызов виртуального метода из конструктора базового класса, лучше всего обрабатывать с использованием двухэтапного построения, но двухэтапное построение должно вести себя как деталь реализации, а не как требование, чтобы клиенты вызывали второй этап.
supercat 24.03.2015 18:19:16
Тем не менее, проблемы могут возникнуть с этим кодом, как и с любым другим случаем, показанным в этом потоке; GetDependencyне гарантируется безопасность для вызова до вызова MySubClassконструктора. Кроме того, наличие зависимостей по умолчанию, создаваемых по умолчанию, - это не то, что вы бы назвали «чистым DI».
Groo 8.03.2016 09:06:28
В примере делается «выброс зависимости». ;-) Для меня это еще один хороший контрпример для вызова виртуального метода из конструктора. SomeDependency больше не создается в производных MySubClass, что приводит к нарушению поведения для каждой функции MyClass, которая зависит от SomeDependency.
Nachbars Lumpi 31.10.2016 13:05:19

Еще одна интересная вещь, которую я обнаружил, заключается в том, что ошибку 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";
   }

}

0
22.05.2014 16:50:58
Вы не должны найти обходной путь, но решить актуальную проблему.
alzaimar 5.01.2015 07:15:05
Я согласен @alzaimar! Я пытаюсь оставить варианты для лиц, сталкивающихся с подобной проблемой, и тех, кто не хочет реализовывать решения, представленные выше, возможно, из-за некоторых ограничений. В связи с этим (как я уже упоминал в моем обходном пути выше), я также хочу подчеркнуть, что ReSharper, по возможности, должен иметь возможность пометить этот обходной путь как ошибку. Однако в настоящее время этого не происходит, что может привести к двум вещам - они забыли об этом сценарии или хотели сознательно исключить его из-за какого-либо действительного варианта использования, о котором сейчас нельзя думать.
adityap 9.01.2015 21:00:11

Одним из важных недостатков является то, как правильно решить эту проблему?

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

Следующий код, взятый из руководящих принципов проектирования конструктора 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
2.04.2018 19:47:23
Я думаю, что конструктор DerivedFromBetter вызывает простоту конструктора BetterBaseClass. приведенный выше код должен быть эквивалентен общедоступному DerivedFromBetter (): base (), поэтому intialize будет вызываться дважды
user1778606 23.12.2015 15:06:44
Вы можете определить защищенный конструктор в классе BetterBaseClass, который имеет дополнительный bool initializeпараметр, который определяет, Initializeвызывается ли базовый конструктор. Затем производный конструктор будет вызывать, base(false)чтобы избежать вызова Initialize дважды
Sven Vranckx 29.03.2018 10:12:00
@ user1778606: абсолютно! Я исправил это с твоим наблюдением. Спасибо!
Gustavo Mori 2.04.2018 19:46:57
@GustavoMori Это не работает. Базовый класс по-прежнему вызывает DisplayState до запуска конструктора DerivedFromBetter, поэтому он выводит «BetterBaseClass».
user1969177 2.02.2019 01:34:49

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

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());
    }
}
1
14.10.2015 16:23:58
Я почти никогда не делаю этого, так как это затрудняет отладку, если вы хотите войти в конструктор.
Phil1970 18.04.2017 01:11:48

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

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

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"
    }
} 
3
28.08.2016 18:48:23
Нет родительских и дочерних классов, но есть базовые и производные.
hyankov 17.04.2019 16:32:47

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

    public **virtual** User User{ get; set; }
3
5.10.2017 19:15:39

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

-1
13.12.2017 21:14:46
Это заставляет предупреждение исчезнуть, но не решает проблему. Вы сталкиваетесь с той же проблемой, что и другие, когда вы добавляете более производный класс.
Stefan Bormann 30.08.2018 12:55:33

Я думаю, что игнорирование предупреждения может быть законным, если вы хотите дать дочернему классу возможность устанавливать или переопределять свойство, которое родительский конструктор будет использовать сразу:

internal class Parent
{
    public Parent()
    {
        Console.WriteLine("Parent ctor");
        Console.WriteLine(Something);
    }

    protected virtual string Something { get; } = "Parent";
}

internal class Child : Parent
{
    public Child()
    {
        Console.WriteLine("Child ctor");
        Console.WriteLine(Something);
    }

    protected override string Something { get; } = "Child";
}

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

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

Выходные данные выше при создании экземпляра дочернего класса:

Parent ctor
Child
Child ctor
Child
0
11.12.2019 11:55:40