1.什么是虚方法?
C++书中介绍为了指明某个成员函数具有多态性,用关键字virtual来标志其为虚函数。传统的多态实际上就是由虚函数(Virtual Function)利用虚表(Virtual Table)实现的也就是说,虚函数应为多态而生。
MSDN上这样解释virtual:virtual 关键字用于修饰方法、属性、索引器或事件声明,并使它们可以在派生类中被重写。虚拟成员的实现可由派生类中的重写成员更改。
我理解是见到virtual对成员(方法、属性、索引器或事件声明)对象进行修饰的时候,就是用了虚对象成员,常见的就是虚方法。
2.什么时候用虚方法呢?
虚方法是为了面向对象的多态而生的。那么简单的说当我们在C#中要想实现多态的方法之一就是使用到虚函数;复杂点说,那就是因为OOP的核心思想就是用程序语言描述客观世界的对象,从而抽象出一个高内聚、低偶合,易于维护和扩展的模型。
当我们想实现同一个对象时,可以将这个对象的共同部分或者共同易于变化的特性写成一个类,定义成虚方法,但是具体实现该对象下面的子对象时就可以通过继承该父对象类,那么就可以继承下来父类的虚方法了,如果子对象有自己的具体实现,就可以重写(用overrid)这个虚方法或者选择重现实现(用new)这个虚方法。这样当我们需要添加子类的时候,不用考虑改变父类的代码实现。
举一个文字方面的例子,下面再做程序上面的例子:
比如飞禽都有飞这个动作,但是对于不同的鸟类它的飞的动作方式是不同的,有的是滑行,有的要颤抖翅膀,虽然都是飞的行为,但具体实现却是千差万别,在我们抽象的模型中不可能把一个个飞的动作都考虑到,那么怎样为以后留下好的扩展,怎样来处理各个具体飞禽类千差万别的飞行动作呢?比如我现在又要实现一个类“鹤”,它也有飞禽的特征(比如飞这个行为),如何使我可以只用简单地继承“飞禽”,而不去修改“飞禽”这个抽象模型现有的代码,从而达到方便地扩展系统呢?
因此面向对象的概念中引入了虚函数来解决这类问题。
使用虚函数就是在父类中把子类中共有的但却易于变化或者不清楚的特征抽取出来,作为子类需要去重新实现的操作(override)。而虚函数也是OOP中实现多态的关键之一。
3.代码举例:
第一个C#例子:
using System;
namespace peter
{
public class birds
{
public string wing; // 翅膀
public string feather; // 羽毛
public virtual bool fly()
{
//空下来让子类去实现
}
}
//麻雀
public class sparrow:birds
{
public override bool fly()
{
// 实现麻雀飞的动作
}
}
//凤凰
public class phoenix:birds
{
public override bool fly()
{
// 实现凤凰飞的动作
}
}
//实现射击鸟的动作
public class ShootBird(birds b)
{
if(b.fly())
{
//
}
}
public class call()
{
static void Main()
{
// 注意这里声明传入一个“飞禽”类作为参数,而不是某个具体的“鸟类”。好处就是以后不管再出现多少
// 种鸟类,只要是从飞禽继承下来的,都照打不误:)(多态的方式)
//打麻雀
ShootBird(new sparrow());
// 打鹤
ShootBird(new phoenix());
// 都是打鸟的过程,我只要实现了具体某个鸟类(从“飞禽”派生而来)的定义,就可以对它
// 进行射击,而不用去修改ShootBird函数和飞禽基类
ShootBird(new 其它的飞禽());
}
}
}
这样我们只需要在抽象模型“飞禽”里定义Fly()这个行为,表示所有由此“飞禽”派生出去的子类都会有Fly()这个行为,而至于Fly()到底具体是怎么实现的,那么就由具体的子类去实现就好了,不会再影响“飞禽”这个抽象模型了。
第二个C#例子:
虚函数从C#的程序编译的角度来看,它和其它一般的函数有什么区别呢?一般函数在编译时采用先期绑定(详见紫色阴影这篇文章)编译到了执行文件中,其相对地址在程序运行期间是不发生变化的。而虚函数在编译期间采用的是后期绑定,它的相对地址是不确定的,它会根据运行时期对象实例来动态判断要调用的函数,其中那个声明时定义的类叫声明类,那个执行时实例化的类叫实例类。
( 如:飞禽 bird = new 麻雀();
那么飞禽就是声明类,麻雀是实例类。 )
具体的检查的流程如下:
(1)当调用一个对象的函数时,系统会直接去检查这个对象声明定义的类,即声明类,看所调用的函数是否为虚函数;
(2)如果不是虚函数,那么它就直接执行该函数。而如果有virtual关键字,也就是一个虚函数,那么这个时候它就不会立刻执行该函数了,而是转去检查对象的实例类。
(3)在这个实例类里,他会检查这个实例类的定义中是否重新实现了该虚函数(通过override关键字),如果是,则执行该实例类中的这个重新实现的函数。而如果没有的话,系统就会不停地往上找实例类的父类,并对父类重复刚才在实例类里的检查,直到找到第一个重写了该虚函数的父类为止,然后执行该父类里重写后的函数。
知道这点,就可以理解下面代码的运行结果了:
class A
{
protected virtual Func() // 注意virtual,表明这是一个虚函数
{
Console.WriteLine("Func In A");
}
}
class B : A // 注意B是从A类继承,所以A是父类,B是子类
{
protected override Func() // 注意override ,表明重新实现了虚函数
{
Console.WriteLine("Func In B");
}
}
class C : B // 注意C是从B类继承,所以B是父类,C是子类
{
}
class D : A // 注意D是从A类继承,所以A是父类,D是子类
{
protected new Func() // 注意new ,表明覆盖父类里的同名类,而不是重新实现
{
Console.WriteLine("Func In D");
}
}
static void main()
{
A a; // 定义一个a这个A类的对象.这个A就是a的声明类
A b; // 定义一个b这个A类的对象.这个A就是b的声明类
A c; // 定义一个c这个A类的对象.这个A就是c的声明类
A d; // 定义一个d这个A类的对象.这个A就是d的声明类
a = new A(); // 实例化a对象,A是a的实例类
b = new B(); // 实例化b对象,B是b的实例类
c = new C(); // 实例化c对象,C是c的实例类
d = new D(); // 实例化d对象,D是d的实例类
a.Func() ;
// 执行a.Func:1.先检查声明类A 2.检查到是虚方法 3.转去检查实例类A,就为本身 4.执行实例类A中的方法 5.输出结果 Func In A
b.Func() ;
// 执行b.Func:1.先检查声明类A 2.检查到是虚方法 3.转去检查实例类B,有重载的 4.执行实例类B中的方法 5.输出结果 Func In B
c.Func() ;
// 执行c.Func:1.先检查声明类A 2.检查到是虚方法 3.转去检查实例类C,无重载的 4.转去检查类C的父类B,有重载的 5.执行父类B中的Func方法 5.输出结果 Func In B
d.Func();
// 执行d.Func:1.先检查声明类A 2.检查到是虚方法 3.转去检查实例类D,无重载的(这个地方要注意了,虽然D里有实现Func(),但没有使用override关键字,所以不会被认为是重载) 4.转去检查类D的父类A,就为本身 5.执行父类A中的Func方法 5.输出结果 Func In A
D d1 = new D()
d1.Func(); // 执行D类里的Func(),输出结果 Func In D
}
4.虚方法与接口的区别
如果非得要把这两者相提并论,还真能找出一丝的联系。这一丝的联系还得从多态的种类说起。多态的种类有两种,一为基类继承多态(Base Class Polymorphism),二为接口继承多态(Interface Polymorphism)。虚函数的使用实现的是基类继承多态,从设计模式的角度来说基类继承体系描述的是Is-A的问题。比如飞禽就是基类(父类),麻雀和鹤为子类继承了飞禽这个类。麻雀和鹤“Is-A”飞禽。除了基类继承多态,我们还有一种接口继承多态。顾名思义,这种多态是通过继承(更确切的说是 “实现”)接口而产生继承体系的。从设计模式的角度来说接口继承体系描述的是Is-Like-A(或者叫Can-do)的问题(详见博客上另一篇文章《从设计模式看抽象类与接口的区别》)。比如一个具有报警功能的门,我们要实现“报警门”这么一个类,“报警门”“Is-A”门,而不是一个报警器,只是 “Is-Like-A”报警器而已。所以“报警门”的报警功能要通过实现报警器这个接口来实现报警功能。