[你必须知道的.NET]第十七回:貌合神离:覆写和重载
本文将介绍以下内容:
- 什么是覆写,什么是重载
- 覆写与重载的区别
- 覆写与重载在多态特性中的应用
1. 引言
覆写(override)与重载(overload),是成就.NET面向对象多态特性的基本技术之一,两个貌似相似而实则不然的概念,常常带给我们很多的误解,因此有必要以专题来讨论清楚其区别,而更重要的是关注其在多态中的应用。
在系列中,我们先后都有关于这一话题的点滴论述,本文以专题的形式再次做以深度讨论,相关的内容请对前文做以参考。
2. 认识覆写和重载
从一个示例开始来认识什么是覆写,什么是重载?
{
//定义虚方法
public virtual void MyFunc()
{
}
//参数列表不同,virtual不足以区分
public virtual void MyFunc(string str)
{
}
//参数列表不同,返回值不同
public bool MyFunc(string str, int id)
{
Console.WriteLine("AAA");
return true;
}
//参数列表不同表现为个数不同,或者相同位置的参数类型不同
public bool MyFunc(int id, string str)
{
Console.WriteLine("BBB");
return false;
}
//泛型重载,允许参数列表相同
public bool MyFunc<T>(string str, int id)
{
return true;
}
//定义抽象方法
public abstract void Func();
}
class Derived: Base
{
//阻隔父类成员
public new void MyFunc()
{
}
//覆写基类成员
public override void MyFunc(string str)
{
//在子类中访问父类成员
base.MyFunc(str);
}
//覆写基类抽象方法
public override void Func()
{
//实现覆写方法
}
}
2.1 覆写基础篇
覆写,又称重写,就是在子类中重复定义父类方法,提供不同实现,存在于有继承关系的父子关系。当子类重写父类的虚函数后,父类对象就可以根据根据赋予它的不同子类指针动态的调用子类的方法。从示例的分析,总结覆写的基本特征包括:
- 在.NET中只有以virtual和abstract标记的虚方法和抽象方法才能被直接覆写。
- 覆写以关键字override标记,强调继承关系中对基类方法的重写。
- 覆写方法要求具有相同的方法签名,包括:相同的方法名、相同的参数列表和相同的返回值类型。
虚方法就是以virtual关键字修饰并在一个或多个派生类中实现的方法,子类重写的虚方法则以override关键字标记。虚方法调用,是在运行时确定根据其调用对象的类型来确定调用适当的覆写方法。.NET默认是非虚方法,如果一个方法被virtual标记,则不可再被static、abstrcat和override修饰。
概念:抽象方法
抽象方法就是以abstract关键字修饰的方法,抽象方法可以看作是没有实现体的虚方法,并且必须在派生类中被覆写,如果一个类包括抽象方法,则该类就是一个抽象类。因此,抽象方法其实隐含为虚方法,只是在声明和调用语法上有所不同。abstract和virtual一起使用是错误的。
2.2 重载基础篇
重载,就是在同一个类中存在多个同名的方法,而这些方法的参数列表和返回值类型不同。值得注意的是,重载的概念并非面向对象编程的范畴,从编译器角度理解,不同的参数列表、不同的返回值类型,就意味着不同的方法名。也就是说,方法的地址,在编译期就已经确定,是这一种静态绑定。从示例中,我们总结重载的基本特征包括:
- 重载存在于同一个类中。
- 重载方法要求具有相同的方法名,不同的参数列表,返回值类型可以相同也可以不同(通过operator implicit 可以实现一定程度的返回值重载,不过不值得推荐)。
- .NET 2.0引入泛型技术,使得相同的参数列表、相同的返回值类型的情况也可以构成重载。
3. 在多态中的应用
多态性,简单的说就是“一个接口,多个方法”,具体表现为相同的方法签名代表不同的方法实现,同一操作作用于不同的对象,产生不同的执行结果。在.NET中,覆写实现了运行时的多态性,而重载实现了编译时的多态性。
运行时的多态性,又称为动态联编,通过虚方法的动态调度,在运行时根据实际的调用实例类型决定调用的方法实现,从而产生不同的执行结果。
{
public virtual void MyFunc(string str)
{
Console.WriteLine("{0} in Base", str);
}
}
class Derived: Base
{
//覆写基类成员
public override void MyFunc(string str)
{
Console.WriteLine("{0} in Derived", str);
}
public static void Main()
{
Base B = new Base();
B.MyFunc("Hello");
Derived A = new Derived();
B = A;
B.MyFunc("Morning");
}
}
从结果中可知,对象B两次执行B.MyFunc调用了不同的方法,第一次调用基类方法MyFunc,而第二次调用了派生类方法MyFunc。在执行过程中,对象B先后指向了不同的类的实例,从而动态调用了不同的实例方法,显然这一执行操作并非确定于编译时,而是在运行时根据对象B执行的不同类型来确定的。我们在此不分析虚拟方法的动态调度机制,而只关注通过虚方法覆写而实现的多态特性,详细的实现机制请参考本系列的其它内容。
编译时的多态性,又称为静态联编,一般包括方法重载和运算符重载。对于非虚方法来说,在编译时通过方法的参数列表和返回值类型决定不同操作,实现编译时的多态性。例如,在实际的开发过程中,.NET开发工具Visual Studio的智能感知功能就很好的为方法重载提供了很好的交互手段,例如:
从智能感知中可知方法MyFunc在派生类Derived中有三次重载,调用哪种方法由程序开发者根据其参数、返回值的不同而决定。由此可见,方法重载是一种编译时的多态,对象A调用哪种方法在编译时就已经确定。
4. 比较,还是规则
- 如果基访问引用的是一个抽象方法,则将导致编译错误。
{
public abstract void Func();
}
class Derived: Base
{
//覆写基类抽象方法
public override void Func()
{
base.Func();
}
}
- 虚方法不能是静态的、密封的。
- 覆写实现的多态确定于运行时,因此更加的灵活和抽象;重载实现的多态确定于编译时,因此更加的简单和高效。二者各有特点与应用,不可替代。
在下表中,将覆写与重载做以总结性的对比,主要包括:
规则 |
覆写(override) |
重载(overload) |
存在位置 |
存在于有继承关系的不同类中 |
存在于同一个类中 |
调用机制 |
运行时确定 |
编译时确定 |
方法名 |
必须相同 |
必须相同 |
参数列表 |
必须相同 |
必须不同 |
返回值类型 |
必须相同 |
可以不相同 |
泛型方法 |
可以覆写 |
可以重载 |
注:参数列表相同表示参数的个数相同,并且相同位置的参数类型也相同。
5. 结论
深入的理解覆写和重载,是对多态特性和面向对象机制的有力补充,本文从基本概念到应用领域将两个概念进行一一梳理,通过对比整理区别,还覆写和重载以更全面的认知角度,同时也更能从侧面深入的了解运行时多态与编译时多态的不同情况。
参考文献
(web)TerryLee, 再谈重载与覆写,http://www.cnblogs.com/Terrylee/archive/2006/03/10/347104.html
(web)失落的BLOGS,C#泛型,http://www.cnblogs.com/lianyonglove/archive/2007/07/27/720682.html