我们先看下面一段程序:
- public class Father
- {
- public void Run0()
- {
- Console.WriteLine("Father.Run0");
- }
- }
- public class Son:Father
- {
- public void Run0()
- {
- Console.WriteLine("Son.Run0");
- }
- }
- class Program
- {
- static void Main(string[] args)
- {
- Father[] fatherList = new Father[2];
- fatherList[0] = new Father();
- fatherList[1] = new Son();
- fatherList[0].Run0();
- fatherList[1].Run0();
- }
- }
程序的运行结果是:
Father.Run0
Father.Run0
稍微细心的朋友可能发现在Son类的Run0方法下面有一段棕色的波浪线,当我们把鼠标放到该下划线上时,会看到下面的提示(编译程序时在程序的“输出”窗口也能看到这个警告):
“MethodDemo.Son.Run0()”隐藏了继承的成员“MethodDemo.Father.Run0()”。如果是有意隐藏,请使用关键字new。
如图:
然后我们再来第二个版本的Run方法,我们称之为Run1(),,与第一个版本的区别是在子类的同名同参(方法名相同,参数个数和参数顺序相同,下同)方法前面加上了一个new关键字,代码如下:
- public class Father
- {
- public void Run1()
- {
- Console.WriteLine("Father.Run1");
- }
- }
- public class Son:Father
- {
- public new void Run1()
- {
- Console.WriteLine("Son.Run1");
- }
- }
- class Program
- {
- static void Main(string[] args)
- {
- Father[] fatherList = new Father[2];
- fatherList[0] = new Father();
- fatherList[1] = new Son();
- fatherList[0].Run1();
- fatherList[1].Run1();
- }
- }
运行结果如下:
Father.Run1
Father.Run1
我们发现加上new关键字之后,程序的运行结果没有发生改变。也就是在C#中,如果在子类中有与父类同名同参的方法时,C#会隐式帮你在子类的方法前面添加一个new关键字。
我们再写第三个版本的Run方法,即Run2(),与第一个版本不同的是父类的Run2()方面前面加了一个virtual关键字,表示这是一个虚方法,子类的Run2()除了与第一个版本号不同之外(Run0()改成Run2())没有什么不同。
程序代码如下:
- public class Father
- {
- public virtual void Run2()
- {
- Console.WriteLine("Father.Run2");
- }
- }
- public class Son:Father
- {
- public void Run2()
- {
- Console.WriteLine("Son.Run2");
- }
- }
- class Program
- {
- static void Main(string[] args)
- {
- Father[] fatherList = new Father[2];
- fatherList[0] = new Father();
- fatherList[1] = new Son();
- fatherList[0].Run2();
- fatherList[1].Run2();
- }
- }
我们看看程序的运行效果:
Father.Run2
Father.Run2
程序运行效果与第一个仍然没有什么区别,不过这次子类(Son)的Run2()方法又出现了与Run方法的第一个版本(Run0())一样的警告:“MethodDemo.Son.Run2()”将隐藏继承的成员“MethodDemo.Father.Run2()”。若要使当前成员重写该实现,请添加关键字override。否则,添加关键字new。
我们再写第四个版本的Run方法,我们称之为Run3(),这次父类中Run3()的修饰符与第三个版本相比没有变化(依然是virtual,虚方法),而子类Son中的Run3()方法与第三个版本相比多了一个override修饰符(这次我们熟悉的那个棕色的波浪线警告没有了)。代码如下:
- public class Father
- {
- public virtual void Run3()
- {
- Console.WriteLine("Father.Run3");
- }
- }
- public class Son:Father
- {
- public override void Run3()
- {
- Console.WriteLine("Son.Run3");
- }
- }
- class Program
- {
- static void Main(string[] args)
- {
- Father[] fatherList = new Father[2];
- fatherList[0] = new Father();
- fatherList[1] = new Son();
- fatherList[0].Run3();
- fatherList[1].Run3();
- }
- }
程序的运行结果如下:
Father.Run3
Son.Run3
这次我们发现程序的运行结果与前面三次不一样了,这次尽管我们声明的对象类型都是Father类(Father数组装的自然都是Father类型的引用),但是因为实例化数组中第二个元素的时候调用了Son类的构造函数,也就是实例化了一个Father类的子类(我们知道子类可以当作父类来看待,他们之间是is a的关系,反之则不行),也就是说fatherList数组中的第二个元素的引用类型是Father类型,但它的实例类型确实Son类型。而在运行的时候,并不是根据我们的引用类型(引用类型是Father类型的)去调用该引用类型的方法,而是调用该引用类型所指向的实例的方法。
为什么会发生这些现象呢?这里要提到两个概念:早绑定(early binding)和晚绑定(Late binding)。这个术语出现在存在继承关系的基类和派生类中,它们同时定义了一个同名同参的方法。
早绑定:在编译的时候就已经确定了将来程序运行基类或是派生类的哪个方法。在编译代码的时候根据引用类型就决定了运行该引用类型中定义的方法,即基类的方法。这种方法运行效率高。
晚绑定:只有在运行的时候才能决定运行基类或者派生类中的哪个方法。运行的时候将根据该实际类型而不是引用类型来调用相关方法,即取决于我们new了什么样对象。也就是即使我们new一个Father类的子类Son的实例,而不管我们是用Father类的引用指向这个Son的实例,方法调用的时候依然是调用Son的方法,而不是Father类的同名方法。
如我们上面所见,为了实现晚绑定,C#引入了两个关键词virtual和override。和Java中不同,Java中一切方法都是虚方法,也就是在运行的时候,JVM会自动检测该引用的类型与实际类型是否一致(无论如何,该引用类型与实际类型之间存在着相等或者继承关系,这样才满足is a的关系),如果一致执行该类型中定义的方法;如果不一致则会检查该引用的实际类型是否具有同名同参方法,如果有则运行该实际类型的同名同参方法。这样会带来一个问题:每次运行的时候都会进行类型检查,这样会带来一定的性能消耗。而在C#中一切方法如果没有显示指明,都是非虚的。对于非虚的方法,CLR运行的时候并不会进行类型检查,而是直接运行该引用的类型中所定义的方法,即使这个引用所指向的实际类型是该引用类型的派生类,并且在派生类中存在着同名同参的方法,也不会运行派生类中定义的方法。这时,派生类中的方法隐藏了基类中的方法。
但是如果在基类中显示声明方法为虚方法,那么CLR在运行的时候会进行类型检查,如果发现引用类型和实际的对象类型不一致,就会检查派生类中是否覆盖(override)了基类中的方法,如果是,则会运行派生类中的方法,而不是引用类型中的方法。