C#能否在构造方法中调用虚方法?

在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();
    }
}

下面是运行结果,异常信息是空指针异常?

C#能否在构造方法中调用虚方法?

原因剖析

(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,也验证了刚刚我们所提到的构造顺序。

C#能否在构造方法中调用虚方法?

上述代码的整个构造顺序如下图所示:

C#能否在构造方法中调用虚方法?

(2)了解完产生本问题的根本原因,反观虚方法的概念,当一个虚方法被调用时,CLR总是根据对象的实际类型来找到应该被调用的方法定义。

换句话说,当虚方法在基类的构造方法中被调用时,它的类型仍然保持的是子类,子类的虚方法将被执行,但是这时子类的构造方法却还没有完成,任何对子类未构造成员的访问都将产生异常

如何避免这类问题呢?

其根本方法就在于:永远不要在非叶子类的构造方法中调用虚方法