.NET中的虚函数

面向对象的程序设计有三大要素,封装、继承和多态。虚函数是多态的重要组成部分,同时又在类的继承关系中有着很多变化。本文讨论.NET中对虚函数的支持。

首先,我们通过一个例子来看看虚函数的普通用法:

class CA {

        public virtual void Foo() {

            Console.WriteLine("CA.Foo");

        }

    }

    class CB : CA {

        public override void Foo() {

            Console.WriteLine("CB.Foo");

        }

}

class Test {

        public static void InvokeFoo(CA ca) {

            ca.Foo();

        }

        public static void Main() {

            InvokeFoo(new CB());

        }

}

输出结果

CB.Foo

在这个例子中,尽管在调用InvokeFoo()的时候,CB被转换成CA,但是当执行ca.Foo的时候,仍然调用了CB的Foo。因为ca此时指向的是一个CB类型的对象。这种调用模式,我们称之为运行时绑定。因为在编译InvokeFoo时,编译器无法获取参数ca的真实类型,只有在运行的时候,才能根据ca的真实类型,决定调用哪一个函数。

在这个例子中,两个关键字值得我们注意,首先是virtual,他告诉编译器,当前函数需要运行时绑定。其次是override,他告诉编译器,我要覆盖基类中的Foo()。

看到这里,可能读者会对两个问题持有疑惑:

[ 问题]: 不用virtual结果如何?

[ 问题]: 不用override结果如何?

读者不妨自己动手修改上例,尝试这两个关键字的不同组合,看看输出的结果如何。在这里,我仅给出组合条件和其输出结果。

序号

基类(CA)中是否有virtual

子类(CB)中是否有override

输出

1

CB.Foo

2

CA.Foo

3

编译错误

4

CA.Foo

我希望通过对这组实验结果的解释,交待一些.NET中虚函数的相关概念。

运行时绑定仅体现在虚函数中。因此在试验4中,输出的结果是CA.Foo。因为Foo没有被申明为virtual,在编译阶段,已经把ca.Foo绑定到CA.Foo。

Override只能用于虚函数中。当子类继承基类,他便拥有了基类所有的函数,Override修饰的函数,将替换基类原来的函数。否则,子类会新增加一个函数,并同时保留基类中的函数。 下面的这个例子,很好的说明了这个问题

class CA {

        public virtual void Foo() {

            Console.WriteLine("CA.Foo");

        }

    }

    class CB : CA {

        public override void Foo() {

            Console.WriteLine("CB.Foo");

        }

    }

    class CC : CA {

        public new void Foo() {

            Console.WriteLine("CC.Foo");

        }

    }

    class Test {

        public static void Main() {

            Console.WriteLine(typeof(CB).GetMethods().Length); // 输出5

            Console.WriteLine(typeof(CC).GetMethods().Length); // 输出6

        }

    }

这段程序输出CB和CC的函数个数,CB的5个函数中,4个来自于Sysetm.Object,剩下的一个就是Foo。CC中多了一个函数,因为使用了new (如果不使用new,也是相同的结果,因为C#编译器默认使用new,但不显示指明new会给出一个警告),说明了CC.Foo是一个不同于CA.Foo的虚函数。

所以,在试验2中,不使用override,我们在InvokeFoo中调用的还是CA.Foo()。虽然这个时候还是运行时绑定,但是因为CB.Foo并没有覆盖CA.Foo,因此我们还是得到了基类的实现。

当一个函数不是虚函数的时候,子类中相同签名的函数总是覆盖了父类中的函数,并不需要override关键字。所以c#编译器会把它当作一个错误,如上表中试验3所示。

如果读者理解了上面的内容,那么来看看一个略微复杂的情况:我们邀请interface出场!

interface IA {

        void Foo();

}

    class CA: IA {

        public void Foo() {

            Console.WriteLine("CA.Foo");

        }

}

[ 问题]: Foo是虚函数吗?

答案是肯定的,就像interface方法不能显示声明为public一样,我们也不能在IA.Foo前面加上virtual。原因很简单,所有的interface方法都是虚函数!在调用interface方法的时候,总是要使用运行时绑定。

[ 问题]: CA实现IA,那么CA.Foo前面需要override吗?

答案是否定的,在C# 中,继承和实现是截然不同的两个概念,尽管在语法上很相似。继承意味着全盘接收基类的函数,而实现只是一个契约,保证当前类会提供interface中声明的函数,而不会接受基类的函数(事实上也不能,因为interface中没有函数的实现)

[ 问题]: CA实现IA,那么CA.Foo前面需要virtual吗?

答案是需要的,否则的话,CA的子类将无法覆写Foo,下面的代码是CA.Foo的IL声明,我们发现了关键字final(注:这里的final是IL语言的关键字,和C#中sealed有些类似,意味着子类不能override当前函数)

.method public hidebysig newslot virtual final

          instance void Foo() cil managed

下面一段代码紧接着上面的代码,读者可以猜测一下输出,看看是否掌握了本文今天讲述的内容,我会在下期博客中讲解其原委,并且和大家进一步通过IL来研究.NET中的虚函数。

class CB : CA, IA {

        public void Foo() {

          Console.WriteLine("CB.Foo");

        }

}

class Test {

        public static void InvokeFoo(CA ia) {

            ca.Foo();

        }

        public static void Main() {

            InvokeFoo(new CA());

            InvokeFoo(new CB());

          }

By com.microsoft.stbc.devdiv.ndp.interop.dev/mountaintai二世