在构造函数中调用虚函数会导致程序出现莫名其妙的行为,这主要是对象还没有完全构造完成。下面我们先来看一段代码:
class B { protected B() { Method(); } protected virtual void Method() { Console.WriteLine("B Method"); } } class A:B { private readonly string str = "你好"; public A(string str) { this.str=str; } protected override void Method() { Console.WriteLine(str); } public static void Main() { var d = new A("A Method"); } }
在这里我要问一下各位读者,在上述代码中打印出的内容是什么?大部分读者会回答 “A Method” ,实际上的答案是 “你好” 。这是为什么呢?这是因为基类的构造函数调用一个定义在本类中的但是为派生类所重写的虚函数,程序运行的时候会调用派生类的版本,程序在运行期的类型是 A 而不是 B。在 C# 中系统会认为这个对象是一个可以正常使用的对象,这是因为程序在进入构造函数的函数体之前已经把该对象的所有成员变量都进行了初始化。但是者并不意味着这些成员变量的值和开发人员最终想要的值相符,因为程序仅仅执行了成员变量的初始化语句,而没有执行构造函数中的逻辑。
我们将前面的代码稍加修改看一下:
abstract class B { protected B() { Method(); } protected abstract void Method(); } class A:B { private readonly string str = "你好"; public A(string str) { this.str=str; } protected override void Method() { Console.WriteLine(str); } public static void Main() { var d = new A("A Method"); } }
针对上述代码,我想问问各位读者,这段代码可以通过编译码?答案是可以通过编译,这是因为程序就不会创建一个类型为 B 的对象,他创建的对象只是 B 的实现了 Method 方法的子类,程序代码所运行的也是那个子类的 Method 方法。这么做主要是为了避免在构造函数中调用抽象类中的方法,防止抛出异常。虽然这么写可以避免这个问题但是还存在一个很大的缺陷,它会造成 str 这个对象在整个生命周期中无法保持恒定的值。在构造函数还没有把该对象初始化完成之前,它的取值是由初始化语句决定的,但是执行完构造函数之后它的值却变成了构造函数中所设定的那个值。派生类对象所具备的成员变量的默认值是由初始化语句或者系统来确定的,因此开发人员如果想要在构造函数中给这些变量赋值那么就必须等到程序运行到构造函数时才可以。
Tip:C# 对象的运行期类型是一开始就定好的,即便基类是抽象类也依然可以调用其中的虚方法。
小结
在基类构造函数中调用虚函数会导致代码严重依赖于派生类的实现,然后这些实现是无法控制且容易出错的。如果要避免错误,派生类就必须通过初始化语句把所有的实例变量设置好,但是这又会使得开发人员无法运用更多的编程技巧。也就是说在这种情况下派生类必须定义默认构造函数,并且不能定义别的构造函数,这将会给开发人员带来很大的负担。