面向对象的第三个主要特性。
多态性是同一操作作用于不同的类的对象,由不同的类进行不同的解释,最终产生不同的执行效果。
多态性按照产生多态行为的时间分为:编译时多态与运行时多态。
1 编译时多态性
编译时多态是指通过设置不同的方法签名,在编译时由编译器根据方法的签名决定调用何种方法。
通过指定方法的访问级别(例如 public或 private)、可选修饰符(例如 abstract或 sealed)、返回值、名称和任何方法参数,可以在类或结构中声明方法。这些部分统称为方法的“签名”。
使用不同的方法签名实现多态性称为方法重载。为进行方法重载,方法的返回类型不是方法签名的一部分。但是,在确定委托和委托所指向方法之间的兼容性时,返回类型是方法签名的一部分。
安装具体签名的不同可以分为:参数类型或数量重载、输出与输入型参数引用重载。
1.1 参数类型或数量重载
在方法重载时,实参与形参一一对应,如果相应位置类型相同且方法参数数量相等,则完成匹配,即实现同名方法不同参数的重载。
如有下面方法:
void method()
{
Console.WriteLine("is a zero parameter");
}
void method(int i)
{
Console.WriteLine("is a one parameter");
}
void method(int i, double d)
{
Console.WriteLine("is a two parameter");
}
当调用函数
this.method(1, 2.0);
会调用第三个方法,输出:is a two parameter。
1.2 输出与输入型参数引用重载
输入引用从参数使用 ref关键字修饰,在方法调用传递过程中,不同于值传递,传递的是值在内存中的地址,由此,在方法内对参数的修改会作用至实参本身。
输出引用参数使用 out关键字修饰,类似于输入引用参数,但在调用方法前不需要对变量进行初始化。
由于本篇的重点是运行时多态,在对编译时多态介绍比较简单,仅作参考。
2 运行时多态性
运行时多态指在基本类中定义虚方法,在派生类中重新定义方法或覆盖虚方法,在发生对象调用时,由公共语言运行时(CLR)根据对象类型决定调用何种方法。
虚方法允许您以统一方式处理多组相关的对象。由虚方法引出的多态性有多重情形,以下着重介绍3种情形。
2.1 新成员隐藏基类成员
通过 new关键字,在派生类中重新定义一个与基类相同名字方法,在使用该派生类的实例被当作基类的实例访问,会调用基类成员方法。
如下示例:
public class BaseClass
{
public void DoWork()
{
Console.WriteLine("base Class do work");
}
}
public class DerivedClass : BaseClass
{
public new void DoWork()
{
Console.WriteLine("derived Class do work");
}
}
执行以下语句时,
DerivedClass D = new DerivedClass();
D.DoWork();
BaseClass B = (BaseClass)D;
B.DoWork();
会输出如下结果。
derivedClass do work
baseClass do work
程序表面,当派生类DerivedClass实例 D被当做基类BaseClass实例 B调用方法 DoWork()时,调用的是基类 baseClass的DoWork()方法。
2.2 虚成员
使用virtual在基类中定义虚成员方法,使用override在派生类中覆盖虚方法,从而使得在使用该派生类的实例被当作基类的实例访问,会调用派生类成员方法。
如下示例:
public class BaseClass
{
public virtual void DoWork()
{
Console.WriteLine("base Class do work");
}
}
public class DerivedClass : BaseClass
{
public override void DoWork()
{
Console.WriteLine("derived Class do work");
}
}
执行以下语句时,
DerivedClass D = new DerivedClass();
D.DoWork();
BaseClass B = (BaseClass)D;
B.DoWork();
会输出如下结果。
derivedClass do work
derivedClass do work
程序表面,当派生类中对基类方法进行覆盖,派生类的实例 D被当做基类实例 B调用方法时,仍然调用的是派生类DoWork()方法。
2.3 阻止派生类重写虚拟成员
使用关键字sealed阻止派生类重写虚方法,基类至sealed修饰的派生类将继续沿用成员继承机制,而对sealed修饰的派生类再派生时,将使用新成员隐藏基类成员机制。
示例如下:
public class A
{
public virtual void DoWork()
{
Console.WriteLine("a do work");
}
}
public class B : A
{
public override void DoWork()
{
Console.WriteLine("b do work");
}
}
public class C : B
{
public sealed override void DoWork()
{
Console.WriteLine("c do work");
}
}
public class D : C
{
public new void DoWork()
{
Console.WriteLine("d do work");
}
}
执行以下语句时,
D d = new D();
Console.WriteLine("d");
d.DoWork();
C c = (C)d;
Console.WriteLine("c");
c.DoWork();
B b = (B)d;
Console.WriteLine("b");
b.DoWork();
A a = (A)d;
Console.WriteLine("a");
a.DoWork();
会输出如下结果。
d
d do work
c
c do work
b
c do work
a
c do work
程序表明,派生类实例对象d被当做基类实例对象c调用DoWork()方法时,执行的是基类 c的方法;派生类实例对象d被当做基类实例对象b调用DoWork()方法时,执行的是基类 c的方法;派生类实例对象d被当做基类实例对象a调用DoWork()方法时,执行的是基类 c的方法,符合新成员隐藏基成员、虚方法机制。
在此不妨谈谈其具体实现机制,当派生类对象被强制类型转换赋值给基类对象时(这里有个著名的里氏法则,子类都可以向父类转化,反之不行,具体证明我也不会),基类对象是拿着派生类的引用,也就是说地址还指向派生类的对象,当使用override覆盖时,派生类对继承过来的基类成员会重新定义,也就是派生类中的基类副本会发生改变,但这并不会影响基类本身的定义,因而当派生类对象被当做基类对象访问时,是基类调用,但是是派生类内的基类发生调用,因而也就调用的是我们在派生类中定义的覆盖成员;而使用 new定义新的成员并不会改变派生类中基类的副本,因而当派生类对象被当做基类对象访问时,还是由派生类中的基类副本,调用其原本的基类中的成员;而密封不过是阻止了虚函数在派生类中被覆盖,并没有从根本上引入新的机制。由于认知有限,难免让各位见笑了,如有高见,可以留言。
参考
C# 编程指南
C# 自学手册