Я получаю предупреждение от ReSharper о вызове виртуального члена из конструктора моих объектов.
Почему бы этого не делать?
Вы можете найти статью от @ m.edmondson сейчас здесь: codeproject.com/Articles/802375/…





Потому что до тех пор, пока конструктор не завершит выполнение, объект не будет полностью создан. Любые члены, на которые ссылается виртуальная функция, не могут быть инициализированы. В C++, когда вы находитесь в конструкторе, this относится только к статическому типу конструктора, в котором вы находитесь, а не к фактическому динамическому типу создаваемого объекта. Это означает, что вызов виртуальной функции может даже пойти не туда, где вы ожидаете.
Да, вообще плохо вызывать виртуальный метод в конструкторе.
На этом этапе объект может быть еще не полностью построен, и инварианты, ожидаемые методами, могут еще не выполняться.
Чтобы ответить на ваш вопрос, рассмотрите следующий вопрос: что будет напечатан приведенный ниже код при создании экземпляра объекта 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 имеет значение NULL. Базовый конструктор объекта вызывается перед собственным конструктором. Имея вызов virtual в конструкторе объекта, вы создаете возможность того, что наследующие объекты выполнят код до того, как они будут полностью инициализированы.
Это более ясно, чем ответ выше. Образец кода стоит тысячи слов.
Я думаю, что инициализация foo на месте (например, private string foo = "INI";) сделает более ясным, что foo действительно инициализируется. (вместо некоторого состояния неинициализированный).
В этом конкретном случае есть разница между C++ и C#. В C++ объект не инициализируется, поэтому вызывать виртуальную функцию внутри конструктора небезопасно. В C# при создании объекта класса все его члены инициализируются нулем. Можно вызвать виртуальную функцию в конструкторе, но если вы можете получить доступ к членам, которые все еще равны нулю. Если вам не нужен доступ к членам, вполне безопасно вызывать виртуальную функцию на C#.
В C++ не запрещено вызывать виртуальную функцию внутри конструктора.
Тот же аргумент справедлив и для C++, если вам не нужен доступ к членам, вам все равно, что они не были инициализированы ...
Нет. Когда вы вызываете виртуальный метод в конструкторе C++, он вызывает не самую глубокую переопределенную реализацию, а версию, связанную с текущим типом. Он вызывается виртуально, но как будто для типа текущего класса - у вас нет доступа к методам и членам производного класса.
В C# конструктор базового класса запускает перед конструктора производного класса, поэтому любые поля экземпляра, которые производный класс может использовать в возможно замещенном виртуальном члене, еще не инициализированы.
Обратите внимание, что это просто предупреждение, чтобы заставить вас обратить внимание и убедиться, что все в порядке. Существуют фактические варианты использования для этого сценария, вам просто нужно задокументировать поведение виртуального члена, чтобы он не мог использовать какие-либо поля экземпляра, объявленные в производном классе ниже, где его вызывает конструктор.
Когда создается объект, написанный на C#, происходит то, что инициализаторы запускаются по порядку от самого производного класса к базовому классу, а затем конструкторы запускаются по порядку от базового класса к самому производному классу (см. блог Эрика Липперта, чтобы узнать, почему это).
Кроме того, в .NET объекты не изменяют тип по мере их создания, а начинаются как наиболее производный тип с таблицей методов для наиболее производного типа. Это означает, что вызовы виртуальных методов всегда выполняются для наиболее производного типа.
Когда вы объединяете эти два факта, вы остаетесь с проблемой, заключающейся в том, что если вы вызовете виртуальный метод в конструкторе, а это не самый производный тип в его иерархии наследования, он будет вызываться для класса, конструктор которого не был run, и поэтому может быть не в подходящем состоянии для вызова этого метода.
Эта проблема, конечно, смягчается, если вы помечаете свой класс как запечатанный, чтобы гарантировать, что это самый производный тип в иерархии наследования - в этом случае совершенно безопасно вызывать виртуальный метод.
Грег, скажите, пожалуйста, зачем кому-то иметь класс SEALED (который не может быть НАСЛЕДОВАННЫМ), если у него есть ВИРТУАЛЬНЫЕ члены [то есть для переопределения в ПРОИЗВОДНЫХ классах]?
Если вы хотите убедиться, что производный класс не может быть получен в дальнейшем, вполне приемлемо его запечатать.
@Paul - Дело в том, что завершено создание виртуальных членов класса [es] основание, и, таким образом, класс отмечен как полностью производный, каким вы хотите его видеть.
@Greg Если поведение виртуального метода не имеет ничего общего с переменными экземпляра, разве это не нормально? Кажется, может быть, мы сможем объявить, что виртуальный метод не будет изменять переменные экземпляра? (статический?) Например, если вы хотите иметь виртуальный метод, который можно переопределить для создания экземпляра более производного типа. Мне это кажется безопасным и не требует этого предупреждения.
@Sahuagin - можно написать виртуальный метод, который можно без проблем вызывать из конструктора, и вы можете добавить комментарий //ReSharper Warning Disable, чтобы он не жаловался. Но R # просто предупреждает вас, что это потенциально опасно. Как всегда, предупреждения иногда можно игнорировать, если вы знаете, что делаете.
Просто любопытно. Если вы не хотите запечатывать класс, что было бы подходящим способом получить / установить свойство виртуального члена в конструкторе? Нужен ли вам метод / свойство оболочки с логикой для учета возможности переопределения члена?
@mo. - Вызов чего-либо, что прямо или косвенно не находится на вершине цепочки наследования, может вызвать проблемы. Если вы не хотите запечатать весь класс, но хотите вызвать конкретный виртуальный метод или использовать конкретное виртуальное свойство из конструктора, вы можете переопределить и запечатать элементы, к которым вам требуется доступ, например protected sealed override void Foo() { ... }.
@PaulPacurar - если вы хотите вызвать виртуальный метод в самом производном классе, вы все равно получите предупреждение, хотя знаете, что это не вызовет проблемы. В этом случае вы можете поделиться своими знаниями с системой, запечатав этот класс.
Есть один случай, когда вы можете вызывать виртуальные члены в конструкторе. Когда вы используете некоторые структуры ORM, которые требуют, чтобы свойства классов сущностей были виртуальными. И вы можете инициализировать свойство, которое имеет отношение один-ко-многим к объекту в конструкторе, чтобы присвоить ему значение по умолчанию. Вот откуда я получил предупреждение и нашел этот пост. Думаю, мы тут ни при чем.
Я откатил внесенное изменение, потому что оно было неправильным. Я никогда не узнаю, как это было одобрено.
Если вы не хотите запечатывать класс и хотите использовать метод унаследованного базового класса, вызовите base.Method();.
@CADbloke, нет, это не то, как это работает, когда метод виртуальный.
О'кей, спасибо. Я оставлю это здесь как предупреждение для других (относительно виртуальных методов, а также моих комментариев).
Я откатил это, потому что редактирование было неправильным. Когда я сказал «инициализаторы», я имел в виду именно этот термин. Разъяснение того, что он «не имеет ничего общего с инициализаторами объекта I», когда это именно то, о чем я говорю, не является разъяснением, это просто делает его неверным.
РЕШЕНИЕ. Если вы вызываете виртуальный метод в конструкторе, вы, вероятно, уже знаете, что управляете жизненным циклом объекта. И я готов поспорить, что создаваемый вами класс можно описать как «вспомогательный прокси». - Вы хотите убедиться, что у унаследованного класса есть возможность инициализировать себя. - Итак, создайте виртуальный метод void Initialize (), и пусть он будет первым методом, вызываемым в конструкторе базового класса. - Затем подкласс может выбрать переопределение метода Initialize для любых операций, подобных конструктору. - Вы видите этот шаблон в контроллерах MVC в .Net
так что объяснение очень ясное и прямое, но решение ...? просто нужно избегать этого? и если вам нужно это сделать, объяснение в том, что вы плохо спроектировали?
Ваш конструктор может (позже, в расширении вашего программного обеспечения) вызываться из конструктора подкласса, который переопределяет виртуальный метод. Теперь будет вызываться не реализация функции подкласса, а реализация базового класса. Так что здесь нет смысла вызывать виртуальную функцию.
Однако, если ваш дизайн удовлетворяет принципу замещения Лискова, вреда не будет. Наверное, поэтому терпят - предупреждение, а не ошибка.
Правила 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. Ваши программисты могут не знать, чего ожидать!
Ответ Грега Бича, хотя, к сожалению, не получил такого высокого уровня, как мой ответ, я считаю, что это лучший ответ. В нем определенно есть еще несколько ценных пояснительных деталей, на которые я не нашел времени.
Собственно правила в Java такие же.
@ JoãoPortela На самом деле C++ совсем другой. Вызов виртуальных методов в конструкторах (и деструкторах!) Разрешается с использованием типа (и vtable), который создается в настоящее время, а не самого производного типа, как это делают Java и C#. Вот соответствующая запись в FAQ.
@JacekSieka, вы абсолютно правы. Я давно не писал на C++ и как-то все это запутал. Следует ли мне удалить комментарий, чтобы никого не запутать?
C# во многом отличается от Java и VB.NET; в C# для полей, которые инициализируются в точке объявления, инициализация будет обработана до вызова базового конструктора; это было сделано для того, чтобы объекты производного класса можно было использовать из конструктора, но, к сожалению, такая возможность работает только для функций производного класса, инициализация которых не контролируется никакими параметрами производного класса.
Причины предупреждения уже описаны, но как исправить предупреждение? Вы должны запечатать либо класс, либо виртуальный член.
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++ / CLI конструкторы обернуты в код, который вызовет Dispose для частично построенного объекта, если построение завершится неудачно. Вызов Dispose в таких случаях часто необходим для предотвращения утечки ресурсов, но методы Dispose должны быть готовы к тому, что объект, на котором они выполняются, может быть не полностью сконструирован.
Выше есть хорошо написанные ответы о том, почему вы не стал бы хотите это сделать. Вот контрпример, в котором, возможно, вы захотите это сделать с помощью бы (переведено на C# из Практический объектно-ориентированный дизайн в Ruby Сэнди Мец, стр. 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 { }
Я бы хотел использовать для этого фабричные методы.
Я бы хотел, чтобы .NET Framework вместо того, чтобы включать в основном бесполезный Finalize в качестве члена Object по умолчанию, использовал этот слот vtable для метода ManageLifetime(LifetimeStatus), который будет вызываться, когда конструктор возвращается к клиентскому коду, когда конструктор бросает или когда обнаруживается, что объект брошен. Большинство сценариев, которые повлекут за собой вызов виртуального метода из конструктора базового класса, лучше всего можно было бы обработать, используя двухэтапное построение, но двухэтапное построение должно вести себя как деталь реализации, а не требование, чтобы клиенты вызывали второй этап.
Тем не менее, с этим кодом могут возникнуть проблемы, как и с любым другим случаем, показанным в этом потоке; Не гарантируется безопасность вызова GetDependency до вызова конструктора MySubClass. Кроме того, создание экземпляров зависимостей по умолчанию - это не то, что вы бы назвали «чистым DI».
Пример делает «исключение зависимости». ;-) Для меня это еще один хороший контрпример для вызова виртуального метода из конструктора. SomeDependency больше не создается в производных MySubClass, что приводит к нарушению поведения каждой функции MyClass, которая зависит от SomeDependency.
Еще одна интересная вещь, которую я обнаружил, заключается в том, что ошибку ReSharper можно «удовлетворить», выполнив что-то вроде ниже, что для меня глупо. Однако, как упоминалось многими ранее, вызывать виртуальные свойства / методы в конструкторе по-прежнему не рекомендуется.
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";
}
}
Вы не должны искать обходной путь, а решите реальную проблему.
Я согласен @alzaimar! Я пытаюсь оставить варианты для людей, которые сталкиваются с подобной проблемой и не хотят реализовывать решения, представленные выше, вероятно, из-за некоторых ограничений. При этом (как я уже упоминал в своем обходном пути выше) еще одна вещь, на которую я пытаюсь указать, - это то, что ReSharper, если возможно, должен иметь возможность пометить этот обходной путь как ошибку. Однако в настоящее время этого не происходит, что может привести к двум вещам - они забыли об этом сценарии или они хотели намеренно исключить его из-за какого-то допустимого варианта использования, о котором сейчас нельзя думать.
@adityap Чтобы подавить предупреждение, используйте подавление предупреждений jetbrains.com/help/resharper/…
Один важный недостающий бит: как правильно решить эту проблему?
Как и Грег объяснил, основная проблема здесь в том, что конструктор базового класса вызовет виртуальный член до того, как будет создан производный класс.
Следующий код, взятый из Рекомендации по проектированию конструкторов 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);
}
}
гм, я думаю, что конструктор DerivedFromBetter вызывает имплицитность конструктора BetterBaseClass. приведенный выше код должен быть эквивалентен public DerivedFromBetter (): base (), поэтому intialize будет вызываться дважды
Вы можете определить защищенный конструктор в классе BetterBaseClass, который имеет дополнительный параметр bool initialize, который определяет, вызывается ли Initialize в базовом конструкторе. Затем производный конструктор вызовет base(false), чтобы избежать двойного вызова Initialize.
@ user1778606: абсолютно! Я исправил это вашим наблюдением. Спасибо!
@GustavoMori Это не работает. Базовый класс по-прежнему вызывает DisplayState до запуска конструктора DerivedFromBetter, поэтому он выводит «BetterBaseClass».
Просто чтобы добавить свои мысли. Если вы всегда инициализируете частное поле при его определении, этой проблемы следует избегать. По крайней мере, приведенный ниже код работает как шарм:
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());
}
}
Я почти никогда этого не делаю, так как это несколько усложняет отладку, если вы хотите перейти в конструктор.
Предупреждение является напоминанием о том, что виртуальные члены могут быть переопределены в производном классе. В этом случае все, что родительский класс сделал с виртуальным членом, будет отменено или изменено путем переопределения дочернего класса. Взгляните на небольшой пример удара для ясности.
Приведенный ниже родительский класс пытается установить значение виртуального члена в своем конструкторе. И это вызовет предупреждение 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();
}
}
Дочерний класс здесь переопределяет родительское свойство. Если это свойство не было помечено как виртуальное, компилятор предупредит, что свойство скрывает свойство родительского класса, и предложит добавить ключевое слово 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, она удалит ключевое слово virtual, что отключит ленивую загрузку ее отношений.
public **virtual** User User{ get; set; }
Я бы просто добавил метод Initialize () к базовому классу, а затем вызвал бы его из производных конструкторов. Этот метод будет вызывать любые виртуальные / абстрактные методы / свойства ПОСЛЕ выполнения всех конструкторов :)
Это приводит к исчезновению предупреждения, но не решает проблему. Когда вы добавляете более производный класс, вы сталкиваетесь с той же проблемой, что и объясняли другие.
Я думаю, что игнорирование предупреждения может быть законным, если вы хотите дать дочернему классу возможность устанавливать или переопределять свойство, которое родительский конструктор будет использовать сразу:
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
@ m.edmondson, серьезно .. ваш комментарий должен быть здесь ответом. Хотя объяснение Грега верное, я не понял его, пока не прочитал ваш блог.