Virtual member call in a constructor

Virtual member call in a constructor

技术背景

在 C# 等面向对象编程语言中,构造函数用于初始化对象的状态。虚成员(如虚方法、虚属性)允许派生类重写基类的实现,以实现多态性。然而,在构造函数中调用虚成员可能会导致一些意想不到的问题,这与对象的构造顺序和虚方法的调用机制有关。

实现步骤

1. 对象构造顺序

在 C# 中,对象构造时,初始化器从最派生类到基类依次运行,然后构造函数从基类到最派生类依次运行。同时,.NET 对象在构造过程中类型不会改变,从一开始就是最派生类型,方法表也是最派生类型的。这意味着虚方法调用总是在最派生类型上运行。

2. 虚成员调用问题

如果在构造函数中进行虚方法调用,且该类不是继承层次结构中的最派生类型,那么该方法将在其构造函数尚未运行的类上被调用,可能导致对象处于不适合调用该方法的状态。

以下是一个示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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!?!
}
}

在上述代码中,当创建 Child 对象时,会抛出 NullReferenceException 异常,因为 foo 还未初始化。这是因为基类构造函数在派生类构造函数之前调用,而在基类构造函数中调用虚方法时,派生类的成员还未初始化。

核心代码

示例 1:避免问题的方法

可以通过将类标记为 sealed 来确保它是继承层次结构中的最派生类型,这样调用虚方法就是安全的。

1
2
3
4
5
6
7
sealed class A : B
{
public A()
{
Foo(); // no warning
}
}

也可以密封方法:

1
2
3
4
5
6
7
8
9
10
11
12
class A : B
{
public A()
{
Foo(); // no warning
}

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

示例 2:改进的实现

通过引入 Initialize 方法,确保在所有构造函数执行完毕后再调用虚方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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);
}
}

最佳实践

  • 尽量避免在构造函数中调用虚成员,因为对象可能尚未完全构造,方法期望的不变量可能不成立。
  • 如果必须在构造函数中调用虚成员,确保派生类能够处理这种情况,并且相关方法不会依赖于未初始化的成员。
  • 可以使用 Initialize 方法来确保在所有构造函数执行完毕后再调用虚成员。

常见问题

1. 为什么在构造函数中调用虚成员会有问题?

因为基类构造函数在派生类构造函数之前调用,而虚方法调用总是在最派生类型上运行,所以在基类构造函数中调用虚方法时,派生类的成员可能尚未初始化。

2. 如何解决在构造函数中调用虚成员的警告?

可以将类标记为 sealed 或密封虚成员方法,也可以通过引入 Initialize 方法来确保在所有构造函数执行完毕后再调用虚成员。

3. C# 和 C++ 在这方面有什么区别?

在 C++ 中,对象在构造过程中 this 仅指构造函数的静态类型,而不是正在创建的对象的实际动态类型,这意味着虚函数调用可能不会按预期进行。在 C# 中,对象从一开始就是最派生类型,但在构造函数中调用虚函数可能会访问到未初始化的成员。


Virtual member call in a constructor
https://119291.xyz/posts/virtual-member-call-in-a-constructor/
作者
ww
发布于
2025年6月18日
许可协议