比起前面的封装和继承,多态这个概念不是那么好理解。我们还是从一个事例开始:
公司最近为了陶冶情操,养了几种动物(Animal),有猫(Cat)、狗(Dog)、羊(Sheep),这些动物都有共同的特性,会吃(Eat)、会叫(Shout),但是它们吃的不同,叫的也不同。既然这样,我们能不能设计一个动物类(Animal)和它的成员(Eat方法、Shout方法)来表示这些动物的共同特征,而当我们关注猫时,猫来实现这两个成员(吃鱼、喵喵叫);当我们关注狗时,狗来实现这两个成员(吃肉和汪汪叫)。
1.什么是多态?
上述例子就是一个典型的多态,就是父类的一些成员,子类继承后去重写从而实现不同的功能。
多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。这就是多态,这种特性称为多态性。
2.多态的分类
多态性分为两种,一种是编译时的多态性,一种是运行时的多态性。
编译时的多态性(重载):编译时的多态性是通过重载来实现的。对于非虚的成员来说,系统在编译时,根据传递的参数、返回的类型等信息决定实现何种操作。
运行时的多态性(重写):运行时的多态性就是指直到系统运行时,才根据实际情况决定实现何种操作。C#中运行时的多态性是通过覆写虚成员实现。
3.多态的实现
我们知道多态有两种,一种是编译时通过重载实现,另一种是运行时,通过重写或叫覆写来实现,那么如何实现他们?
3.1编译时多态:重载(overload)
重载(overload):重载指的是同一个类中有两个或多个名字相同但是参数(参数签名)不同的方法,(注:返回值不能区别函数是否重载),重载没有关键字。
注意:
A.从重载的定义来看,重载是一种编译时多态
B.重载不需要事先定义可重载的方法,即没有关键字
C.重载只是针对一个类内部的几个参数不同,名称相同的方法。
我们还是用那几只陶冶情操的动物来示例说明,代码如下:
1 /// <summary> 2 /// 狗(多态:重载事例) 3 /// </summary> 4 class Dog 5 { 6 /// <summary> 7 /// 叫 8 /// </summary> 9 public void Shout() 10 { 11 Console.WriteLine("汪!"); 12 } 13 14 /// <summary> 15 /// 叫(重载方法) 16 /// </summary> 17 public void Shout(int count) 18 { 19 int i = 0; 20 string shout = ""; 21 do 22 { 23 shout += "汪!"; 24 i++; 25 } while (i <= count); 26 Console.WriteLine(shout); 27 } 28 }
/调用 Dog dog = new Dog(); dog.Shout(); dog.Shout(5);
3.2运行时多态:重写
重写有两种,一种是override修饰符,另一种使用new修饰符,下面会举例说明两种重写的使用方法和异同。
重写(override):也称过载,重写是指子类对父类中虚函数或抽象函数的“覆盖”(这也就是有些书将过载翻译为覆盖的原因),但是这种“覆盖”和用new关键字来覆盖是有区别的。
1 /// <summary> 2 /// 动物类(父类) 3 /// </summary> 4 class Animal 5 { 6 /// <summary> 7 /// 名字 8 /// 说明:类和子类可访问 9 /// </summary> 10 protected string name; 11 12 13 /// <summary> 14 /// 构造函数 15 /// </summary> 16 /// <param name="name"></param> 17 public Animal(string name) 18 { 19 this.name=name; 20 } 21 22 /// <summary> 23 /// 名字(虚属性) 24 /// </summary> 25 public virtual string MyName 26 { 27 get { return this.name; } 28 29 } 30 31 /// <summary> 32 /// 吃(虚方法) 33 /// </summary> 34 public virtual void Eat() 35 { 36 Console.WriteLine("我会吃!"); 37 } 38 39 /// <summary> 40 /// 叫(虚方法) 41 /// </summary> 42 public virtual void Shout() 43 { 44 Console.WriteLine("我会叫!"); 45 } 46 } 47 48 /// <summary> 49 /// 狗(子类) 50 /// </summary> 51 class Dog:Animal 52 { 53 string myName; 54 public Dog(string name): base(name) 55 { 56 myName = name; 57 } 58 59 /// <summary> 60 /// 名字(重写父类属性) 61 /// </summary> 62 public override string MyName 63 { 64 get { return "我是:狗狗,我叫:"+this.name; } 65 66 } 67 68 69 /// <summary> 70 /// 吃(重写父类虚方法) 71 /// </summary> 72 public override void Eat() 73 { 74 Console.WriteLine("我喜欢吃肉!"); 75 } 76 77 /// <summary> 78 /// 叫(重写父类方法) 79 /// </summary> 80 public override void Shout() 81 { 82 Console.WriteLine("汪!汪!汪!"); 83 } 84 }
//调用方法 Animal dog = new Dog("旺财"); string myName=dog.MyName; Console.WriteLine(myName); dog.Eat(); dog.Shout();
//运行结果如下: 我是:狗狗,我叫:旺财 我喜欢吃肉! 汪!汪!汪!
重写(new)
new:覆盖指的是不同类中(基类或派生类)有两个或多个返回类型、方法名、参数都相同,但是方法体不同的方法。但是这种覆盖是一种表面上的覆盖,所以也叫隐藏,被覆盖的父类方法是可以调用得到的。
1 /// <summary> 2 /// 动物类(父类) 3 /// </summary> 4 class Animal 5 { 6 /// <summary> 7 /// 名字 8 /// 说明:类和子类可访问 9 /// </summary> 10 protected string name; 11 12 13 /// <summary> 14 /// 构造函数 15 /// </summary> 16 /// <param name="name"></param> 17 public Animal(string name) 18 { 19 this.name=name; 20 } 21 22 /// <summary> 23 /// 名字(虚属性) 24 /// </summary> 25 public virtual string MyName 26 { 27 get { return this.name; } 28 29 } 30 31 /// <summary> 32 /// 吃(虚方法) 33 /// </summary> 34 public virtual void Eat() 35 { 36 Console.WriteLine("我会吃!"); 37 } 38 39 /// <summary> 40 /// 叫(虚方法) 41 /// </summary> 42 public virtual void Shout() 43 { 44 Console.WriteLine("我会叫!"); 45 } 46 } 47 48 /// <summary> 49 /// 狗(子类) 50 /// </summary> 51 class Dog:Animal 52 { 53 string myName; 54 public Dog(string name): base(name) 55 { 56 myName = name; 57 } 58 /// <summary> 59 /// 名字(重写父类属性) 60 /// </summary> 61 public override string MyName 62 { 63 get { return "我是:狗狗,我叫:"+this.name; } 64 } 65 66 /// <summary> 67 /// 吃(重写父类虚方法) 68 /// </summary> 69 new public void Eat() 70 { 71 Console.WriteLine("我喜欢吃肉!"); 72 } 73 74 /// <summary> 75 /// 叫(重写父类方法) 76 /// </summary> 77 public new void Shout() 78 { 79 Console.WriteLine("汪!汪!汪!"); 80 } 81 }
//调用方法 使用new重写,则只调用父类的方法 Animal dog = new Dog("旺财"); string myName=dog.MyName; Console.WriteLine(myName); dog.Eat(); dog.Shout(); //执行结果如下: 我是:狗狗,我叫:旺财 我会吃! 我会叫!
如下改一下调用方法:
//调用方法 Dog dog = new Dog("旺财"); string myName=dog.MyName; Console.WriteLine(myName); dog.Eat(); dog.Shout(); //执行结果如下: 我是:狗狗,我叫:旺财! 我爱吃肉! 汪!汪!汪!
可以看出,当派生类Dog的Eat()方法使用new修饰时,Dog的对象转换为Animal对象后,调用的是Animal类中的Eat()方法。其实可以理解为,使用new关键字后,使得Dog中的Eat()方法和Animal中的Eat()方法成为毫不相关的两个方法,只是它们的名字碰巧相同而已。所以, Animal类中的Eat()方法不管用还是不用virtual修饰,也不管访问权限如何,或者是没有,都不会对Dog的Eat()方法产生什么影响(只是因为使用了new关键字,如果Dog类没用从Animal类继承Eat()方法,编译器会输出警告)。
我想这是设计者有意这么设计的,因为有时候我们就是要达到这种效果。严格的说,不能说通过使用new来实现多态,只能说在某些特定的时候碰巧实现了多态的效果。
3.3 要点:
a.多态是面向对象的重要特性之一,指同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。
b.多态分为两种:一种是编译时多态,使用重载实现;另一种是运行时多态,使用重写实现;
c.重写有两种,一种使用override关键词,另一种使用new关键词
d.new重写实际上是对父类方法的隐藏,被覆盖的父类方法可以调用得到。因此new可以重写(或说是隐藏)的父类方法不一定要定义为虚方法或抽象方法。只是如果父类方法是虚方法或抽象方法时会覆盖父类方法,如果不是,则隐藏。
e.重载和覆盖的发生条件:
重载,必然发生在一个类中,函数名相同,参数类型或者顺序不同构成重载,与返回类型无关
重写,必然发生在基类和派生类中,其类函数用virtual修饰,派生类用override修饰
覆盖,在子类中写一个和基类一样名字(参数不同也算)的非虚函数,会让基类中的函数被隐藏,编译后会提示要求使用New关键字 new 修饰
隐藏,在子类中可以通过new 隐藏父类的方法
f.new覆盖与重写、重载的区别:
当子类与父类的参数不同时
当基类函数不是虚函数时,基类函数将被隐藏。(因为子类和基类不在同一范围内,所以不是重载)
当基类函数是虚函数时,基类函数将被隐藏。(因为子类和基类不在同一范围内,所以不是重载;因为参数不同,所以不是重写)
当子类与父类的参数相同时
当基类函数不是虚函数时,基类函数将被隐藏。(因为子类和基类不在同一范围内,所以不是重载,因为基类不是虚函数,所以是隐藏不是重写)
当基类函数是虚函数时,基类函数将被覆盖。(因为子类和基类不在同一范围内,所以不是重载)