在C#程序中,构造方法调用虚方法是一个需要避免的禁忌,这样做到底会导致什么异常呢?
我们不妨通过下面一段代码来看看:.
// 基类
public class A
{
protected Ref my;
public A()
{
my = new Ref();
// 构造方法
Console.WriteLine(ToString());
}
// 虚方法
public override string ToString()
{
// 这里使用了内部成员my.str
return my.str;
}
}
// 子类
public class B : A
{
private Ref my2;
public B()
: base()
{
my2 = new Ref();
}
// 重写虚方法
public override string ToString()
{
// 这里使用了内部成员my2.str
return my2.str;
}
}
// 一个简单的引用类型
public class Ref
{
public string str = "我是一个对象";
}
public class Program
{
public static void Main(string[] args)
{
try
{
B b = new B();
}
catch (Exception ex)
{
// 输出异常信息
Console.WriteLine(ex.GetType().ToString());
}
Console.ReadKey();
}
}
下面是运行结果,异常信息是空指针异常?
原因剖析
(1)要解释这个问题产生的原因,我们需要详细地了解一个带有基类的类型(事实上是System.Object,所有的内建类型都有基类)被构造时,所有构造方法被调用的顺序。
在C#中,当一个类型被构造时,它的构造顺序是这样的:
执行变量的初始化表达式 → 执行父类的构造方法(需要的话)→ 调用类型自己的构造方法
我们可以通过以下代码示例来看看上面的构造顺序是如何体现的:
public class Program
{
public static void Main(string[] args)
{
// 构造了一个最底层的子类类型实例
C newObj = new C();
Console.ReadKey();
}
}
// 基类类型
public class Base
{
public Ref baseString = new Ref("Base 初始化表达式");
public Base()
{
Console.WriteLine("Base 构造方法");
}
}
// 继承基类
public class A : Base
{
public Ref aString = new Ref("A 初始化表达式");
public A()
: base()
{
Console.WriteLine("A 构造方法");
}
}
// 继承A
public class B : A
{
public Ref bString = new Ref("B 初始化表达式");
public B()
: base()
{
Console.WriteLine("B 构造方法");
}
}
// 继承B
public class C : B
{
public Ref cString = new Ref("C 初始化表达式");
public C()
: base()
{
Console.WriteLine("C 构造方法");
}
}
// 一个简单的引用类型
public class Ref
{
public Ref(string str)
{
Console.WriteLine(str);
}
}
调试运行,可以看到派生顺序是 : Base → A → B → C,也验证了刚刚我们所提到的构造顺序。
上述代码的整个构造顺序如下图所示:

(2)了解完产生本问题的根本原因,反观虚方法的概念,当一个虚方法被调用时,CLR总是根据对象的实际类型来找到应该被调用的方法定义。
换句话说,当虚方法在基类的构造方法中被调用时,它的类型仍然保持的是子类,子类的虚方法将被执行,但是这时子类的构造方法却还没有完成,任何对子类未构造成员的访问都将产生异常。
如何避免这类问题呢?
其根本方法就在于:永远不要在非叶子类的构造方法中调用虚方法。