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

为什么这是不该做的?


当前回答

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

其他回答

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

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

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

正如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#中,基类构造函数在派生类构造函数之前运行,因此派生类可能在可能重写的虚拟成员中使用的任何实例字段都尚未初始化。

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