前言:我们都知道面向对象的三大特性:封装,继承,多态。封装和继承对于初学者而言比较好理解,但要理解多态,尤其是深入理解,初学者往往存在有很多困惑,为什么这样就可以?有时候感觉很不可思议,由此,面向对象的魅力体现了出来,那就是多态,多态用的好,可以提高程序的扩展性。常用的设计模式,比如简单工厂设计模式,核心就是多态。
其实多态就是:允许将子类类型的指针赋值给父类类型的指针。也就是同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在运行时,可以通过指向基类的指针,来调用实现派生类中的方法。如果这边不理解可以先放一放,先看下面的事例,看完之后再来理解这句话,就很容易懂了。 理解多态之前首先要对面向对象的里氏替换原则和开放封闭原则有所了解。
里氏替换原则(Liskov Substitution Principle):派生类(子类)对象能够替换其基类(超类)对象被使用。通俗一点的理解就是“子类是父类”,举个例子,“男人是人,人不一定是男人”,当需要一个父类类型的对象的时候可以给一个子类类型的对象;当需要一个子类类型对象的时候给一个父类类型对象是不可以的!
开放封闭原则(Open Closed Principle):封装变化、降低耦合,软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。因此,开放封闭原则主要体现在两个方面:对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。
对这两个原则有一定了解之后就能更好的理解多态。
我们都知道,喜鹊(Magpie)、老鹰(Eagle)、企鹅(Penguin)都是属于鸟类,我们可以根据这三者的共有特性提取出鸟类(Bird)做为父类,喜鹊喜欢吃虫子,老鹰喜欢吃肉,企鹅喜欢吃鱼。
创建基类Bird如下,添加一个虚方法Eat():
/// <summary>
/// 鸟类:父类
/// </summary>
public class Bird
{
/// <summary>
/// 吃:虚方法
/// </summary>
public virtual void Eat()
{
Console.WriteLine("我是一只小小鸟,我喜欢吃虫子~");
}
}
创建子类Magpie如下,继承父类Bird,重写父类Bird中的虚方法Eat():
/// <summary>
/// 喜鹊:子类
/// </summary>
public class Magpie:Bird
{
/// <summary>
/// 重写父类中Eat方法
/// </summary>
public override void Eat()
{
Console.WriteLine("我是一只喜鹊,我喜欢吃虫子~");
}
}
创建一个子类Eagle如下,继承父类Bird,重写父类Bird中的虚方法Eat():
/// <summary>
/// 老鹰:子类
/// </summary>
public class Eagle:Bird
{
/// <summary>
/// 重写父类中Eat方法
/// </summary>
public override void Eat()
{
Console.WriteLine("我是一只老鹰,我喜欢吃肉~");
}
}
创建一个子类Penguin如下,继承父类Bird,重写父类Bird中的虚方法Eat():
/// <summary>
/// 企鹅:子类
/// </summary>
public class Penguin:Bird
{
/// <summary>
/// 重写父类中Eat方法
/// </summary>
public override void Eat()
{
Console.WriteLine("我是一只小企鹅,我喜欢吃鱼~");
}
}
到此,一个基类,三个子类已经创建完毕,接下来我们在主函数中来看下多态是怎样体现的。
static void Main(string[] args)
{
//创建一个Bird基类数组,添加基类Bird对象,Magpie对象,Eagle对象,Penguin对象
Bird[] birds = {
new Bird(),
new Magpie(),
new Eagle(),
new Penguin()
};
//遍历一下birds数组
foreach (Bird bird in birds)
{
bird.Eat();
}
Console.ReadKey();
}
运行结果:
由此可见,子类Magpie,Eagle,Penguin对象可以赋值给父类对象,也就是说父类类型指针可以指向子类类型对象,这里体现了里氏替换原则。
父类对象调用自己的Eat()方法,实际上显示的是父类类型指针指向的子类类型对象重写父类Eat后的方法。这就是多态。
多态的作用到底是什么呢? 其实多态的作用就是把不同的子类对象都当作父类来看,可以屏蔽不同子类对象之间的差异,写出通用的代码,做出通用的编程,以适应需求的不断变化。 以上程序也体现了开放封闭原则,如果后面的同事需要扩展我这个程序,还想再添加一个猫头鹰(Owl),很容易,只需要添加一个Owl类文件,继承Bird,重写Eat()方法,添加给父类对象就可以了。至此,该程序的扩展性得到了提升,而又不需要查看源代码是如何实现的就可以扩展新功能。这就是多态带来的好处。
还是刚才的例子,我们发现Bird这个父类,我们根本不需要使用它创建的对象,它存在的意义就是供子类来继承。所以我们可以用抽象类来优化它。 我们把Bird父类改成抽象类,Eat()方法改成抽象方法。代码如下:
/// <summary>
/// 鸟类:基类
/// </summary>
public abstract class Bird
{
/// <summary>
/// 吃:抽象方法
/// </summary>
public abstract void Eat();
}
抽象类Bird内添加一个Eat()抽象方法,没有方法体。也不能实例化。 其他类Magpie,Eagle,Penguin代码不变,子类也是用override关键字来重写父类中抽象方法。 Main主函数中Bird就不能创建对象了,代码稍微修改如下:
static void Main(string[] args)
{
//创建一个Bird基类数组,添加 Magpie对象,Eagle对象,Penguin对象
Bird[] birds = {
new Magpie(),
new Eagle(),
new Penguin()
};
//遍历一下birds数组
foreach (Bird bird in birds)
{
bird.Eat();
}
Console.ReadKey();
}
执行结果:
由此可见,我们选择使用虚方法实现多态还是抽象类抽象方法实现多态,取决于我们是否需要使用基类实例化的对象.
比如说 现在有一个Employee类作为基类,ProjectManager类继承自Employee,这个时候我们就需要使用虚方法来实现多态了,因为我们要使用Employee创建的对象,这些对象就是普通员工对象。 再比如说 现在有一个Person类作为基类,Student,Teacher 类继承Person,我们需要使用的是Student和Teacher创建的对象,根本不需要使用Person创建的对象, 所以在这里Person完全可以写成抽象类。
总而言之,是使用虚方法,或者抽象类抽象方法实现多态,视情况而定,什么情况?以上我说的两点~
接下来~~~~
我要问一个问题,喜鹊和老鹰都可以飞,这个飞的能力,我怎么来实现呢?
XXX答:“在父类Bird中添加一个Fly方法不就好了~~”
我再问:“好的,照你说的,企鹅继承父类Bird,但是不能企鹅不能飞啊,这样在父类Bird中添加Fly方法是不是不合适呢?”
XXX答:“那就在能飞的鸟类中分别添加Fly方法不就可以了吗?”
对,这样是可以,功能完全可以实现,可是这样违背了面向对象开放封闭原则,下次我要再扩展一个鸟类比如猫头鹰(Owl),我还要去源代码中看下Fly是怎么实现的,然后在Owl中再次添加Fly方法,相同的功能,重复的代码,这样是不合理的,程序也不便于扩展;
其次,如果我还要添加一个飞机类(Plane),我继承Bird父类,合适吗?
很显然,不合适!所以我们需要一种规则,那就是接口了,喜鹊,老鹰,飞机,我都实现这个接口,那就可以飞了,而企鹅我不实现这个接口,它就不能飞~~
添加一个接口IFlyable,代码如下:
/// <summary>
/// 飞 接口
/// </summary>
public interface IFlyable
{
void Fly();
}
喜鹊Magpie实现IFlyable接口,代码如下:
/// <summary>
/// 喜鹊:子类,实现IFlyable接口
/// </summary>
public class Magpie:Bird,IFlyable
{
/// <summary>
/// 重写父类Bird中Eat方法
/// </summary>
public override void Eat()
{
Console.WriteLine("我是一只喜鹊,我喜欢吃虫子~");
}
/// <summary>
/// 实现 IFlyable接口方法
/// </summary>
public void Fly()
{
Console.WriteLine("我是一只喜鹊,我可以飞哦~~");
}
}
老鹰Eagle实现IFlyable接口,代码如下:
/// <summary>
/// 老鹰:子类实现飞接口
/// </summary>
public class Eagle:Bird,IFlyable
{
/// <summary>
/// 重写父类Bird中Eat方法
/// </summary>
public override void Eat()
{
Console.WriteLine("我是一只老鹰,我喜欢吃肉~");
} /// <summary>
/// 实现 IFlyable接口方法
/// </summary>
public void Fly()
{
Console.WriteLine("我是一只老鹰,我可以飞哦~~");
}
}
在Main主函数中,创建一个IFlyable接口数组,代码实现如下:
static void Main(string[] args)
{
//创建一个IFlyable接口数组,添加 Magpie对象,Eagle对象
IFlyable[] flys = {
new Magpie(),
new Eagle()
};
//遍历一下flys数组
foreach (IFlyable fly in flys)
{
fly.Fly();
}
Console.ReadKey();
}
执行结果:
由于企鹅Penguin没有实现IFlyable接口,所以企鹅不能对象不能赋值给IFlyable接口对象,所以企鹅,不能飞~
好了,刚才我提到了飞机也能飞,继承Bird不合适的问题,现在有了接口,这个问题也可以解决了。如下,我添加一个飞机Plane类,实现IFlyable接口,代码如下:
/// <summary>
/// 飞机类,实现IFlyable接口
/// </summary>
public class Plane:IFlyable
{
/// <summary>
/// 实现接口方法
/// </summary>
public void Fly()
{
Console.WriteLine("我是一架飞机,我也能飞~~");
}
}
在Main主函数中,接口IFlyable数组,添加Plane对象:
class Program
{
static void Main(string[] args)
{
//创建一个IFlyable接口数组,添加 Magpie对象,Eagle对象,Plane对象
IFlyable[] flys = {
new Magpie(),
new Eagle(),
new Plane()
};
//遍历一下flys数组
foreach (IFlyable fly in flys)
{
fly.Fly();
}
Console.ReadKey();
}
}
执行结果:
由此,可以看出用接口实现多态程序的扩展性得到了大大提升,以后不管是再扩展一个蝴蝶(Butterfly),还是鸟人(Birder)创建一个类,实现这个接口,在主函数中添加该对象就可以了。 也不需要查看源代码是如何实现的,体现了开放封闭原则!
接口充分体现了多态的魅力~~
以上通过一些小的事例,给大家介绍了面向对象中三种实现多态的方式,或许有人会问,在项目中怎么使用多态呢?多态的魅力在项目中如何体现? 那么接下来我做一个面向对象的简单计算器,来Show一下多态在项目中使用吧!
加减乘除运算,我们可以根据共性提取出一个计算类,里面包含两个属性 Number1和Number2,还有一个抽象方法Compute();代码如下:
/// <summary>
/// 计算父类
/// </summary>
public abstract class Calculate
{
public int Number1
{
get;
set;
}
public int Number2
{
get;
set;
}
public abstract int Compute();
}
接下来,我们添加一个加法器,继承计算Calculate父类:
/// <summary>
/// 加法器
/// </summary>
public class Addition : Calculate
{
/// <summary>
/// 实现父类计算方法
/// </summary>
/// <returns>加法计算结果</returns>
public override int Compute()
{
return Number1 + Number2;
}
}
再添加一个减法器,继承计算Calculate父类:
/// <summary>
/// 减法器
/// </summary>
public class Subtraction : Calculate
{
/// <summary>
/// 实现父类计算方法
/// </summary>
/// <returns>减法计算结果</returns>
public override int Compute()
{
return Number1 - Number2;
}
}
在主窗体FormMain中,编写计算事件btn_Compute_Click,代码如下:
private void btn_Compute_Click(object sender, EventArgs e)
{
//获取两个参数
int number1 = Convert.ToInt32(this.txt_Number1.Text.Trim());
int number2 = Convert.ToInt32(this.txt_Number2.Text.Trim());
//获取运算符
string operation = cbb_Operator.Text.Trim();
//通过运算符,返回父类类型
Calculate calculate = GetCalculateResult(operation);
calculate.Number1 = number1;
calculate.Number2 = number2;
//利用多态,返回运算结果
string result = calculate.Compute().ToString();
this.lab_Result.Text = result;
}
/// <summary>
/// 通过运算符,返回父类类型
/// </summary>
/// <param name="operation"></param>
/// <returns></returns>
private Calculate GetCalculateResult(string operation)
{
Calculate calculate = null;
switch (operation)
{
case "+":
calculate = new Addition();
break;
case "-":
calculate = new Subtraction();
break;
}
return calculate;
}
在该事件中主要调用GetCalculateResult方法,通过运算符,创建一个对应的加减乘除计算器子类,然后赋值给父类,其实这就是设计模式中的简单工厂设计模式,我给你一个运算符你给我生产一个对应的加减乘除计算器子类,返回给我。。其实大多数的设计模式的核心就是多态,掌握好多态,设计模式看起来也很轻松。
现阶段工作已经完成,但是过了一段时间,又添加新的需求了,我还要扩展一个乘法了,那好,很简单只要创建一个乘法计算器继承Calculate父类即可,看代码:
/// <summary>
/// 乘法计算器
/// </summary>
public class Multiplication:Calculate
{
public override int Compute()
{
return Number1*Number2;
}
}
然后在GetCalculateResult函数中添加一个case 就好了:
switch (operation)
{
case "+":
calculate = new Addition();
break;
case "-":
calculate = new Subtraction();
break;
case "*":
calculate = new Multiplication();
break;
}
执行结果:
好了,就这么方便,一个新的功能就扩展完毕了,我根本不需要查看源代码是如何实现的,这就是多态的好处!
另外举例说明:
我再来补充一点。如果楼主熟悉设计或者自己写过框架,虚方法还有种作用就是当做扩展点来使用。比如有一个业务流程,需要执行a b c三个步骤,有很多其它的流程也是执行这三个步骤。但只是B步骤执行的功能不一样。这时可以把流程抽像出来。然后把B方法设置成虚方法。这样其它类继承这个流程类就可以随意改变B步骤的内容并保持原有的流程不变.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
public class Flow
{
public void A()
{
//执行步骤A
}
public virtual void B()
{
//执行步骤B
}
public void C()
{
//执行步骤C
}
public void Run()
{
A();
B(); //步骤B是扩展点 ,可以由子类决定具体执行什么
C();
}
}
public class FlowA:Flow
{
public override void B()
{
//执行FlowA的B步骤
}
}
public class FlowB : Flow
{
public override void B()
{
//执行FlowB的B步骤
}
}
FlowB flowB = new FlowB();
flowB.Run(); //复用了Flow的流程并采用了自己的B步骤
|
1、virtual方法表示此方法可以被重写, 也就是说这个方法具有多态.父类中的方法是通用方法,可以在子类中重写以重新规定方法逻辑.
2、virtual方法可以直接使用,和普通方法一样
3、不是必须重写的. 子类可以使用base.方法 的方式调用, 无论有没有在子类使用override去重写
virtual关键字只是明确标示此方法可以被重写, 其实它和一般的方法没有什么区别
相应的
sealed关键字标示此方法不可以被重写
1.C#类和接口的区别 接口是负责功能的定义,项目中通过接口来规范类,操作类以及抽象类的概念! 而类是负责功能的具体实现! 在类中也有抽象类的定义,抽象类与接口的区别在于: 抽象类是一个不完全的类,类里面有抽象的方法,属性,也可以有具体的方法和属性,需要进一步的专业化。 但接口是一个行为的规范,里面的所有东西都是抽象的! 一个类只可以继承一个基类也就是父类,但可以实现多个接口 PS:接口除了规范一个行为之外,在具体项目中的实际作用也是十分重要的,在面向对象的设计原则以及设计模式的使用中,无不体现作为一个接口的使用好处,最直接的就是设计原则中OCP(开放封闭原则),我们使用接口,而不需要关心他的具体实现,具体实现的细节变化也无关客户端(使用接口的类)的使用,对与扩展是开放的,我们可以另写一个接口的实现来扩展当前程序,而不影响上层的使用,但对修改是封闭的,即我们不能够再去修改接口的定义,当然这个“不能够”是指在规范原则上不应该这么做!
2.抽象类和接口的区别 答: 抽象类(abstract class)可以包含功能定义和实现,接口(interface)只能包含功能定义 抽象类是从一系列相关对象中抽象出来的概念, 因此反映的是事物的内部共性;接口是为了满足外部调用而定义的一个功能约定, 因此反映的是事物的外部特性 分析对象,提炼内部共性形成抽象类,用以表示对象本质,即“是什么” 为外部提供调用或功能需要扩充时优先使用接口
3. C#语言中,值类型和引用类型有何不同?
解答 值类型和引用类型的区别在于,值类型的变量直接存放实际的数据,而引用类型的变量存放的则是数据的地址,即对象的引用。 值类型变量直接把变量的值保存在堆栈中,引用类型的变量把实际数据的地址保存在堆栈中,而实际数据则保存在堆中。注意,堆和堆栈是两个不同的概念,在内存中的存储位置也不相同,堆一般用于存储可变长度的数据,如字符串类型;而堆栈则用于存储固定长度的数据,如整型类型的数据int(每个int变量占用四个字节)。由数据存储的位置可以得知,当把一个值变量赋给另一个值变量时,会在堆栈中保存两个完全相同的值;而把一个引用变量赋给另一个引用变量,则会在堆栈中保存对同一个堆位置的两个引用,即在堆栈中保存的是同一个堆的地址。在进行数据操作时,对于值类型,由于每个变量都有自己的值,因此对一个变量的操作不会影响到其它变量;对于引用类型的变量,对一个变量的数据进行操作就是对这个变量在堆中的数据进行操作,如果两个引用类型的变量引用同一个对象,实际含义就是它们在堆栈中保存的堆的地址相同,因此对一个变量的操作就会影响到引用同一个对象的另一个变量。
4.结构和类的区别
解答 1) 结构是一个值类型,保存在栈上,而类是一个引用类型,保存在受管制的堆上。 2) 对结构中的数据进行操作比对类或对象中的数据进行操作速度要快。 3) 一般用结构存储多种类型的数据,当创建一个很多类或对象共用的小型对象时,使用结构效率更高。 4.抽象方法和虚方法的区别 抽象方法 使用abstract关键字 public abstract bool Withdraw(…); 抽象方法是必须被派生类覆写的方法。 抽象方法是可以看成是没有实现体的虚方法 如果类中包含抽象方法,那么类就必须定义为抽象类,不论是否还包含其它一般方法
虚方法 使用virtual关键字 public virtual bool Withdraw(…); 调用虚方法,运行时将确定调用对象是什么类的实例,并调用适当的覆写的方法。 虚方法可以有实现体
---------------------------------------------------------------------------------------------
虚拟方法和抽象方法有什么区别?
抽象方法只有声明没有实现,需要在子类中实现;虚拟方法有声明和实现,并且可以在子类中覆盖,也可以不覆盖使用父类的默认实现
虚拟方法有实现代码
抽象方法则没有,
并且抽象类不能被实例化,只能实例化实现了全部抽象方法的派生类
抽象方法是虚拟方法的一种 抽象方法没有实现,它的存在只是为派生类统一接口;派生类应该实现这个方法 如果编写一个基类,它永远不会被实现,那么就应该将这个类中的一个或多个方法定义为 抽象方法。
抽象方法只有声明没有实现,需要在子类中实现;虚拟方法有声明和实现,并且可以在子类中覆盖,也可以不覆盖使用父类的默认实现
补充一点
只允许在抽象类中使用抽象方法声明
学习
其实大家首先要搞清楚,虚方法与多态性关系密切,虚方法允许派生类完全或部分重写该类的方法,需写方法体。抽象类中可以包含抽象方法与一般的方法,抽象类不可以new,抽象方法只是一个定义,没有方法体,也就是没有{},也不要在里面写内容。它们两个相像的一点是都用override重写