协慌网

登录 贡献 社区

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

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

为什么不做这件事?

答案

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

同样在. NET 对象中,不会在构造时更改类型,而是从最派生类型开始,方法表用于最派生类型。这意味着虚方法调用始终在最派生类型上运行。

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

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

为了回答您的问题,请考虑以下问题:在实例化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!?!
    }
}

答案是实际上会抛出NullReferenceException ,因为foo为 null。 在自己的构造函数之前调用对象的基础构造函数 。通过在对象的构造函数中进行virtual调用,您可以介绍继承对象在完全初始化之前执行代码的可能性。

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