Virtual member call in a constructor
Virtual member call in a constructor
技术背景
在 C# 等面向对象编程语言中,构造函数用于初始化对象的状态。虚成员(如虚方法、虚属性)允许派生类重写基类的实现,以实现多态性。然而,在构造函数中调用虚成员可能会导致一些意想不到的问题,这与对象的构造顺序和虚方法的调用机制有关。
实现步骤
1. 对象构造顺序
在 C# 中,对象构造时,初始化器从最派生类到基类依次运行,然后构造函数从基类到最派生类依次运行。同时,.NET 对象在构造过程中类型不会改变,从一开始就是最派生类型,方法表也是最派生类型的。这意味着虚方法调用总是在最派生类型上运行。
2. 虚成员调用问题
如果在构造函数中进行虚方法调用,且该类不是继承层次结构中的最派生类型,那么该方法将在其构造函数尚未运行的类上被调用,可能导致对象处于不适合调用该方法的状态。
以下是一个示例代码:
1 |
|
在上述代码中,当创建 Child
对象时,会抛出 NullReferenceException
异常,因为 foo
还未初始化。这是因为基类构造函数在派生类构造函数之前调用,而在基类构造函数中调用虚方法时,派生类的成员还未初始化。
核心代码
示例 1:避免问题的方法
可以通过将类标记为 sealed
来确保它是继承层次结构中的最派生类型,这样调用虚方法就是安全的。
1 |
|
也可以密封方法:
1 |
|
示例 2:改进的实现
通过引入 Initialize
方法,确保在所有构造函数执行完毕后再调用虚方法。
1 |
|
最佳实践
- 尽量避免在构造函数中调用虚成员,因为对象可能尚未完全构造,方法期望的不变量可能不成立。
- 如果必须在构造函数中调用虚成员,确保派生类能够处理这种情况,并且相关方法不会依赖于未初始化的成员。
- 可以使用
Initialize
方法来确保在所有构造函数执行完毕后再调用虚成员。
常见问题
1. 为什么在构造函数中调用虚成员会有问题?
因为基类构造函数在派生类构造函数之前调用,而虚方法调用总是在最派生类型上运行,所以在基类构造函数中调用虚方法时,派生类的成员可能尚未初始化。
2. 如何解决在构造函数中调用虚成员的警告?
可以将类标记为 sealed
或密封虚成员方法,也可以通过引入 Initialize
方法来确保在所有构造函数执行完毕后再调用虚成员。
3. C# 和 C++ 在这方面有什么区别?
在 C++ 中,对象在构造过程中 this
仅指构造函数的静态类型,而不是正在创建的对象的实际动态类型,这意味着虚函数调用可能不会按预期进行。在 C# 中,对象从一开始就是最派生类型,但在构造函数中调用虚函数可能会访问到未初始化的成员。