c#构造函数调用构造函数




在构造函数中调用虚拟成员 (12)

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

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


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


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

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


只是为了补充我的想法。 如果在定义私有字段时总是初始化它,则应避免此问题。 至少下面的代码就像一个魅力:

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

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

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


在这个特定的情况下,C ++和C#是有区别的。 在C ++中,对象未初始化,因此在构造函数中调用虚函数是不安全的。 在C#中,当一个类对象被创建时,它的所有成员都被初始化为零。 可以在构造函数中调用虚函数,但是如果可以访问仍然为零的成员。 如果你不需要访问成员,在C#中调用虚函数是非常安全的。


当用C#编写的对象被构​​造时,会发生什么是初始化器从大多数派生类到基类的顺序运行,然后构造函数按从基类到最派生类的顺序运行( 请参阅Eric Lippert的博客以了解详细信息至于为什么这是 )。

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

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

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


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

但是,如果您的设计符合Liskov替代原则,则不会有任何伤害。 可能这就是为什么它被容忍 - 一个警告,而不是一个错误。


我只是将一个Initialize()方法添加到基类,然后从派生的构造函数中调用它。 该方法将在所有构造函数执行后调用任何虚拟/抽象方法/属性:)


是的,在构造函数中调用virtual方法通常很糟糕。

此时,目标可能还没有完全构建,并且方法期望的不变量可能还没有成立。


谨防盲目地遵循Resharper的建议并使课程密封! 如果它是EF Code First中的模型,它将删除虚拟关键字,并禁用其关系的延迟加载。

    public **virtual** User User{ get; set; }

为了回答你的问题,考虑这个问题:当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 ,因为foo是空的。 一个对象的基础构造函数在它自己的构造函数之前被调用 。 通过在对象的构造函数中进行virtual调用,您可以引入继承对象在完全初始化之前执行代码的可能性。





virtual-functions