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

时间:2020-03-06 14:35:01  来源:igfitidea点击:

我从ReSharper收到一个警告,提示我对象构造函数调用了虚拟成员。

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

解决方案

因为在构造函数完成执行之前,对象没有完全实例化。虚函数引用的任何成员都可能不会初始化。在C ++中,当我们在构造函数中时," this"仅是指我们所在构造函数的静态类型,而不是所创建对象的实际动态类型。这意味着虚拟函数调用甚至可能不会到达我们期望的位置。

是的,在构造函数中调用虚拟方法通常是不好的。

此时,对象可能尚未完全构建,并且方法所期望的不变性可能尚未成立。

为了回答问题,请考虑以下问题:实例化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为null。对象的基础构造函数在其自己的构造函数之前被调用。通过在对象的构造函数中进行"虚拟"调用,我们将引入一种可能性,即继承对象将在代码完全初始化之前执行代码。

在这种特定情况下,C ++和C之间是有区别的。
在C ++中,对象未初始化,因此在构造函数内部调用虚拟函数是不安全的。
在Cw中,创建类对象时,其所有成员都将初始化为零。可以在构造函数中调用虚函数,但是如果我们将访问仍为零的成员,则可以。如果我们不需要访问成员,则在C#中调用虚拟函数是相当安全的。

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

请注意,这只是警告,请我们注意并确保它正常。在这种情况下有实际的用例,我们只需要记录虚拟成员的行为,即它不能使用在派生类中声明的构造函数调用以下的实例字段。

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

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

当我们将这两个事实结合在一起时,便会遇到以下问题:如果在构造函数中进行虚拟方法调用,并且该方法不是其继承层次结构中派生最多的类型,则将在其构造函数尚未被使用的类上被调用运行,因此可能不处于调用该方法的适当状态。

如果将类标记为密封以确保它是继承层次结构中最派生的类型,则可以减轻此问题,在这种情况下,调用虚方法是绝对安全的。

可以从覆盖虚拟方法的子类的构造函数中调用构造函数(此后,在软件的扩展中)。现在,不是子类的函数实现,而是基类的实现将被调用。因此,在这里调用虚函数实际上没有任何意义。

但是,如果设计满足Liskov替代原则,则不会造成任何损害。也许这就是为什么它可以容忍警告而不是错误。

Care规则与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 B
  {
    protected virtual void Foo() { }
  }

  class A : B
  {
    public A()
    {
      Foo(); // warning here
    }
  }

我们可以密封A类:

sealed class A : B
  {
    public A()
    {
      Foo(); // no warning
    }
  }

或者我们可以密封方法Foo:

class A : B
  {
    public A()
    {
      Foo(); // no warning
    }

    protected sealed override void Foo()
    {
      base.Foo();
    }
  }