在构造函数中调用虚拟成员

我从ReSharper得到关于从我的对象构造函数调用虚拟成员的警告。 为什么这是不该做的事?

当用C#编写的对象被构​​造时,会发生什么是初始化器从大多数派生类到基类的顺序运行,然后构造函数按从基类到最派生类的顺序运行( 请参阅Eric Lippert的博客以了解详细信息至于为什么这是 )。

同样在.NET中,对象不会像构造types那样改变types,而是从派生types最多的方面开始,而方法表则是派生types最多的types。 这意味着虚拟方法调用总是运行在最派生的types上。

当你把这两个事实结合在一起的时候,你会留下一个问题:如果你在一个构造函数中调用一个虚方法,而且它不是inheritance层次结构中最派生的types,那么它将被调用一个构造函数没有被运行,因此可能不会处于适当的状态来调用该方法。

如果将类标记为密封,以确保它是inheritance层次结构中派生最多的types,则此问题当然会得到缓解 – 在这种情况下,调用虚拟方法是非常安全的。

为了回答你的问题,考虑这个问题:当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 ,因为foo是null。 一个对象的基础构造函数在它自己的构造函数之前被调用 。 通过在一个对象的构造函数中进行virtual调用,可以引入inheritance对象在完全初始化之前执行代码的可能性。

C#的规则与Java和C ++的规则非常不同。

在C#中某个对象的构造函数中,该对象以完全派生types的forms存在于完全初始化(不是“构造”)的窗体中。

 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的构造函数调用一个虚拟函数,它将parsing为B中的任何覆盖(如果提供的话)。

即使你这样故意设置A和B,充分理解系统的行为,你以后可能会感到震惊。 假设你在B的构造函数中调用虚函数,“知道”它们将被B或A处理。 然后,时间stream逝,而其他人决定他们需要定义C,并覆盖那里的一些虚拟function。 突然之间,B的构造函数最终调用C中的代码,这可能导致相当令人惊讶的行为。

无论如何,避免构造函数中的虚函数可能是一个好主意,因为C#,C ++和Java之间的规则如此不同。 你的程序员可能不知道该期待什么!

已经说明了警告的原因,但是如何解决警告? 你必须封闭课堂或虚拟成员。

  class B { protected virtual void Foo() { } } class A : B { public A() { Foo(); // warning here } } 

你可以盖上甲类:

  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#中,基类的构造函数派生类的构造函数之前运行,所以派生类可能在被覆盖的虚拟成员中使用的任何实例字段都没有被初始化。

请注意,这只是一个警告 ,让你注意,并确保它是正确的。 在这种情况下有实际的使用情况,你只需要logging虚拟成员的行为 ,它不能使用调用它的构造函数下的派生类中声明的任何实例字段。

上面有很好的答案,为什么你不想这样做。 下面是一个反例的例子,你可能想这么做(从Sandi Metz的Practical Object-Oriented Design in Ruby中翻译成C#,第126页)。

请注意GetDependency()不触及任何实例variables。 如果静态方法是虚拟的,那将是静态的。

(公平地说,通过dependency injection容器或对象初始化器可能有更聪明的做法…)

 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 { } 

是的,在构造函数中调用virtual方法通常是不好的。

在这一点上,目标可能还没有完全构build,而且方法所期望的不variables可能还没有成立。

你的构造函数可以(后来,在你的软件的扩展中)从覆盖虚拟方法的子类的构造函数中调用。 现在不是子类的函数的实现,而是调用基类的实现。 所以在这里调用一个虚拟函数并没有什么意义。

但是,如果您的devise符合Liskov替代原则,则不会有任何伤害。 也许这就是为什么它是容忍的 – 一个警告,而不是一个错误。

其他答案还没有解决的这个问题的一个重要方面是, 如果派生类期望它的作用,基类从其构造函数中调用虚拟成员是安全的。 在这种情况下,派生类的devise者负责确保在施工完成之前运行的任何方法的行为与在这种情况下的行为一样合理。 例如,在C ++ / CLI中,构造函数被包装在代码中,如果构build失败,将在部分构造的对象上调用Dispose 。 在这种情况下调用Dispose通常需要防止资源泄漏,但是Dispose方法必须为运行它们的对象可能尚未完全构build的可能性做好准备。

因为直到构造函数完成执行,对象没有完全实例化。 由虚函数引用的任何成员可能不会被初始化。 在C ++中,当您在构造函数中时, this只会引用您所在构造函数的静态types,而不是指正在创build的对象的实际dynamictypes。 这意味着虚拟函数调用可能不会达到您所期望的。

警告提醒虚拟成员可能被派生类覆盖。 在这种情况下,父类对虚拟成员所做的任何操作都将被覆盖子类撤消或更改。 看清楚这个小例子

下面的父类尝试在其构造函数中将值设置为虚拟成员。 这会触发重新发出警告,让我们看看代码:

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

这里的子类覆盖父属性。 如果这个属性没有被标记为虚拟的,那么编译器会警告该属性隐藏父类的属性,并build议如果有意的话,添加“新”关键字。

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

在这个特定的情况下,C ++和C#是有区别的。 在C ++中,对象没有被初始化,所以在构造函数中调用虚函数是不安全的。 在C#中,当一个类对象被创build时,它的所有成员都被初始化为零。 可以在构造函数中调用虚函数,但是如果您可能访问仍然为零的成员。 如果你不需要访问成员,在C#中调用虚函数是非常安全的。

只是添加我的想法。 如果在定义私有字段时总是初始化,则应避免此问题。 至less下面的代码就像一个魅力:

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

要小心盲目地遵循着Resharper的build议,并把课程封闭起来! 如果它是EF Code First中的一个模型,它将删除虚拟关键字,并禁用其关系的延迟加载。

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

我发现的另一个有趣的事情是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"; } 

}

一个重要的缺点是,解决这个问题的正确方法是什么?

正如Greg所解释的那样 ,这里的根本问题是基类构造函数会在构造派生类之前调用​​虚拟成员。

以下代码来自MSDN的构造器devise指南 ,演示了这个问题。

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

当创build一个新的DerivedFromBad实例时,基类构造函数调用DisplayState并显示BadBaseClass因为该字段尚未被派生的构造函数更新。

 public class Tester { public static void Main() { var bad = new DerivedFromBad(); } } 

改进的实现从基类构造函数中删除虚拟方法,并使用Initialize方法。 创buildDerivedFromBetter的新实例将显示预期的“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"; this.Initialize(); } public override void DisplayState() { Console.WriteLine(this.state); } }