相信大家都对面向对象的三个特征封装、继承、多态很熟悉,每个人都能说上一两句,但是大多数都仅仅是知道这些是什么,不知道CLR内部是如何实现的,所以本篇文章主要说说多态性中的一些概念已经内部实现的机理。
一、多态的概念
首先解释下什么叫多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。换句话说,实际上就是同一个类型的实例调用“相同”的方法,产生的结果是不同的。这里的“相同”打上双引号是因为这里的相同的方法仅仅是看上去相同的方法,实际上它们调用的方法是不同的。
说到多态,我们不能免俗的提到下面几个概念:重载、重写、虚方法、抽象方法以及隐藏方法。下面就来一一介绍他们的概念。
1、重载(overload):在同一个作用域(一般指一个类)的两个或多个方法函数名相同,参数列表不同的方法叫做重载,它们有三个特点(俗称两必须一可以):
- 方法名必须相同
- 参数列表必须不相同
- 返回值类型可以不相同
如:
2、重写(override):子类中为满足自己的需要来重复定义某个方法的不同实现,需要用override关键字,被重写的方法必须是虚方法,用的是virtual关键字。它的特点是(三个相同):
public void Sleep() { Console.WriteLine("Animal睡觉"); } public int Sleep(int time) { Console.WriteLine("Animal{0}点睡觉", time); return time; }
- 相同的方法名
- 相同的参数列表
- 相同的返回值。
如:父类中的定义:
子类中的定义:
public virtual void EatFood() { Console.WriteLine("Animal吃东西"); }
子类中的定义:
public override void EatFood() { Console.WriteLine("Cat吃东西"); //base.EatFood(); }
tips:经常有童鞋问重载和重写的区别,而且网络上把这两个的区别作为C#做常考的面试题之一。实际上这两个概念完全没有关系,仅仅都带有一个“重”字。他们没有在一起比较的意义,仅仅分辨它们不同的定义就好了。 |
3、虚方法:即为基类中定义的允许在派生类中重写的方法,使用virtual关键字定义。如:
public virtual void EatFood() { Console.WriteLine("Animal吃东西"); }
注意:虚方法也可以被直接调用。如:
Animal a = new Animal(); a.EatFood();
运行结果:
4、抽象方法:在基类中定义的并且必须在派生类中重写的方法,使用abstract关键字定义。如:
public abstract class Biology { public abstract void Live(); } public class Animal : Biology { public override void Live() { Console.WriteLine("Animal重写的抽象方法"); //throw new NotImplementedException(); } }
注意:抽象方法只能在抽象类中定义,如果不在抽象类中定义,则会报出如下错误:
虚方法和抽象方法的区别是:因为抽象类无法实例化,所以抽象方法没有办法被调用,也就是说抽象方法永远不可能被实现。 |
5、隐藏方法:在派生类中定义的和基类中的某个方法同名的方法,使用new关键字定义。如在基类Animal中有一方法Sleep():
public void Sleep() { Console.WriteLine("Animal Sleep"); }
new public void Sleep() { Console.WriteLine("Cat Sleep"); }
或者为:public new void Sleep() { Console.WriteLine("Cat Sleep"); }
(2)隐藏方法中父类的实例调用父类的方法,子类的实例调用子类的方法。
(3)和上一条对比:重写方法中子类的变量调用子类重写的方法,父类的变量要看这个父类引用的是子类的实例还是本身的实例,如果引用的是父类的实例那么调用基类的方法,如果引用的是派生类的实例则调用派生类的方法。
好了,基本概念讲完了,下面来看一个例子,首先我们新建几个类:
public abstract class Biology { public abstract void Live(); } public class Animal : Biology { public override void Live() { Console.WriteLine("Animal重写的Live"); //throw new NotImplementedException(); } public void Sleep() { Console.WriteLine("Animal Sleep"); } public int Sleep(int time) { Console.WriteLine("Animal在{0}点Sleep", time); return time; } public virtual void EatFood() { Console.WriteLine("Animal EatFood"); } } public class Cat : Animal { public override void EatFood() { Console.WriteLine("Cat EatFood"); //base.EatFood(); } new public void Sleep() { Console.WriteLine("Cat Sleep"); } //public new void Sleep() //{ // Console.WriteLine("Cat Sleep"); //} } public class Dog : Animal { public override void EatFood() { Console.WriteLine("Dog EatFood"); //base.EatFood(); } }
下面来看看需要执行的代码:
class Program { static void Main(string[] args) { //Animal的实例 Animal a = new Animal(); //Animal的实例,引用派生类Cat对象 Animal ac = new Cat(); //Animal的实例,引用派生类Dog对象 Animal ad = new Dog(); //Cat的实例 Cat c = new Cat(); //Dog的实例 Dog d = new Dog(); //重载 a.Sleep(); a.Sleep(23); //重写和虚方法 a.EatFood(); ac.EatFood(); ad.EatFood(); //抽象方法 a.Live(); //隐藏方法 a.Sleep(); ac.Sleep(); c.Sleep(); Console.ReadKey(); } }
首先,我们定义了几个我们需要使用的类的实例,需要注意的是
(1)Biology类是抽象类,无法实例化;
(2)变量ac是Animal的实例,但是指向一个Cat的对象。因为Cat类型是Animal类型的派生类,所以这种转换没有问题。这也是多态性的重点。
下面我们来一步一步的分析:
(1)
//重载 a.Sleep(); a.Sleep(23);
很明显,Animal的变量a调用的两个Sleep方法是重载的方法,第一句调用的是无参数的Sleep()方法,第二句调用的是有一个int 参数的Sleep方法。注意两个Sleep方法的返回值不一样,这也说明了重写的三个特征中的最后一个特征——返回值可以不相同。
运行的结果如下:
(2)
在这一段中,a、ac以及ad都是Animal的实例,但是他们引用的对象不同,a引用的是Animal对象,ac引用的是Cat对象,ad引用的是Dog对象,这个差别会造成执行结果的什么差别呢,请看执行结果:
//重写和虚方法 a.EatFood(); ac.EatFood(); ad.EatFood();
第一句Animal实例,直接调用Animal的虚方法EatFood,没有任何问题。
在第二、三句中,虽然同样是Animal的实例,但是他们分别指向Cat和Dog对象,所以调用的Cat类和Dog类中各自重写的EatFood方法,就像是Cat实例和Dog实例直接调用EatFood方法一样。这个也就是多态性的体现:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。
(3)
//抽象方法 a.Live();
这个比较简单,就是直接重写父类Biology中的Live方法,执行结果如下:
(4)
//隐藏方法 a.Sleep(); ac.Sleep(); c.Sleep();
在分析隐藏方法时要和虚方法、重写相互比较。变量 a 调用 Animal 类的 Sleep 方法以及变量 c 调用 Cat 类的 Sleep 方法没有异议,但是变量 ac 引用的是一个 Cat 类型的对象,它应该调用 Animal 类型的 EatFood 方法呢,还是 Cat 类型的 EatFood 方法呢?答案是调用父类即Animal的EatFood方法。执行结果如下:
大多数的文章都是介绍到这里为止,仅仅是让我们知道这些概念以及调用的方法,而没有说明为什么会这样。下面我们就来深入一点,谈谈多态背后的机理。
二、深入理解多态性
要深入理解多态性,就要先从值类型和引用类型说起。我们都知道值类型是保存在线程栈上的,而引用类型是保存在托管堆中的。因为所有的类都是引用类型,所以我们仅仅看引用类型。
现在回到刚才的例子,Main函数时程序的入口,在JIT编译器将Main函数编译为本地CPU指定时,发现该方法引用了Biology、Animal、Cat、Dog这几个类,所以CLR会创建几个实例来表示这几个类型本身,我们把它称之为“类型对象”。该对象包含了类中的静态字段,以及包含类中所有方法的方法表,还包含了托管堆中所有对象都要有的两个额外的成员——类型对象指针(Type Object Point)和同步块索引(sync Block Index)。
可能上面这段对于有些没有看过相关CLR书籍的童鞋没有看懂,所以我们画个图来描述一下:
上面的这个图是在执行Main函数之前CLR所做的事情,下面开始执行Main函数(方便起见,简化一下Main函数):
//Animal的实例 Animal a = new Animal(); //Animal的实例,引用派生类Cat对象 Animal ac = new Cat(); //Animal的实例,引用派生类Dog对象 Animal ad = new Dog(); a.Sleep(); a.EatFood(); ac.EatFood(); ad.EatFood();
下面实例化三个Animal实例,但是他们实际上指向的分别是Animal对象、Cat对象和Dog对象,如下图:
请注意,变量ac和ad虽然都是Animal类型,但是指向的分别是Cat对象和Dog对象,这里是关键。
当执行a.Sleep()时,由于Sleep是非虚实例方法,JIT编译器会找到发出调用的那个变量(a)的类型(Animal)对应的类型对象(Animal类型对象)。然后调用该类型对象中的Sleep方法,如果该类型对象没有Sleep方法,JIT编译器会回溯类的基类(一直到Object)中查找Sleep方法。
当执行ac.EatFood时,由于EatFood是虚实例方法,JIT编译器调用时会在方法中生成一些额外的代码,这些代码会首先检查发出调用的变量(ac),然后跟随变量的引用地址找到发出调用的对象(Cat对象),找到发出调用的对象对应的类型对象(Cat类型对象),最后在该类型对象中查找EatFood方法。同样的,如果在该类型对象中没有查找到EatFood方法,JIT编译器会回溯到该类型对象的基类中查找。
上面描述的就是JIT编译器在遇到调用类型的非虚实例方法以及虚实例方法时的不同执行方式,也这是处理这两类方法的不同方式造成了表面上我们看到的面向对象的三个特征之一——多态性。
-----
c#中的委托
个人认为,可以从以下2点来理解:
(1) 从数据结构来讲,委托是和类一样是一种用户自定义类型。
(2) 从设计模式来讲,委托(类)提供了方法(对象)的抽象。
委托是方法的抽象,它存储的就是一系列具有相同签名和返回回类型的方法的地址。调用委托的时候,委托包含的所有方法将被执行。
public delegate int MyDelegate (string s);
上面的委托可被用于引用任何一个带有一个单一的 string 参数的方法,并返回一个 int 类型变量
2. 委托定义
delegate void MyDel(int x);
委托类型声明:
(1) 以deleagate关键字开头。
(2)返回类型+委托类型名+参数列表。
3. 声明委托变量
MyDel del1,del2;
4. 初始化委托变量
(1) 使用new运算符
new运算符的操作数的组成如下:
- 委托类型名
- 一组圆括号,其中包含作为调用列表中的第一个成员的方法的名字。方法可以是实例方法或静态方法。
del1 = new MyDel( myInstObj.MyM1 ); del2 = new MyDel( SClass.OtherM2 );
(2)使用快捷语法
快键语法,它仅由方法说明符构成。之所以能这样,是因为在方法名称和其相应的委托类型之间有隐式转换。
del1 = myInstObj.MyM1; del2 = SClass.OtherM2;
5. 赋值委托
由于委托是引用类型,我们可以通过给它赋值来改变包含在委托变量中的方法地址引用。旧的引用会被垃圾回收器回收。
MyDel del; del = myInstaObj.MyM1; //委托初始化 del = SClass.OtherM2;//委托重新赋值,旧的引用将被回收
6. 组合委托
委托可以使用额外的运算符来组合。这个运算最终会创建一个新的委托,其调用列表是两个操作数的委托调用列表的副本的连接。
委托是恒定的,操作数委托创建后不会被改变。委托组合拷贝的是操作数的副本。
MyDel del1 = myObj.MyMethod; MyDel del2 = SClass.OtherM2; MyDel del3 = del1 + del2; //组合调用列表
7. 委托加减运算
可以使用+=运算符,为委托新增方法。
同样可以使用-=运算符,为委托移除方法。
MyDel del = myObj.MyMethod; del += SClass.OtherM2; // 增加方法 del -= myObj.MyMethod; // 移除方法
8. 委托调用
委托调用跟方法调用类似。委托调用后,调用列表的每个方法将会被执行。
在调用委托前,应判断委托是否为空。调用空委托会抛出异常。
if(null != del) { del();//委托调用 }