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

我收到ReSharper关于从我的对象构造函数调用虚拟成员的警告。

为什么这是不能做的事情?

207879 次浏览

因为在构造函数完成执行之前,对象不会完全实例化。虚函数引用的任何成员都可能不会初始化。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为空。对象的基构造函数在其自己的构造函数之前被调用。通过在对象的构造函数中调用virtual,您引入了继承对象在完全初始化之前执行代码的可能性。

在这种情况下,C++和C#之间存在差异。在C++对象未初始化,因此在构造函数中调用病毒函数是不安全的。在C#中,当创建一个类对象时,它的所有成员都初始化为零。可以在构造函数中调用虚函数,但如果您可能会访问仍然为零的成员。如果您不需要访问成员,在C#中调用虚函数是非常安全的。

在C#中,基类构造函数运行派生类构造函数之前,因此派生类可能在可能重写的虚拟成员中使用的任何实例字段尚未初始化。

请注意,这只是一个警告来引起你的注意并确保它没问题。这个场景有实际的用例,你只需要记录行为的虚拟成员,它不能使用在下面的构造函数调用它的派生类中声明的任何实例字段。

当构造一个用C#编写的对象时,初始化器按顺序从最底层的派生类运行到基类,然后构造器按顺序从基类运行到最底层的派生类(请参阅Eric Lippert的博客了解为什么会这样)。

同样在。NET对象在构造时不会更改类型,而是从最派生的类型开始,方法表是最派生的类型。这意味着虚拟方法调用始终在最派生的类型上运行。

当您将这两个事实结合起来时,您会遇到一个问题,即如果您在构造函数中进行虚方法调用,并且它不是其继承层次结构中派生最多的类型,那么它将在其构造函数尚未运行的类上被调用,因此可能不适合调用该方法。

当然,如果您将类标记为密封以确保它是继承层次结构中最派生的类型,这个问题就会得到缓解——在这种情况下,调用虚拟方法是完全安全的。

你的构造函数可能(稍后,在你的软件扩展中)从覆盖虚方法的子类的构造函数中调用。现在不是子类的函数实现,而是基类的实现将被调用。所以在这里调用虚函数真的没有意义。

但是,如果您的设计满足Liskov替换原则,则不会造成任何伤害。可能这就是为什么它被容忍的原因-警告,而不是错误。

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++ /CLI,构造函数被包装在代码中,如果构造失败,这些代码将在部分构造的对象上调用Dispose。在这种情况下调用Dispose通常是防止资源泄漏所必需的,但是Dispose方法必须准备好应对运行它们的对象可能尚未完全构造好的可能性。

上面有很好的答案可以解释为什么你不会想这样做。这是一个反例,也许你想这样做(由Sandi Metz从Ruby中实用的面向对象设计翻译成C#,第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 { }

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

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

格雷格解释说一样,这里的根本问题是基类构造函数会在构造派生类之前调用虚拟成员。

以下代码取自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的新实例会显示预期的“DerivedFromGood”

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

只是想补充一下我的想法。如果你在定义私有字段时总是初始化它,这个问题应该可以避免。至少下面的代码效果很好:

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 memberthis.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 destroyedConsole.WriteLine(child.Obj);// Output: "Something"}}

谨防盲目听从Resharper的劝告,使班级封号!如果它是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 ctorChildChild ctorChild