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

为什么这是不该做的?


当前回答

我认为,如果您想让子类能够设置或重写父构造函数将立即使用的属性,则忽略警告可能是合法的:

internal class Parent
{
    public Parent()
    {
        Console.WriteLine("Parent ctor");
        Console.WriteLine(Something);
    }

    protected virtual string Something { get; } = "Parent";
}

internal class Child : Parent
{
    public Child()
    {
        Console.WriteLine("Child ctor");
        Console.WriteLine(Something);
    }

    protected override string Something { get; } = "Child";
}

这里的风险是子类从其构造函数设置属性,在这种情况下,值的更改将在调用基类构造函数之后发生。

我的用例是,我希望子类提供一个特定的值或一个实用程序类(如转换器),而不需要在基础上调用初始化方法。

在实例化子类时,上面的输出是:

Parent ctor
Child
Child ctor
Child

其他回答

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

在这个特定的例子中,C++和C#之间有区别。在C++中,对象未初始化,因此在构造函数中调用病毒函数是不安全的。在C#中,当创建类对象时,其所有成员都是零初始化的。可以在构造函数中调用虚拟函数,但如果您将访问仍然为零的成员。如果您不需要访问成员,那么在C#中调用虚拟函数是非常安全的。

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

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

此时,对象可能还没有完全构造,方法所期望的不变量可能还不成立。

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

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