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

为什么这是不该做的?


当前回答

小心盲目地听从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 ctor
Child
Child ctor
Child

我发现的另一件有趣的事情是,通过执行下面这样的操作可以“满足”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";
   }
}

当用C#编写的对象被构造时,会发生的情况是初始化器按照从最派生类到基类的顺序运行,然后构造函数按照从基类到最派生类的顺序运行(有关为什么这样做的详细信息,请参阅Eric Lippert的博客)。

此外,在.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();
    }
  }