C#与OOP之 接口
接口(interface)
简单地说接口就是一种对行为的契约或者规范。比如我们一说到“笔”,那么我们就知道它一定可以用来“书写”,而不管它是铅笔还是水笔,不管它是用木制的还是塑料制的。这里的“笔”就相当于一个契约(接口),它描述了“书写”这样一个行为。只要这个对象是“笔”,那么它就一定能“书写”(而不管对象具体是什么类型的东西)。正因为有了“笔”对“书写”行为的这样一个约定,所以当我们到商店里去买钢笔时,不会再问去售货小姐“这个东西能不能用来书写”;也不会在第一次用某种牌子的铅笔之前还要先看说明书才知道它能不能够书写。
而在我们的程序里,我们可以通过接口(interface)来制定这种契约。只要对象实现这个契约,那么这个对象就一定具有契约中所规定的行为,而不用去管这个对象到底是什么。当然你可能会说,那我这个对象表面上假装实现了这个契约,但在对象里面却不具有(实现)契约中所规定的行为(就像生活中的一些伪劣产品,广告里说实现了什么,但实际上都是假的),那么我们的编译器这个时候就充当明察秋毫的“质检人员”,把你的“伪劣”对象都找出来,不让你编译通过。所以编译器是我们使用接口的保证。
先来看看C#里关于interface的语法:
申明一个接口:
public interface 笔
{
void 书写(); // 申明了书写行为,但仅仅是“打了个广告”而已,没有实现
}
注意:interface里申明的方法不需要诸如public、private等访问修饰符,因为interface里的方法都默认是public的。(秘密的规范还有什么意义呢?)
一个类被申明为实现某个接口的格式和继承很相似:
public class 钢笔 : 笔
{
public void 书写()
{
// 具体实现书写的过程
}
}
所以也会经常说类“继承”了某个接口。
那么在程序里定义接口(interface)到底有用呢?这主要体现在3个方面。
1、当我们定义了自己的类,那么类的方法(即:行为)就是你自己定义的,你必须要通过其他形式(比如文档)来告诉其他的使用者,你的类有哪些方法,能实现什么。那么有了interface,我们可以在做自己的类之前先定义一个interface,然后让自己的类来遵守(实现)这个interface,那么你只需要把这个interface交给使用者就可以了,甚至在你的类还没有coding完成之前就可以先把interface交给使用者,这样你的类的coding和使用者coding的过程可以同时进行,从而提高开发速度。
2、在我们的程序里使用interface则可以获得更好扩充性和灵活性
来个例子(还是以笔为例):
public interface 笔 { void 书写(); }
public class 钢笔 : 笔 { public void 书写() { ...... // 用钢笔自己的方式来实现到底怎么书写 } ...... // 其他钢笔的属性和行为 }
public class 铅笔 : 笔 { public void 书写() { ...... // 用铅笔自己的方式来实现到底怎么书写 } ...... // 其他铅笔笔的属性和行为 }
public class 学生 { // 我们传入的参数是“笔”这个规范类型,而不是具体的钢笔类型或铅笔类型 public void 写作文(笔 tool) { ...... tool.书写(); ..... } }
…… public static main() { 钢笔 pen; // 申明“钢笔”的对象 铅笔 pencil; // 申明“铅笔”的对象
pen = new 钢笔(); // 实例“钢笔”对象实例 pencil = new 铅笔(); // 实例“铅笔”对象实例
学生 student = new 学生();
// 这里会有一个自动转型的过程, // 会自动将pen和pencil向上转换成“笔”来符合 “写作文(笔 tool)” 这个方法的参数规定 student.写作文(pen) // 学生用钢笔写作文 student.写作文(pencil) // 学生用铅笔写作文 // 当以后发明出了“激光笔”,就让用学生用激光笔来写作文, // 而不用去修改 学生.写作文(笔 tool) 这个方法了(因为激光笔也会遵守“笔”的规范)。 } |
从上面的例子可以看出来,我们的学生.写作文(笔 tool) 这个方法使用了接口作为参数,这样只要实现了该接口的类型都可以传递进去,而不用管它具体是什么类型的,这使得我们的程序更容易扩充。同样凡是接受“笔”作为参数的方法,我们都可以动态的把“钢笔”或“铅笔”传进取(至于是传“钢笔”还是“铅笔”可以有很多技巧,比如用配置文件来标志)来动态的实现不同的效果和功能,这使得我们的程序更加灵活。
3、在类似C#和Java这种单根继承的语言里,可以通过使用interface来在一定程度上实现多重继承的效果。
------------------------------------------------------------------------------------------------------------
最后还是说一说C#里运行时环境对接口的执行过程:
C#程序在运行中是如何使用接口的,如何访问接口函数,具体流程如下:
1、当调用一个接口的函数时,系统会去检查这个接口对应实例是什么;
2、找到这个实例后,再去找这个实例对应的实例类是什么(实例类,在虚函数一文里曾说明过);
3、根据这个实例类去检查该实例类是否和接口发生了捆绑(看是否实现了该接口,冒号后面就是);
4、好!如果实例类实现了该接口(发生了捆绑),那么它就在这个实例类中找到接口中所申明的方法的定义,然后执行该方法,然后结束。
5、如果没找到,它就继续往父类去找,直到找到第一个和接口捆绑的父类为止
6、找到后,它再检查这个父类里该方法是否是被定义为虚函数;
7、如果不是,他马上就执行这个方法,然后结束;
8、如果是,麻烦了,系统又要从头来过,去检查该最开始那个实例类里有否重载了父类里的这个虚函数…...(具体过程见上一篇《虚函数》)。
DEMO:
interface I { void Func(); }
class A : I { public virtual void Func() { Console.WriteLine("Func In A"); } }
class B : A , I // 注意这里,用接口来实现类似多重继承的效果 { public void Func() { Console.WriteLine("Func In B"); } }
class C : A { public override void Func() { Console.WriteLine("Func In C"); } }
class D : A { public new void Func() { Console.WriteLine("Func In D"); } }
public static void main() { I a = new A() ; //申明了接口a,并马上和一个类的实例发生关系了 I b = new B() ; //申明了接口b,并马上和一个类的实例发生关系了 I c = new C() ; //申明了接口c,并马上和一个类的实例发生关系了 I d = new D() ; //申明了接口d,并马上和一个类的实例发生关系了
//检查a的实例类A,发现A和接口I捆绑了,所以执行A的函数Func ,结果: Func In A a.Func();
// 检查b的实例类B,发现B和接口I捆绑了,所以执行B的函数Func(而不会去执行父类A的,尽管A也 // 实现I接口),结果: Func In B b.Func();
// 检查c的实例类C,发现其没有和接口I捆绑,系统继续找它的父类. 发现A和I捆绑了,他就去找 // 函数A,发现A是虚拟函数,系统又从头来找类的实例C,发现C重载(override)了Func,好了,马 //上执行该函数. 结果是Func In C c.Func();
// 检查d的实例类D,发现其没有和接口I捆绑,系统继续找它的父类. 发现A和I捆绑了,他就去找 // 函数A,发现A是虚拟函数,系统又从头来找类的实例D,但是D里没有重载(override)Func(而是 // 用new覆盖了),所以又会到D的父类里找,所以还是执行A的Func(),结果是Func In A d.Func() ; } |
封装
就是把类的内部隐藏起来
好处:减少耦合 ,类内部的实现可以自由地修改,类具有清晰的对外接口
数据隐藏
实现方法就是 访问限制修饰符
public
protected internal
internal
protected
private
继承性 inheritance
一个类可以有能力直接从另一个类获得其代码和数据
派生类从基类那里获得其所有的成员
C#只支持单继承
防止继承 public sealed class classname
何时使用继承
代码重用,减少编写的代码量
设计重用 公用的字段和方法可以放到父类中,然后由其派生新的子类
子类有自己的字段和方法
多态性
是面向对象程序设计中的重要概念。
在运行时,可以通过指向基类的应用,来调用实现派生类中的方法。
同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。
多态性通过派生类覆写基类中的虚函数型方法来实现。
重载Overload 存在于同一类中
方法名必须相同
参数列表必须不相同
返回类型可以不相同
覆写Override 存在与继承关系的类中
只有虚方法和抽象方法才能覆写
相同的方法名称
相同的参数列表
相同的返回值类型
抽象方法 abstract
是必须被派生类覆写的方法
可以看成没有实现体的虚方法
虚函数
使用virtual关键字public virtual bool withdraw()
接口 interface
接口为类提供了蓝图
接口只提供定义
实现接口的数据类型必须提供接口成员的实现
接口本身可以从多个基接口派生