首页 > 代码库 > 继承、多态、封装、接口
继承、多态、封装、接口
继承部分
继承(加上封装和多态性)是面向对象的编程的三个主要特性(也称为“支柱”)之一。 继承用于创建可重用、扩展和修改在其他类中定义的行为的新类。 其成员被继承的类称为“基类”,继承这些成员的类称为“派生类”。 派生类只能有一个直接基类。 但是,继承是可传递的。 如果 ClassB 派生出 ClassC,ClassA 派生出 ClassB,则 ClassC 会继承 ClassB 和 ClassA 中声明的成员。
定义一个类从其他类派生时,派生类隐式获得基类的除构造函数和析构函数以外的所有成员。 因此,派生类可以重用基类中的代码而无需重新实现这些代码。 可以在派生类中添加更多成员。 派生类以这种方式扩展基类的功能。
结构不支持继承,但可以实现接口。
抽象方法和虚方法
当基类将方法声明为 virtual 时,派生类可以用自己的实现重写该方法。 如果基类将成员声明为 abstract,则在直接继承自该类的任何非抽象类中都必须重写该方法。 如果派生类自身是抽象的,则它继承抽象成员而不实现它们。抽象成员和虚成员是多态性的基础,多态性是面向对象的编程的第二个主要特性。
抽象基类
如果希望禁止通过 new关键字直接进行实例化,可以将类声明为abstract 。如果这样做,则仅当从该类派生新类时才能使用该类。 抽象类可以包含一个或多个自身声明为抽象的方法签名。 这些签名指定参数和返回值,但没有实现(方法体)。 抽象类不必包含抽象成员;但是,如果某个类确实包含抽象成员,则该类自身必须声明为抽象类。自身不是抽象类的派生类必须为抽象基类中的任何抽象方法提供实现。
当然继承抽象类的类也可以包含抽象方法,也可以用抽象方法继承抽象方法,抽象方法留给后来以此类做父类的继承类去实现。
接口
“接口”是一种引用类型,有点像仅包含抽象成员的抽象基类。 类在从接口实现时必须为该接口的所有成员提供实现。 类虽然只能从一个直接基类派生,但可以实现多个接口。
接口用于为不一定具有“是”关系的类定义特定功能。 例如,System.IEquatable<T> 接口可由任何类或构造实现,这些类或构造必须启用代码来确定该类型的两个对象是否等效(但是该类型定义等效性)。 IEquatable<T> 不表示基类和派生类之间存在的同一种“是”关系(例如 Mammal 是 Animal)。
派生类对基类成员的访问
派生类可以访问基类的公共成员、受保护成员、内部成员和受保护内部成员。即使派生类继承基类的私有成员,仍不能访问这些成员。但是,所有这些私有成员在派生类中仍然存在,且执行与基类自身中相同的工作。 例如,假定一个受保护基类方法访问私有字段。 要使继承的基类方法正常工作,派生类中必须有该字段。
禁止进一步派生
类可以将自身或其成员声明为 sealed,从而禁止其他类从该类自身或其任何成员继承。
派生类隐藏基类成员
派生类可以通过以相同的名称和签名声明基类成员来隐藏这些成员。可以使用 new 修饰符显式指示成员不作为基类成员的重写。不是必须要使用 new,但如果不使用 new,将生成编译器警告。
继承小结: .Net天生就是面向对象的(都继承了object(对象)类),继承其实不仅仅是自定义类和继承接口、抽象类等,还和类型转换有关系。值类型只是在栈上,就不说了。引用类型之间的相互转换,简单一句:向上转换,即子类转父类(例子自己整下)。用来检验继承关系中是否合理时用is-a,如if(obj is 类型A) //obj是父类类型对象,“类型A”是子类类型。当然也可以用as。
接口部分
接口包含 类 或 结构 可以实现相关一组功能的定义。关键字:interface。
通过使用接口,可以在选件类中,例如,包括从多个源的行为。 由于C#语言不支持多重继承,所以该功能很重要。 此外,如果要模拟结构的继承,就必须使用接口,因为结构类不能从另一个结构或选件类实际继承。
接口可以包含方法、属性、事件、索引器,或者这四个成员类型的任意组合(—_—,这四个到最后还都是方法。)。接口不能包含常数、字段、运算符、实例构造函数、析构函数或类型。 接口成员将自动是公共的,因此,它们不会包含任何访问修饰符。 成员也不能是 静态。
若要实现接口成员,实现的该接口的相应成员必须是公共的,非静态的,并且具有名称和签名和接口成员相同。
当类或结构实现接口时,类或结构必须提供接口定义的任何成员的实现。继承类的属性和索引可以定义接口中定义的属性或索引器的额外访问器。
接口可以实现接口。通过定义小而巧的接口,一方面可以防止接口污染,另一方可以通过接口继承接口来重新组合接口。继承组合接口的类,必须实现接口继承的接口。 要注意的是,继承接口的类(暂时称为基类)必须无条件的实现接口。如果基类有扩展类,扩展类不需要实现接口的方法就是接口的子类,原因是基类已提供接口的成员的实现(间接继承)。
使用虚拟成员,基类也可以实现接口成员。在这种情况下,派生类可以通过重写虚拟成员来更改接口行为。
显示接口实现
如果类实现两个接口,并且这两个接口包含具有相同签名的成员,那么在类中实现该成员将导致两个接口都使用该成员作为它们的实现。
如果两个接口成员执行不同的函数,那么这可能会导致其中一个接口的实现不正确或两个接口的实现都不正确。可以显式地实现接口成员 -- 即创建一个仅通过该接口调用并且特定于该接口的类成员。这是使用接口名称和一个句点命名该类成员来实现的。
显式实现还用于解决两个接口分别声明具有相同名称的不同成员(如属性和方法)的情况。例子的网址。
还有一个例子:同时实现两个接口,类必须对属性 P 和/或方法 P 使用显式实现以避免编译器错误。
interface ILeft{ int P { get;}}
interface IRight{ int P();}
class Middle : ILeft, IRight{ public int P() { return 0; }
int ILeft.P { get { return 0; } }}
接口小结:显式实现的接口成员不能从类实例访问。通过显示接口实现的方法,访问修饰符是私有的且通过类是无法访问的。参考;在显示实现的接口成员前,不能添加任何访问修饰符,如private、public等,C#语法中是不允许的。
显示实现接口后,被实现的方法变成private的,所以通过类对象访问不到,显示实现接口,只能通过接口变量来调用。
还有一点:接口与接口之间可以相互继承,并且可以多继承。用接口时要注意接口污染等问题。
自娱自乐:
什么是接口?
接口是一种规范,协议,约定好遵守某种规范就可以写好通用的代码。定义了一组具有各种功能的方法。
接口解决了什么问题?
接口解决了类的多继承的问题;接口解决了类继承以后体积庞大的问题(接口未实现);
接口之间可以实现多继承;
接口的主要目的是什么?
主要目的:就是为了实现多态。
多态部分
多态性常被视为自封装和继承之后,面向对象的编程的第三个支柱。 Polymorphism(多态性)是一个希腊词,指“多种形态”,多态性具有两个截然不同的方面:
- 在运行时,在方法参数和集合或数组等位置,派生类的对象可以作为基类的对象处理。发生此情况时,该对象的声明类型不再与运行时类型相同。
- 基类可以定义并实现虚方法,派生类可以重写这些方法,即派生类提供自己的定义和实现。在运行时,客户端代码调用该方法,CLR 查找对象的运行时类型,并调用虚方法的重写方法。因此,可以在源代码中调用基类的方法,但执行该方法的派生类版本。
虚方法允许您以统一方式处理多组相关的对象。可以使用多态性通过两个基本步骤解决这一问题:
- 创建一个类层次结构,其中每个特定形状类均派生自一个公共基类。
- 使用虚方法通过对基类方法的单个调用,来调用任何派生类上的相应方法。
多态性的概述
1) 虚成员
当派生类从基类继承时,它会获得基类的所有方法、字段、属性和事件。 派生类的设计器可以选择是否
- 重写基类中的虚拟成员。
- 继承最接近的基类方法而不重写它
- 定义隐藏基类实现的成员的新非虚实现
仅当基类成员声明为 virtual 或 abstract 时,派生类才能重写基类成员。 派生成员必须使用 override 关键字显式指示该方法将参与虚调用。
虚方法和属性允许派生类扩展基类,而无需使用基类方法去实现。接口提供另一种方式来定义将实现留给派生类的方法或方法集
2) 使用新成员隐藏基类成员
如果希望派生成员具有与基类中的成员相同的名称,但又不希望派生成员参与虚调用,则可以使用 new 关键字。 new 关键字放置在要替换的类成员的返回类型之前。通过将派生类的实例强制转换为基类的实例,仍然可以从客户端代码访问隐藏的基类成员。
3) 阻止派生类重写虚拟成员
无论在虚拟成员和最初声明虚拟成员的类之间已声明了多少个类,虚拟成员永远都是虚拟的。 如果类 A 声明了一个虚拟成员,类 B 从 A 派生,类 C 从类 B 派生,则类 C 继承该虚拟成员,并且可以选择重写它,而不管类 B 是否为该成员声明了重写。
派生类可以通过将重写声明为 sealed 来停止虚拟继承。 这需要在类成员声明中的 override 关键字前面放置 sealed 关键字。
通过使用 new 关键字,密封的方法可以由派生类替换。
public class A{ public virtual void DoWork() { }}
public class B : A{ public override void DoWork() { }}
public class C : B{ public sealed override void DoWork() { }}
public class D : C{ public new void DoWork() { }}
在此情况下,如果在 D 中使用类型为 D 的变量调用 DoWork,被调用的将是新的 DoWork。 如果使用类型为 C、B 或 A 的变量访问 D 的实例,对 DoWork 的调用将遵循虚拟继承的规则,即把这些调用传送到类 C 的 DoWork 实现。
4) 从派生类访问基类虚拟成员
已替换或重写某个方法或属性的派生类仍然可以使用基关键字访问基类的该方法或属性。以下代码提供了一个示例:
public class Base{ public virtual void DoWork() {/*...*/ }}
public class Derived : Base
{public override void DoWork() {
//Perform Derived‘s work here.
// Call DoWork on base class
base.DoWork(); }}
建议虚拟成员在它们自己的实现中使用 base 来调用该成员的基类实现。 允许基类行为发生使得派生类能够集中精力实现特定于派生类的行为。 未调用基类实现时,由派生类负责使它们的行为与基类的行为兼容。
使用Override和New关键字进行版本控制
C# 语言经过专门设计,以便不同库中的基类与派生类之间的版本控制可以不断向前发展,同时保持向后兼容。 这具有多方面的意义。例如,这意味着在基类中引入与派生类中的某个成员具有相同名称的新成员在 C# 中是完全支持的,不会导致意外行为。它还意味着类必须显式声明某方法是要重写一个继承方法,还是一个隐藏具有类似名称的继承方法的新方法。
在 C# 中,派生类可以包含与基类方法同名的方法。
- 基类方法必须定义为 virtual。
- 如果派生类中的方法前面没有 new 或 override 关键字,则编译器将发出警告,该方法将有如存在 new 关键字一样执行操作。
- 如果派生类中的方法前面带有 new 关键字,则该方法被定义为独立于基类中的方法。
- 如果派生类中的方法前面带有 override 关键字,则派生类的对象将调用该方法,而不是调用基类方法。
- 可以从派生类中使用 base 关键字调用基类方法。
- override 、virtual 和 new 关键字还可以用于属性、索引器和事件中。
默认情况下,C# 方法为非虚方法。 如果某个方法被声明为虚方法,则继承该方法的任何类都可以实现它自己的版本。 若要使方法成为虚方法,必须在基类的方法声明中使用 virtual 修饰符。然后,派生类可以使用 override 关键字重写基虚方法,或使用 new 关键字隐藏基类中的虚方法。 如果 override 关键字和 new 关键字均未指定,编译器将发出警告,并且派生类中的方法将隐藏基类中的方法。
看个例子:
public class Derived : Base{
public override void DoWork(int param) { }
public void DoWork(double param) { }}
在 Derived 的一个实例中调用 DoWork 时,C# 编译器将首先尝试使该调用与最初在 Derived 上声明的 DoWork 版本兼容(也就是调用非虚方法)。重写方法不被视为是在类上进行声明的,而是在基类上声明的方法的新实现。仅当 C# 编译器无法将方法调用与 Derived 上的原始方法匹配时,它才尝试将该调用与具有相同名称和兼容参数的重写方法匹配。 例如:
int val = 5;
Derived d = new Derived();
d.DoWork(val); // Calls DoWork(double).
由于变量 val 可以隐式转换为 double 类型,因此 C# 编译器将调用 DoWork(double),而不是 DoWork(int)。 有两种方法可以避免此情况。 首先,避免将新方法声明为与虚方法同名。 其次,可以通过将 Derived 的实例强制转换为 Base 来使 C# 编译器搜索基类方法列表,从而使其调用虚方法。 由于是虚方法,因此将调用 Derived 上的 DoWork(int) 的实现。 例如:
((Base)d).DoWork(val); // Calls DoWork(int) on Derived.
重写ToString方法
C# 中的每个类或结构都隐式继承 Object 类。 因此,C# 中的每个对象都会获得 ToString 方法,此方法返回该对象的字符串表示形式。创建自定义类或结构时,应该重写 ToString 方法,以便向客户端代码提供类型信息。
多态小结:多态其实就是为了方便程序的可扩展性。其实多态不止是虚方法和重写,还和继承、抽象类、接口有关系。我们通常用的多态就是靠继承关系来实现的:不同对象收到相同的消息时,会产生不同行为,同一个类在不同的场合下表现出不同的行为特征(赤裸裸的继承关系O(∩_∩)O~)。多态应用有很多,此处说点常用的:把不同的子类对象都当作父类来看,可以屏蔽不同子类对象之间的差异,写出通用的代码,作出通用的编程,以适应需求的不断变化。例如,电脑插入U盘、硬盘,电脑(看做父类)只管读写,而调用的读写方法却是子类去实现。这样很方便程序扩展,如:电脑(父类)可以扩展去读取:MP3、MP4等等(只是个简单的例子)。当然多态还得遵守一个里氏替换原则。即:父类引用指向子类对象。父类对象不能替换子类。
封装部分
每个对象都包含它能进行操作所需要的所有信息,这个特性称为封装,因此对象不必依赖其他对象来完成自己的操作。这样方法和属性包装在类中,通过类的实例来实现。
以下是个没有封装的例子:
//实现两个数相加
int numA=3;
int numB=4;
int result;
result=numA+numB;
以下是个用了封装的例子:
//实现两个数相加
class AddMethod(){
private int _numA=0;
private int _numB=0;
public NumA{
get{return _numA;}
set{_numA=value;}}
public NumB{
get{return _numB;}
set{_numB=value;}}
public int getResult(){
return _numA+_numB;}}
封装的好处:
- 良好的封装能够减少耦合。例如,可以让类和主函数的偶合分离。
- 类具有清晰的对外接口。例如,类中的属性和方法。
封装小结:封装简单说就是把可变的代码封装起来。面向对象有一条开发封闭原则,即:对修改封闭,对扩展开发。若果写代码总是ctrl+c和ctrl+v加简单的修改,那么维护起来将是件头疼的事。把可变的代码封装起来,在未来修改代码时,可以利用面向对象中继承和多态来修改原来的代码,因为你没改源码,所以原来的代码将会正常运行,这也就进步体现了对修改封闭(不准在类里修改代码),对扩展开发(可以被继承或者被重写或者别的什么)。
总结
接口的存在意义:多态。多态的意义:程序可扩展性。最终→节省陈本,提高效率。
当一个抽象类实现接口的时候,如果不想把接口中的成员实现,可以把该类成员实现为abstract。(抽象类也可以实现接口,用abstrac标记)。
当一个类同时继承了某个类,并且也实现了某些接口的时候,必须要将继承的类写在前面。
当多个类型,都具有某个或某个功能时(方法),但是这几个类型又分属不同的系列,这时为了优化就可以考虑把这几个类型共有的方法提取到一个接口中,让这几个类型分别实现该接口。
面向对象其实就是,面向抽象编程,使用抽象(父类、抽象类、接口)不适用具体。
在编程时:
- 接口→抽象类→父类→具体类
- 能使用接口就不用抽象类,能使用抽象类就不用类,能用父类就不用子类。
- 避免定义“体积庞大的接口”,“多功能接口”,会造成“接口污染”。只把相关联的一组成员定义到一个接口中(尽量在接口中少定义成员)。单一职责原则。