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

为什么这是不该做的?


当前回答

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

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

其他回答

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

此外,在.NET对象中,对象在构造时不会改变类型,而是从最派生的类型开始,方法表是最派生的。这意味着虚拟方法调用总是在最派生的类型上运行。

当你结合这两个事实时,你会遇到这样的问题:如果你在构造函数中调用了一个虚拟方法,并且它不是继承层次结构中最派生的类型,那么它将在一个尚未运行构造函数的类上被调用,因此可能不处于调用该方法的合适状态。

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

我发现的另一件有趣的事情是,通过执行下面这样的操作可以“满足”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#中,基类构造函数在派生类构造函数之前运行,因此派生类可能在可能重写的虚拟成员中使用的任何实例字段都尚未初始化。

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

对于你为什么不想这样做,上面有很好的答案。这里有一个反例,也许你想这样做(由Sandi Metz翻译成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 { }

这个问题的一个重要方面(其他答案尚未解决)是,如果派生类希望基类从其构造函数中调用虚拟成员,则基类是安全的。在这种情况下,派生类的设计者负责确保在构造完成之前运行的任何方法都将在这种情况下尽可能合理地运行。例如,在C++/CLI中,构造函数被包装在代码中,如果构造失败,这些代码将对部分构造的对象调用Dispose。在这种情况下,调用Dispose通常是防止资源泄漏所必需的,但Dispose方法必须做好准备,以防运行它们的对象可能尚未完全构造。