Виртуальный членский вызов в конструкторе
Я получаю предупреждение от ReSharper о вызове виртуального члена от моего конструктора объектов.
Почему бы это не делать?
17 ответов
Когда создается объект, написанный на C#, происходит то, что инициализаторы работают в порядке от самого производного класса до базового класса, а затем конструкторы работают в порядке от базового класса до самого производного класса ( подробности см. В блоге Эрика Липперта). относительно того, почему это).
Кроме того, в.NET-объектах не изменяют тип по мере их создания, а начинают как наиболее производный тип с таблицей методов для наиболее производного типа. Это означает, что вызовы виртуальных методов всегда выполняются для самого производного типа.
Когда вы объедините эти два факта, у вас возникнет проблема, заключающаяся в том, что если вы выполняете вызов виртуального метода в конструкторе, а он не является самым производным типом в своей иерархии наследования, то он будет вызываться в классе, конструктор которого не был Выполнить, и, следовательно, может не быть в подходящем состоянии для вызова этого метода.
Эта проблема, конечно, смягчается, если вы помечаете свой класс как закрытый, чтобы гарантировать, что он является наиболее производным типом в иерархии наследования - в этом случае совершенно безопасно вызывать виртуальный метод.
Чтобы ответить на ваш вопрос, рассмотрите этот вопрос: что напечатает приведенный ниже код, когда 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
}
}
Вы можете запечатать класс 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();
}
}
В C# конструктор базового класса выполняется перед конструктором производного класса, поэтому любые поля экземпляра, которые производный класс может использовать в возможно переопределенном виртуальном члене, еще не инициализированы.
Обратите внимание, что это всего лишь предупреждение, чтобы заставить вас обратить внимание и убедиться, что все в порядке. Существуют реальные варианты использования для этого сценария, вам просто нужно документировать поведение виртуального члена, чтобы он не мог использовать поля экземпляра, объявленные в производном классе ниже, где находится конструктор, вызывающий его.
Выше приведены хорошо написанные ответы о том, почему вы не хотите этого делать. Вот контрпример, где, возможно, вы захотите это сделать (переведено на 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 { }
Да, обычно плохо вызывать виртуальный метод в конструкторе.
На данный момент объект может быть еще не полностью построен, а ожидаемые методами инварианты могут еще не сохраняться.
Потому что пока конструктор не завершит выполнение, объект не будет полностью создан. Любые члены, на которые ссылается виртуальная функция, не могут быть инициализированы. В C++, когда вы находитесь в конструкторе, this
относится только к статическому типу конструктора, в котором вы находитесь, а не к фактическому динамическому типу создаваемого объекта. Это означает, что вызов виртуальной функции может даже не идти туда, куда вы ожидаете.
Одним из важных недостатков является то, как правильно решить эту проблему?
Как объяснил Грег, корень проблемы в том, что конструктор базового класса будет вызывать виртуальный член до того, как будет создан производный класс.
Следующий код, взятый из руководящих принципов конструктора 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++/CLI конструкторы заключены в код, который будет вызывать Dispose
на частично построенном объекте, если строительство не удалось. призвание Dispose
в таких случаях часто необходимо предотвращать утечки ресурсов, но Dispose
методы должны быть готовы к тому, что объект, на котором они выполняются, не был полностью построен.
Ваш конструктор может (позже, в расширении вашего программного обеспечения) вызываться из конструктора подкласса, который переопределяет виртуальный метод. Теперь будет вызвана не реализация функции подклассом, а реализация базового класса. Поэтому не имеет смысла вызывать виртуальную функцию здесь.
Однако, если ваш дизайн соответствует принципу подстановки Лискова, никакого вреда не будет. Вероятно, поэтому это терпимо - предупреждение, а не ошибка.
Предупреждение является напоминанием о том, что виртуальные члены могут быть переопределены в производном классе. В этом случае все, что родительский класс сделал с виртуальным членом, будет отменено или изменено путем переопределения дочернего класса. Посмотрите на небольшой пример удара для ясности
Родительский класс ниже пытается установить значение для виртуального члена в его конструкторе. И это вызовет повторное предупреждение, давайте посмотрим на код:
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"
}
}
Остерегайтесь слепого следования совету Решарпера и запечатывания класса! Если это модель в EF Code First, она удалит виртуальное ключевое слово, что отключит отложенную загрузку его отношений.
public **virtual** User User{ get; set; }
Я думаю, что игнорирование предупреждения может быть законным, если вы хотите дать дочернему классу возможность устанавливать или переопределять свойство, которое родительский конструктор будет использовать сразу:
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
Просто чтобы добавить мои мысли. Если вы всегда инициализируете приватное поле при его определении, этой проблемы следует избегать. По крайней мере, приведенный ниже код работает как шарм:
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());
}
}
В этом конкретном случае есть разница между C++ и C#. В C++ объект не инициализирован, и поэтому небезопасно вызывать вирусную функцию внутри конструктора. В C# при создании объекта класса все его члены инициализируются нулями. Можно вызвать виртуальную функцию в конструкторе, но если вы можете получить доступ к элементам, которые по-прежнему равны нулю. Если вам не нужен доступ к членам, вполне безопасно вызывать виртуальную функцию в C#.
Я бы просто добавил метод Initialize() в базовый класс, а затем вызвал бы его из производных конструкторов. Этот метод вызовет любые виртуальные / абстрактные методы / свойства ПОСЛЕ того, как все конструкторы были выполнены:)
Еще одна интересная вещь, которую я обнаружил, состоит в том, что ошибку 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";
}
}