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

为什么这是不该做的?


当前回答

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

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

以下代码摘自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);
    }
}

其他回答

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之间的规则是如此不同。你的程序员可能不知道该怎么做!

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

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

我只需要向基类添加一个Initialize()方法,然后从派生构造函数调用它。在执行所有构造函数之后,该方法将调用任何虚拟/抽象方法/财产:)

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

然而,如果您的设计满足Liskov替换原则,则不会造成任何伤害。也许这就是为什么它被容忍的原因——一个警告,而不是错误。