.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    输出

是                          是                          CB.Foo
是                          否                          CA.Foo
否                          是                          编译错误
否                          否                          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());
    }
}


上一篇: Js版连连看V1.1
下一篇: 比较直接量和构造函数创建的Js对象
文章来自: msdn
引用通告: 查看所有引用 | 我要引用此文章
Tags:
最新日志:
评论: 0 | 引用: 0 | 查看次数: 3350
发表评论
登录后再发表评论!