目录
里氏替换原则(Liskov Substitution Principle):
开放封闭原则(Open Closed Principle):
——————————————————————————————————————————————————
封装、继承、多态,面向对象的三大特性
简要说明继承和多态:
继承和多态是最能直接体现面向对象特性的。
继承说明了类与类之间、类与接口之间的关系。
而多态则是继承功能的扩展,通过多态更能体现继承机制的优势,同时也体现了面向对象的优越性。
继承:
结构如下:
<访问修饰符符> class <基类>
{
...
}
class <派生类> : <基类>
{
...
}
继承的特点:
- 【派生扩展不能移除】派生类是对基类的扩展,可以添加新成员,但不能移除已经继承成员的定义。
- 【继承传递性】继承可以传递,B继承A,C继承B,那么C中也继承了A的方法。
- 【构造函数继承特点】构造函数和析构函数不能被直接继承(但通过base关键字可以继承构造函数),除此之外其他成员能被继承。基类中成员的访问方式【封装修饰词】只能决定派生类能否访问它们。
- 【覆盖访问】派生类如果定义了与继承而来的成员同名的新成员,那么就可以覆盖已继承的成员,但这兵不是删除了这些成员,只是不能再访问这些成员。
- 【虚方法与多态】类可以定义虚方法、虚属性及虚索引指示器,它的派生类能够重载这些成员,从而使类可以展示出多态性。【多态】
- 【非常规多重继承】派生类只能从一个类中继承,可以通过接口来实现多重继承。【java为多继承】
【访问基类base的用法】:
不过有时我们也需要访问基类的某些成员(例如存在多次继承时,base可以完成创建派生类实例时调用其基类构造函数或者调用基类上已被其他方法重写的方法。)
使用base关键字可以访问基类成员,包括基类的属性和方法,但是这些属性和方法必须是公共【public】而非私有类型。
示例:
其中包含:有参数的构造函数、new重写、构造函数/方法/公共变量等基类成员的base访问
namespace 继承和多态TEST
{//从运行结果可以看到很重要的一点,继承时先执行父类构造函数,
//接着再执行子类构造函数,最后再执行方法
//定义父类a
class a
{
int a1 = 1;
public int a2 = 2;
public a()
{//这是父类的构造函数1
a1 += 1;
Console.WriteLine("输出:父类构造函数1----a1: {0}", a1);
}
public a(int a2)
{//这是父类的构造函数2
a1 = a2;
Console.WriteLine("输出:父类构造函数2----a2: {0}", a1);
}
public void Display()
{//这是父类的display方法
Console.WriteLine("输出:这是父类的dispaly方法");
}
//注意,构造函数和方法不一样,前者不需要返回值,后者需要
}
//这是子类b,继承了a,注意不用带括号
class b :a
{
int b1 = 10;
public b()
{//子类构造函数1
Console.WriteLine("输出:子类构造函数1,不带参数");
}
public b(int i) : base()
{//子类构造函数2
Console.WriteLine("输出:子类构造函数2,带参数");
}//直接加base()的意思为先调用基类的构造函数,再执行子类
//#1之后会改成base(1)来进行对比
public void B1()
{//子类方法1
Console.WriteLine("输出:子类方法1");
Console.WriteLine("之后为在派生类中调用的基类方法");
base.Display();//子方法中调用基类方法
}
public void B2()
{//子类方法2
Console.WriteLine("输出:这里输出基类的全局变量" + base.a2);
}
new public void Display()
{
Console.WriteLine("输出:派生类重写后的Display函数");
}
}
class ExecuteRectangle
{
static void Main(string[] args)
{
//这里提供带参数派生类构造函数的使用对比
//结果仍然是先输出父类构造函数,然后再输出子类构造函数
b b0 = new b(2);//在继承时,如果基类构造函数是有参数的,子类构造函数也必须有一个有参数的构造函数,否则会报错
b b = new b();
b.B1();
b.B2();
//派生类访问基类成员两种方法:
//1、调用base.<成员> 调用基类的方法
Console.WriteLine("输出:调用base方法访问基类方法和(公共)变量:" + b.a2);
((a)b).Display();
//2、显示类型转换为父类
Console.WriteLine("输出:使用显示类型转换为父类:" + "a2:"+ ((a)b).a2);
//输出被派生类重写(屏蔽)后的基类方法
Console.WriteLine("之后里是派生类重写后的基类方法dispaly:");
b.Display();
Console.ReadLine();
}
}
}
使用new关键字隐藏(重写)基类成员
通常我们先完成基类的初始化,在派生类中使用继承后的基类属性和方法(不需要在派生类中再次声明)。
但此时如果继承的基类成员不能很好的满足派生类的需要,我们可以重新定义,使用new关键字来重写基类的成员,从而覆盖(隐藏)继承过来的属性或者方法。注意数据类型跟名称不能变。
需要注意的是,派生类的覆盖(重写),不会影响基类。
class SomeClass
{
public string Field1;
}
class OtherClass : SomeClass
{
new public string Field1;
}
密封类与抽象类与【虚方法】
当继承被滥用时,类与类之间的关系会变得很乱导致无法理解,因此使用继承必须要慎重。
如果我们想要一些类不被继承,我们可以使用密封类。
密封类:我们只需在父类前加上sealed修饰符,那这个类将不能被继承了。
密封方法:也是在方法前加上sealed修饰符。
抽象类和密封类刚好相反,它是为继承而生的。抽象类不能实例化,抽象方法没有具体执行代码,必须在非抽象的派生类中重写。也就是基类并不实现任何执行代码,只是进行定义。这一点和接口有相同的地方。
使用方法:
抽象对象(基类和方法)前带abstract
派生对象(派生类与派生方法)前带override
额外:抽象方法【abstract】与虚方法【virtual 】!
有时并不想把类声明为抽象类,但又不想把方法在基类里具体实现,而是在派生类中重写,此时可以使用虚方法
几个注意要点:
- 虚方法和抽象方法的关键字不同,但是其派生类的关键字还是【override】
- 虚方法的类不带【virtual】关键字,但是抽象方法的类必须带【abstract】关键字
- 虚方法必须声明方法主体(即方法体中必须要有代码),而抽象方法中没有方法主体
- 抽象方法没有方法体,子类必须重写方法体!!因此抽象方法可以看成是一个没有方法体的虚方法
- 虚方法的子类可以不重写(因为基类虚方法中已经有了方法体,不重写也可以实现,抽象方法不同)
- 虚方法只能出现在有继承关系的类对象中,且只有类对象的成员函数可以说明为虚方法
- 静态成员函数、内联函数、构造函数不能是虚函数;但是析构函数可以是
实例:关于密封、抽象与虚方法
class Program
{
static void Main(string[] args)
{
Man man = new Man();
man.Eat();
man.Say();
}
}
sealed class C
{
//被密封的类C,不可被继承
}
public abstract class People
{
//注意:如果类中有抽象方法,则类必须声明为抽象类。
public People()
{
Console.WriteLine("父类的构造函数");
}
public abstract void Eat();
//有时候不想把类声明为抽象类,但又想实现方法在基类里不具体实现,
//而是想实现方法由派生类重写。遇到这种情况时可使用virtual关键字将方法声明为虚方法
public virtual void Say()
{
//注意虚方法必须声明方法主体,抽象方法则不需要
Console.WriteLine("我是父类的虚方法");
}
}
class Man:People
{
public Man()
{
Console.WriteLine("子类构造函数");
}
public override void Eat()
{
Console.WriteLine("我是子类");
}
public override void Say()
{
Console.WriteLine("我是子类的Say方法");
}
}
重写和重载的区别
其实二者都是多态的概念,但是一个是静态多态【重载(Overload)】,一个是动态多态【重写(Override)】。
这里见另一篇博文:地址链接
多态:
多态性可以是静态的或动态的。
在静态多态性中,函数的响应是在编译时发生的。主要包括:
- 函数重载
- 运算符重载
- 以上两者都是【重载(Overload)】
在动态多态性中,函数的响应是在运行时发生的。这一般发生于抽象函数的继承和重载上。
动态多态性是通过 抽象类 和 虚方法 实现的 ,此外也有通过接口来实现多态的情况。即主要包括:
- 抽象类抽象方法多态
- 虚方法多态
- 接口多态
- 以上三者都涉及到【重写(Override)】
注:以下部分参考这篇博客。
静态多态性:
通过函数重载实现多态,即:同一范围(譬如同一类)内,有多个相同名称的函数定义(除名称外定义不同),包括:
- 参数列表-参数类型不同
- 参数列表-不同参数类型的前后顺序不同
- 参数列表-参数个数不同
- 注意:单纯返回类型不同的函数不能重载
例:以下代码包含所有情况
namespace 继承和多态TEST
{
class 函数重载
{
void print(int i)
{
Console.WriteLine("输出int: {0}", i);
}
void print(double f)
{
Console.WriteLine("输出float: {0}", f);
}
void print(int s,double d)
{
Console.WriteLine("输出顺序:{0},{1}", s,d);
}
void print(double d,int s)
{
Console.WriteLine("输出顺序: {0},{1}", d, s);
}
static void Main(string[] args)
{
函数重载 p = new 函数重载();
// 调用 print 来打印整数
p.print(5);
// 调用 print 来打印浮点数
p.print(500.263);
// 调用 print 来打印字符串
p.print(5,5.555);
p.print(5.4555, 5);
Console.ReadKey();
}
}
}
结果:
输出int: 5
输出float: 500.263
输出顺序:5,5.555
输出顺序: 5.4555,5
通过运算符来实现多态,我们以一个简单的计算器项目为代表
新建一个项目,在主窗体form1中创建如图:
/// 计算父类Calculate,包含两个属相和一个抽象方法:Compute【复习:抽象方法在基类中不需要实现】
public abstract class Calculate
{
public int Number1
{
get;
set;
}
public int Number2
{
get;
set;
}
public abstract int Compute();
}
/// 加法器Addition ,继承了父类后,对抽象方法进行了【重写】
public class Addition : Calculate
{
public override int Compute()
{
return Number1 + Number2;
}
}
/// 减法器Subtraction ,继承了父类后,对抽象方法进行了【重写】
public class Subtraction : Calculate
{
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.textBox1.Text.Trim());
int number2 = Convert.ToInt32(this.textBox2.Text.Trim());
//获取运算符
string operation = listBox1.Text.Trim();
//通过运算符,返回父类类型
Calculate calculate = GetCalculateResult(operation);
calculate.Number1 = number1;
calculate.Number2 = number2;
//利用多态,返回运算结果
string result = calculate.Compute().ToString();
this.label1.Text = result;
}
/// 在主窗体事件编辑中,添加方法GetCalculateResult,通过运算符,返回父类类型
private Calculate GetCalculateResult(string operation)
{
Calculate calculate = null;
switch (operation)
{
case "+":
calculate = new Addition();
break;
case "-":
calculate = new Subtraction();
break;
}
return calculate;
}
这整个流程即设计模式中的简单工厂设计模式,通过调用GetCalculateResult方法,通过运算符,创建一个对应的加减乘除计算器子类,然后赋值给父类。
如果需求变更,此时只需要再继承父类创建一个新的子类,并在GetCalculateResult()新加一个case即可。
动态多态性:
这里是一个重点。需要了解两个原则:里式替换原则和开放封闭原则(详细见下方)。
现在有三个工程:
- 公司有雇员和项目经理两种,项目经理包含了雇员的责任,请对这种情况进行多态实现【虚方法多态】
- 老师和学生,他们的工作内容截然不同,请进行多态实现【抽象类抽象方法多态】
- 喜鹊吃虫,老鹰吃肉,企鹅吃鱼,但是前两者有飞行能力,后者没有,请进行多态实现【接口多态】
注:接口博文链接
我们现在对三个问题进行分析:
问题是一种包含的关系,项目经理除了雇员的责任外还有自己独有的责任,因此在实现时除了项目经理,我们也要实现雇员,因为我们要创建雇员对象,此时选择虚方法多态。(复习:虚方法自己有自己的方法体,且虚方法的类不用带virtual,方法要带)
问题二中,老师和学生并没有重叠的工作内容,我们可以把他们看作“人类”这个类,这个类没有任何的要求,在实现时我们也不必实例化“人类”对象,因此选择抽象方法。(复习:抽象类要带abstract,方法也要带),但其实我们这里把“人类”定义虚方法的类也可以。两者使用并非完全绝对,只有相对。
问题三只看前部分,我们会发现使用虚方法和抽象方法都可以,且抽象方法会更好一些,但是需求上又增加了“飞行”的部分,因此我们要借助接口,进行多重继承。首先通过设计基类“鸟类”,然后设计“飞行”接口,在派生时看情况继承接口即可。
代码如下:
namespace 继承和多态TEST
{
//这个类是用来示例动态多态性的
/*现在有三个工程:
1、公司有雇员和项目经理两种,项目经理包含了雇员的责任,请对这种情况进行多态实现【虚方法多态】
2、老师和学生,他们的工作内容截然不同,请进行多态实现【抽象类抽象方法多态】
3、喜鹊吃虫,老鹰吃肉,企鹅吃鱼,但是前两者有飞行能力,后者没有,请进行多态实现【接口多态】
分别对其进行实现
*/
class Dynamic_polymorphism
{
static void Main(string[] args)
{
Console.WriteLine("工程一:雇员与项目经理");
Employee[] employees = { new Employee(), new ProjectManager() };
foreach (Employee employee in employees)
{
employee.responsibility();
}
Console.WriteLine("工程二:老师与学生");
Person[] persons = { new Student(), new Teacher() };
foreach(Person person in persons)
{
person.work();
}
Console.WriteLine("工程三:鸟的习性");
//本处与先前不同,分别实例化三种鸟类
Magpie magpie = new Magpie();
Eagle eagle = new Eagle();
Penguin penguin = new Penguin();
magpie.eat();
magpie.fly();
eagle.eat();
eagle.fly();
penguin.eat();
penguin.fly();
//结束
Console.ReadLine();
}
}
//以下为工程一:雇员Employee与项目经理ProjectManager,有方法:【责任】responsibility
class Employee
{
public virtual void responsibility()
{ Console.WriteLine("我是被剥削的雇员,责任较小"); }
}
class ProjectManager : Employee
{
public override void responsibility()
{
Console.WriteLine("我是项目经理,责任重大");
}
}
//以下为工程二:学生老师与人类
abstract class Person
{
public abstract void work();
}
class Student : Person
{
public override void work()
{
Console.WriteLine("学生作业为重");
}
}
class Teacher : Person
{
public override void work()
{
Console.WriteLine("老师教授课程");
}
}
//以下为工程三:鸟基类、喜鹊Magpie、老鹰Eagle、企鹅Penguin、接口【飞行】IFlyable,使用抽象方法和接口作多态
public abstract class Bird
{
public abstract void eat();
}
public interface IFlyable//接口
{
//提供一个飞行的能力
//复习:接口成员,可以在其中添加参数和返回值,但不能在接口中实现方法,仅仅作为“目录”的功能
void fly();
}
class Magpie : Bird, IFlyable
{
public override void eat()
{
Console.WriteLine("喜鹊吃虫");
}
public void fly()
{
Console.WriteLine("喜鹊继承了接口,可以飞");
}
}
class Eagle : Bird,IFlyable
{
public override void eat()
{
Console.WriteLine("老鹰吃肉");
}
public void fly()
{
Console.WriteLine("老鹰继承了接口,可以飞");
}
}
class Penguin : Bird
{
public override void eat()
{
Console.WriteLine("企鹅吃鱼");
}
public void fly()
{
Console.WriteLine("企鹅没继承接口,这里的飞是它偷偷给自己写的方法");
}
}
}
实际上以下三个都是面向对象的五大原则之一,链接在这里。
里氏替换原则(Liskov Substitution Principle):
派生类(子类)对象能够替换其基类(基类)对象被使用。
通俗一点的理解就是“子类是父类”,举个例子,“男人【子类】是人【父类】,人【父类】不一定是男人【子类】”。
当需要一个父类类型的对象的时候可以给一个子类类型的对象;当需要一个子类类型对象的时候给一个父类类型对象是不可以的!
简单的理解为:一个软件实体如果使用的是一个父类,那么一定适用于其子类,而且它察觉不出父类对象和子类对象的区别。
但反过来不可以成立!
开放封闭原则(Open Closed Principle):
封装变化、降低耦合,软件实体应该是可扩展,而不可修改的。
也就是说,对扩展是开放的,而对修改是封闭的。
因此,开放封闭原则主要体现在两个方面:对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。
简单的理解就是,写好的类可以随意扩展【派生】、独立工作,但不要对原类进行修改。
依赖倒置原则:
依赖倒置原则,DIP,Dependency Inverse Principle DIP的表述是:
1、高层模块不应该依赖于低层模块, 二者都应该依赖于抽象。
2、抽象不应该依赖于细节,细节应该依赖于抽象。
这里说的“依赖”是使用的意思,如果你调用了一个类的一个方法,就是依赖这个类,如果你直接调用这个类的方法,就是依赖细节,细节就是具体的类,但如果你调用的是它父类或者接口的方法,就是依赖抽象, 所以 DIP 说白了就是不要直接使用具体的子类,而是用它的父类的引用去调用子类的方法,这样就是依赖于抽象,不依赖具体。
其实简单的说,DIP 的好处就是解除耦合,用了 DIP 之后,调用者就不知道被调用的代码是什么,因为调用者拿到的是父类的引用,它不知道具体指向哪个子类的实例,更不知道要调用的方法具体是什么,所以,被调用代码被偷偷换成另一个子类之后,调用者不需要做任何修改, 这就是解耦了。