日常收集C#接口知识(知识全面)
第一节接口慨述
接口(interface)用来定义一种程序的协定。实现接口的类或者结构要与接口的定义严格一致。有了这个协定,就可以抛开编程语言的限制(理论上)。接口可以从多个基接口继承,而类或结构可以实现多个接口。接口可以包含方法、属性、事件和索引器。接口本身不提供它所定义的成员的实现。接口只指定实现该接口的类或接口必须提供的成员。
接口好比一种模版,这种模版定义了对象必须实现的方法,其目的就是让这些方法可以作为接口实例被引用。接口不能被实例化。类可以实现多个接口并且通过这些实现的接口被索引。接口变量只能索引实现该接口的类的实例。例子:
interfaceIMyExample{ stringthis[intindex]{get;set;} eventEventHandlerEven; voidFind(intvalue); stringPoint{get;set;} } publicdelegatevoidEventHandler(objectsender,Evente);
上面例子中的接口包含一个索引this、一个事件Even、一个方法Find和一个属性Point。
接口可以支持多重继承。就像在下例中,接口"IComboBox"同时从"ITextBox"和"IListBox"继承。
interfaceIControl{ voidPaint(); } interfaceITextBox:IControl{ voidSetText(stringtext); } interfaceIListBox:IControl{ voidSetItems(string[]items); } interfaceIComboBox:ITextBox,IListBox{}
类和结构可以多重实例化接口。就像在下例中,类"EditBox"继承了类"Control",同时从"IDataBound"和"IControl"继承。
interfaceIDataBound{ voidBind(Binderb); } publicclassEditBox:Control,IControl,IDataBound{ publicvoidPaint(); publicvoidBind(Binderb){...} }
在上面的代码中,"Paint"方法从"IControl"接口而来;"Bind"方法从"IDataBound"接口而来,都以"public"的身份在"EditBox"类中实现。
说明:
1、C#中的接口是独立于类来定义的。这与C++模型是对立的,在C++中接口实际上就是抽象基类。
2、接口和类都可以继承多个接口。
3、而类可以继承一个基类,接口根本不能继承类。这种模型避免了C++的多继承问题,C++中不同基类中的实现可能出现冲突。因此也不再需要诸如虚拟继承和显式作用域这类复杂机制。C#的简化接口模型有助于加快应用程序的开发。
4、一个接口定义一个只有抽象成员的引用类型。C#中一个接口实际所做的,仅仅只存在着方法标志,但根本就没有执行代码。这就暗示了不能实例化一个接口,只能实例化一个派生自该接口的对象。
5、接口可以定义方法、属性和索引。所以,对比一个类,接口的特殊性是:当定义一个类时,可以派生自多重接口,而你只能可以从仅有的一个类派生。
接口与组件
接口描述了组件对外提供的服务。在组件和组件之间、组件和客户之间都通过接口进行交互。因此组件一旦发布,它只能通过预先定义的接口来提供合理的、一致的服务。这种接口定义之间的稳定性使客户应用开发者能够构造出坚固的应用。一个组件可以实现多个组件接口,而一个特定的组件接口也可以被多个组件来实现。
组件接口必须是能够自我描述的。这意味着组件接口应该不依赖于具体的实现,将实现和接口分离彻底消除了接口的使用者和接口的实现者之间的耦合关系,增强了信息的封装程度。同时这也要求组件接口必须使用一种与组件实现无关的语言。目前组件接口的描述标准是IDL语言。
由于接口是组件之间的协议,因此组件的接口一旦被发布,组件生产者就应该尽可能地保持接口不变,任何对接口语法或语义上的改变,都有可能造成现有组件与客户之间的联系遭到破坏。
每个组件都是自主的,有其独特的功能,只能通过接口与外界通信。当一个组件需要提供新的服务时,可以通过增加新的接口来实现。不会影响原接口已存在的客户。而新的客户可以重新选择新的接口来获得服务。
组件化程序设计
组件化程序设计方法继承并发展了面向对象的程序设计方法。它把对象技术应用于系统设计,对面向对象的程序设计的实现过程作了进一步的抽象。我们可以把组件化程序设计方法用作构造系统的体系结构层次的方法,并且可以使用面向对象的方法很方便地实现组件。
组件化程序设计强调真正的软件可重用性和高度的互操作性。它侧重于组件的产生和装配,这两方面一起构成了组件化程序设计的核心。组件的产生过程不仅仅是应用系统的需求,组件市场本身也推动了组件的发展,促进了软件厂商的交流与合作。组件的装配使得软件产品可以采用类似于搭积木的方法快速地建立起来,不仅可以缩短软件产品的开发周期,同时也提高了系统的稳定性和可靠性。
组件程序设计的方法有以下几个方面的特点:
1、编程语言和开发环境的独立性;
2、组件位置的透明性;
3、组件的进程透明性;
4、可扩充性;
5、可重用性;
6、具有强有力的基础设施;
7、系统一级的公共服务;
C#语言由于其许多优点,十分适用于组件编程。但这并不是说C#是一门组件编程语言,也不是说C#提供了组件编程的工具。我们已经多次指出,组件应该具有与编程语言无关的特性。请读者记住这一点:组件模型是一种规范,不管采用何种程序语言设计组件,都必须遵守这一规范。比如组装计算机的例子,只要各个厂商为我们提供的配件规格、接口符合统一的标准,这些配件组合起来就能协同工作,组件编程也是一样。我们只是说,利用C#语言进行组件编程将会给我们带来更大的方便。
知道了什么是接口,接下来就是怎样定义接口,请看下一节--定义接口。
第二节定义接口
从技术上讲,接口是一组包含了函数型方法的数据结构。通过这组数据结构,客户代码可以调用组件对象的功能。
定义接口的一般形式为:
[attributes][modifiers]interfaceidentifier[:base-list]{interface-body}[;]
说明:
1、attributes(可选):附加的定义性信息。
2、modifiers(可选):允许使用的修饰符有new和四个访问修饰符。分别是:new、public、protected、internal、private。在一个接口定义中同一修饰符不允许出现多次,new
修饰符只能出现在嵌套接口中,表示覆盖了继承而来的同名成员。Thepublic,protected,internal,andprivate修饰符定义了对接口的访问权限。
3、指示器和事件。
4、identifier:接口名称。
5、base-list(可选):包含一个或多个显式基接口的列表,接口间由逗号分隔。
6、interface-body:对接口成员的定义。
7、接口可以是命名空间或类的成员,并且可以包含下列成员的签名:方法、属性、索引器。
8、一个接口可从一个或多个基接口继承。
接口这个概念在C#和Java中非常相似。接口的关键词是interface,一个接口可以扩展一个或者多个其他接口。按照惯例,接口的名字以大写字母"I"开头。下面的代码是C#接口的一个例子,它与Java中的接口完全一样:
interfaceIShape{ voidDraw(); }
如果你从两个或者两个以上的接口派生,父接口的名字列表用逗号分隔,如下面的代码所示:
interfaceINewInterface:IParent1,IParent2{}
然而,与Java不同,C#中的接口不能包含域(Field)。另外还要注意,在C#中,接口内的所有方法默认都是公用方法。在Java中,方法定义可以带有public修饰符(即使这并非必要),但在C#中,显式为接口的方法指定public修饰符是非法的。例如,下面的C#接口将产生一个编译错误。
interfaceIShape{publicvoidDraw();}
下面的例子定义了一个名为IControl的接口,接口中包含一个成员方法Paint:
interfaceIControl{ voidPaint(); }
在下例中,接口IInterface从两个基接口IBase1和IBase2继承:
interfaceIInterface:IBase1,IBase2{ voidMethod1(); voidMethod2(); }
接口可由类实现。实现的接口的标识符出现在类的基列表中。例如:
classClass:Iface,Iface{ //class成员。 //http://www.cnblogs.com/roucheng/ }
类的基列表同时包含基类和接口时,列表中首先出现的是基类。例如:
classClassA:BaseClass,Iface1,Iface2{ //class成员。 }
以下的代码段定义接口IFace,它只有一个方法:
interfaceIFace{ voidShowMyFace(); }
不能从这个定义实例化一个对象,但可以从它派生一个类。因此,该类必须实现ShowMyFace抽象方法:
classCFace:IFace { publicvoidShowMyFace(){ Console.WriteLine("implementation"); } }
基接口
一个接口可以从零或多个接口继承,那些被称为这个接口的显式基接口。当一个接口有比零多的显式基接口时,那么在接口的定义中的形式为,接口标识符后面跟着由一个冒号":"和一个用逗号","分开的基接口标识符列表。
接口基:
:接口类型列表说明:
1、一个接口的显式基接口必须至少同接口本身一样可访问。例如,在一个公共接口的基接口中指定一个私有或内部的接口是错误的。
2、一个接口直接或间接地从它自己继承是错误的。
3、接口的基接口都是显式基接口,并且是它们的基接口。换句话说,基接口的集合完全由显式基接口和它们的显式基接口等等组成。在下面的例子中
interfaceIControl{ voidPaint(); } interfaceITextBox:IControl{ voidSetText(stringtext); } interfaceIListBox:IControl{ voidSetItems(string[]items); } interfaceIComboBox:ITextBox,IListBox{}
IComboBox的基接口是IControl,ITextBox,和IlistBox。
4、一个接口继承它的基接口的所有成员。换句话说,上面的接口IComboBox就像Paint一样继承成员SetText和SetItems。
5、一个实现了接口的类或结构也隐含地实现了所有接口的基接口。
接口主体
一个接口的接口主体定义接口的成员。
interface-body: {interface-member-declarationsopt}
定义接口主要是定义接口成员,请看下一节--定义接口成员。
第三节定义接口成员
接口可以包含一个和多个成员,这些成员可以是方法、属性、索引指示器和事件,但不能是常量、域、操作符、构造函数或析构函数,而且不能包含任何静态成员。接口定义创建新的定义空间,并且接口定义直接包含的接口成员定义将新成员引入该定义空间。
说明:
1、接口的成员是从基接口继承的成员和由接口本身定义的成员。
2、接口定义可以定义零个或多个成员。接口的成员必须是方法、属性、事件或索引器。接口不能包含常数、字段、运算符、实例构造函数、析构函数或类型,也不能包含任何种类的静态成员。
3、定义一个接口,该接口对于每种可能种类的成员都包含一个:方法、属性、事件和索引器。
4、接口成员默认访问方式是public。接口成员定义不能包含任何修饰符,比如成员定义前不能加abstract,public,protected,internal,private,virtual,override或static修饰符。
5、接口的成员之间不能相互同名。继承而来的成员不用再定义,但接口可以定义与继承而来的成员同名的成员,这时我们说接口成员覆盖了继承而来的成员,这不会导致错误,但编译器会给出一个警告。关闭警告提示的方式是在成员定义前加上一个new关键字。但如果没有覆盖父接口中的成员,使用new关键字会导致编译器发出警告。
6、方法的名称必须与同一接口中定义的所有属性和事件的名称不同。此外,方法的签名必须与同一接口中定义的所有其他方法的签名不同。
7、属性或事件的名称必须与同一接口中定义的所有其他成员的名称不同。
8、一个索引器的签名必须区别于在同一接口中定义的其他所有索引器的签名。
9、接口方法声明中的属性(attributes),返回类型(return-type),标识符(identifier),和形式参数列表(formal-parameter-lis)与一个类的方法声明中的那些有相同的意义。一个接口方法声明不允许指定一个方法主体,而声明通常用一个分号结束。
10、接口属性声明的访问符与类属性声明的访问符相对应,除了访问符主体通常必须用分号。因此,无论属性是读写、只读或只写,访问符都完全确定。
11、接口索引声明中的属性(attributes),类型(type),和形式参数列表(formal-parameter-list)与类的索引声明的那些有相同的意义。
下面例子中接口IMyTest包含了索引指示器、事件E、方法F、属性P这些成员:
interfaceIMyTest{ stringthis[intindex]{get;set;} eventEventHandlerE; voidF(intvalue); stringP{get;set;} } publicdelegatevoidEventHandler(objectsender,EventArgse);
下面例子中接口IStringList包含每个可能类型成员的接口:一个方法,一个属性,一个事件和一个索引。
publicdelegatevoidStringListEvent(IStringListsender); publicinterfaceIStringList { voidAdd(strings); intCount{get;} eventStringListEventChanged; stringthis[intindex]{get;set;} }
接口成员的全权名
使用接口成员也可采用全权名(fullyqualifiedname)。接口的全权名称是这样构成的。接口名加小圆点"."再跟成员名比如对于下面两个接口:
interfaceIControl{ voidPaint(); } interfaceITextBox:IControl{ voidGetText(stringtext); }
其中Paint的全权名是IControl.Paint,GetText的全权名是ITextBox.GetText。当然,全权名中的成员名称必须是在接口中已经定义过的,比如使用ITextBox.Paint.就是不合理的。
如果接口是名字空间的成员,全权名还必须包含名字空间的名称。
namespaceSystem { publicinterfaceIDataTable{ objectClone(); } }
那么Clone方法的全权名是System.IDataTable.Clone。
定义好了接口,接下来就是怎样访问接口,请看下一节--访问接口
第四节、访问接口
对接口成员的访问
对接口方法的调用和采用索引指示器访问的规则与类中的情况也是相同的。如果底层成员的命名与继承而来的高层成员一致,那么底层成员将覆盖同名的高层成员。但由于接口支持多继承,在多继承中,如果两个父接口含有同名的成员,这就产生了二义性(这也正是C#中取消了类的多继承机制的原因之一),这时需要进行显式的定义:
usingSystem; interfaceISequence{ intCount{get;set;} } interfaceIRing{ voidCount(inti); } //http://www.cnblogs.com/roucheng/ interfaceIRingSequence:ISequence,IRing{} classCTest{ voidTest(IRingSequencers){ //rs.Count();错误,Count有二义性 //rs.Count=;错误,Count有二义性 ((ISequence)rs).Count=;//正确 ((IRing)rs).Count();//正确调用IRing.Count } }
上面的例子中,前两条语句rs.Count(1)和rs.Count=1会产生二义性,从而导致编译时错误,因此必须显式地给rs指派父接口类型,这种指派在运行时不会带来额外的开销。
再看下面的例子:
usingSystem; interfaceIInteger{ voidAdd(inti); } interfaceIDouble{ voidAdd(doubled); } interfaceINumber:IInteger,IDouble{} classCMyTest{ voidTest(INumberNum){ //Num.Add();错误 Num.Add(.);//正确 ((IInteger)n).Add();//正确 ((IDouble)n).Add();//正确 } }
调用Num.Add(1)会导致二义性,因为候选的重载方法的参数类型均适用。但是,调用Num.Add(1.0)是允许的,因为1.0是浮点数参数类型与方法IInteger.Add()的参数类型不一致,这时只有IDouble.Add才是适用的。不过只要加入了显式的指派,就决不会产生二义性。
接口的多重继承的问题也会带来成员访问上的问题。例如:
interfaceIBase{ voidFWay(inti); } interfaceILeft:IBase{ newvoidFWay(inti); } interfaceIRight:IBase {voidG();} interfaceIDerived:ILeft,IRight{} classCTest{ voidTest(IDerivedd){ d.FWay();//调用ILeft.FWayhttp://www.cnblogs.com/roucheng/ ((IBase)d).FWay();//调用IBase.FWay ((ILeft)d).FWay();//调用ILeft.FWay ((IRight)d).FWay();//调用IBase.FWay } }
上例中,方法IBase.FWay在派生的接口ILeft中被Ileft的成员方法FWay覆盖了。所以对d.FWay(1)的调用实际上调用了。虽然从IBase->IRight->IDerived这条继承路径上来看,ILeft.FWay方法是没有被覆盖的。我们只要记住这一点:一旦成员被覆盖以后,所有对其的访问都被覆盖以后的成员"拦截"了。
类对接口的实现
前面我们已经说过,接口定义不包括方法的实现部分。接口可以通过类或结构来实现。我们主要讲述通过类来实现接口。用类来实现接口时,接口的名称必须包含在类定义中的基类列表中。
下面的例子给出了由类来实现接口的例子。其中ISequence为一个队列接口,提供了向队列尾部添加对象的成员方法Add(),IRing为一个循环表接口,提供了向环中插入对象的方法Insert(objectobj),方法返回插入的位置。类RingSquence实现了接口ISequence和接口IRing。
usingSystem; interfaceISequence{ objectAdd(); } interfaceISequence{ objectAdd(); } interfaceIRing{ intInsert(objectobj); } classRingSequence:ISequence,IRing { publicobjectAdd(){…} publicintInsert(objectobj){…} }
如果类实现了某个接口,类也隐式地继承了该接口的所有父接口,不管这些父接口有没有在类定义的基类表中列出。看下面的例子:
usingSystem; interfaceIControl{ voidPaint(); } interfaceITextBox:IControl{ voidSetText(stringtext); } interfaceIListBox:IControl{ voidSetItems(string[]items); } interfaceIComboBox:ITextBox,IListBox{}
这里,接口IcomboBox继承了ItextBox和IlistBox。类TextBox不仅实现了接口ITextBox,还实现了接口ITextBox的父接口IControl。
前面我们已经看到,一个类可以实现多个接口。再看下面的例子:
interfaceIDataBound{ voidBind(Binderb); } publicclassEditBox:Control,IControl,IDataBound{ publicvoidPaint(); publicvoidBind(Binderb){...} }
类EditBox从类Control中派生并且实现了Icontrol和IdataBound。在前面的例子中接口Icontrol中的Paint方法和IdataBound接口中的Bind方法都用类EditBox中的公共成员实现。C#提供一种实现这些方法的可选择的途径,这样可以使执行这些的类避免把这些成员设定为公共的。接口成员可以用有效的名称来实现。例如,类EditBox可以改作方法Icontrol.Paint和IdataBound.Bind来来实现。
publicclassEditBox:IControl,IDataBound{ voidIControl.Paint(){...} voidIDataBound.Bind(Binderb){...} }
因为通过外部指派接口成员实现了每个成员,所以用这种方法实现的成员称为外部接口成员。外部接口成员可以只是通过接口来调用。例如,Paint方法中EditBox的实现可以只是通过创建Icontrol接口来调用。
classTest{ staticvoidMain(){ EditBoxeditbox=newEditBox(); editbox.Paint();//错误:EditBox没有Paint事件 IControlcontrol=editbox; control.Paint();//调用EditBox的Paint事件 } }
上例中,类EditBox从Control类继承并同时实现了IControlandIDataBound接口。EditBox中的Paint方法来自IControl接口,Bind方法来自IDataBound接口,二者在EditBox类中都作为公有成员实现。当然,在C#中我们也可以选择不作为公有成员实现接口。
如果每个成员都明显地指出了被实现的接口,通过这种途径被实现的接口我们称之为显式接口成员(explicitinterfacemember)。用这种方式我们改写上面的例子:
publicclassEditBox:IControl,IDataBound{ voidIControl.Paint(){…} voidIDataBound.Bind(Binderb){…} }
显式接口成员只能通过接口调用。例如:
classCTest{ staticvoidMain(){ EditBoxeditbox=newEditBox(); editbox.Paint();//错误:不同的方法 IControlcontrol=editbox; control.Paint();//调用EditBox的Paint方法 } }
上述代码中对editbox.Paint()的调用是错误的,因为editbox本身并没有提供这一方法。control.Paint()是正确的调用方式。
注释:接口本身不提供所定义的成员的实现,它仅仅说明这些成员,这些成员必须依靠实现接口的类或其它接口的支持。
知道了怎样访问接口,我们还要知道怎样实现接口,要实现C#的接口,请看下一节-实现接口
第五节、实现接口
1、显式实现接口成员
为了实现接口,类可以定义显式接口成员执行体(Explicitinterfacememberimplementations)。显式接口成员执行体可以是一个方法、一个属性、一个事件或者是一个索引指示器的定义,定义与该成员对应的全权名应保持一致。
usingSystem; interfaceICloneable{ objectClone(); } interfaceIComparable{ intCompareTo(objectother); } classListEntry:ICloneable,IComparable{ objectICloneable.Clone(){…} intIComparable.CompareTo(objectother){…} }
上面的代码中ICloneable.Clone和IComparable.CompareTo就是显式接口成员执行体。
说明:
1、不能在方法调用、属性访问以及索引指示器访问中通过全权名访问显式接口成员执行体。事实上,显式接口成员执行体只能通过接口的实例,仅仅引用接口的成员名称来访问。
2、显式接口成员执行体不能使用任何访问限制符,也不能加上abstract,virtual,override或static修饰符。
3、显式接口成员执行体和其他成员有着不同的访问方式。因为不能在方法调用、属性访问以及索引指示器访问中通过全权名访问,显式接口成员执行体在某种意义上是私有的。但它们又可以通过接口的实例访问,也具有一定的公有性质。
4、只有类在定义时,把接口名写在了基类列表中,而且类中定义的全权名、类型和返回类型都与显式接口成员执行体完全一致时,显式接口成员执行体才是有效的,例如:
classShape:ICloneable{ objectICloneable.Clone(){…} intIComparable.CompareTo(objectother){…} }
使用显式接口成员执行体通常有两个目的:
1、因为显式接口成员执行体不能通过类的实例进行访问,这就可以从公有接口中把接口的实现部分单独分离开。如果一个类只在内部使用该接口,而类的使用者不会直接使用到该接口,这种显式接口成员执行体就可以起到作用。
2、显式接口成员执行体避免了接口成员之间因为同名而发生混淆。如果一个类希望对名称和返回类型相同的接口成员采用不同的实现方式,这就必须要使用到显式接口成员执行体。如果没有显式接口成员执行体,那么对于名称和返回类型不同的接口成员,类也无法进行实现。
下面的定义是无效的,因为Shape定义时基类列表中没有出现接口IComparable。
classShape:ICloneable { objectICloneable.Clone(){…} } classEllipse:Shape { objectICloneable.Clone(){…} }
在Ellipse中定义ICloneable.Clone是错误的,因为Ellipse即使隐式地实现了接口ICloneable,ICloneable仍然没有显式地出现在Ellipse定义的基类列表中。
接口成员的全权名必须对应在接口中定义的成员。如下面的例子中,Paint的显式接口成员执行体必须写成IControl.Paint。
usingSystem; interfaceIControl { voidPaint(); } interfaceITextBox:IControl { voidSetText(stringtext); } classTextBox:ITextBox { voidIControl.Paint(){…} voidITextBox.SetText(stringtext){…} }
实现接口的类可以显式实现该接口的成员。当显式实现某成员时,不能通过类实例访问该成员,而只能通过该接口的实例访问该成员。显式接口实现还允许程序员继承共享相同成员名的两个接口,并为每个接口成员提供一个单独的实现。
下面例子中同时以公制单位和英制单位显示框的尺寸。Box类继承IEnglishDimensions和IMetricDimensions两个接口,它们表示不同的度量衡系统。两个接口有相同的成员名Length和Width。
程序清单1DemonInterface.cs
interfaceIEnglishDimensions{ floatLength(); floatWidth(); } interfaceIMetricDimensions{ floatLength(); floatWidth(); } classBox:IEnglishDimensions,IMetricDimensions{ floatlengthInches; floatwidthInches; publicBox(floatlength,floatwidth){ lengthInches=length; widthInches=width; } floatIEnglishDimensions.Length(){ returnlengthInches; } floatIEnglishDimensions.Width(){ returnwidthInches; } floatIMetricDimensions.Length(){ returnlengthInches*.f; } floatIMetricDimensions.Width(){ returnwidthInches*.f; } publicstaticvoidMain(){ //定义一个实类对象"myBox":: BoxmyBox=newBox(.f,.f); //定义一个接口"eDimensions":: IEnglishDimensionseDimensions=(IEnglishDimensions)myBox; IMetricDimensionsmDimensions=(IMetricDimensions)myBox; //输出: System.Console.WriteLine("Length(in):{}",eDimensions.Length()); System.Console.WriteLine("Width(in):{}",eDimensions.Width()); System.Console.WriteLine("Length(cm):{}",mDimensions.Length()); System.Console.WriteLine("Width(cm):{}",mDimensions.Width()); } }
输出:Length(in):30,Width(in):20,Length(cm):76.2,Width(cm):50.8
代码讨论:如果希望默认度量采用英制单位,请正常实现Length和Width这两个方法,并从IMetricDimensions接口显式实现Length和Width方法:
publicfloatLength(){ returnlengthInches; } publicfloatWidth(){ returnwidthInches; } floatIMetricDimensions.Length(){ returnlengthInches*.f; } floatIMetricDimensions.Width(){ returnwidthInches*.f; }
这种情况下,可以从类实例访问英制单位,而从接口实例访问公制单位:
System.Console.WriteLine("Length(in):{0}",myBox.Length()); System.Console.WriteLine("Width(in):{0}",myBox.Width()); System.Console.WriteLine("Length(cm):{0}",mDimensions.Length()); System.Console.WriteLine("Width(cm):{0}",mDimensions.Width());
2、继承接口实现
接口具有不变性,但这并不意味着接口不再发展。类似于类的继承性,接口也可以继承和发展。
注意:接口继承和类继承不同,首先,类继承不仅是说明继承,而且也是实现继承;而接口继承只是说明继承。也就是说,派生类可以继承基类的方法实现,而派生的接口只继承了父接口的成员方法说明,而没有继承父接口的实现,其次,C#中类继承只允许单继承,但是接口继承允许多继承,一个子接口可以有多个父接口。
接口可以从零或多个接口中继承。从多个接口中继承时,用":"后跟被继承的接口名字,多个接口名之间用","分割。被继承的接口应该是可以访问得到的,比如从private类型或internal类型的接口中继承就是不允许的。接口不允许直接或间接地从自身继承。和类的继承相似,接口的继承也形成接口之间的层次结构。
请看下面的例子:
usingSystem; interfaceIControl{ voidPaint(); } interfaceITextBox:IControl{ voidSetText(stringtext); } interfaceIListBox:IControl{ voidSetItems(string[]items); } interfaceIComboBox:ITextBox,IListBox{}
对一个接口的继承也就继承了接口的所有成员,上面的例子中接口ITextBox和IListBox都从接口IControl中继承,也就继承了接口IControl的Paint方法。接口IComboBox从接口ITextBox和IListBox中继承,因此它应该继承了接口ITextBox的SetText方法和IListBox的SetItems方法,还有IControl的Paint方法。
一个类继承了所有被它的基本类提供的接口实现程序。
不通过显式的实现一个接口,一个派生类不能用任何方法改变它从它的基本类继承的接口映射。例如,在声明中
interfaceIControl{ voidPaint(); } classControl:IControl{ publicvoidPaint(){...} } classTextBox:Control{ newpublicvoidPaint(){...} }
TextBox中的方法Paint隐藏了Control中的方法Paint,但是没有改变从Control.Paint到IControl.Paint的映射,而通过类实例和接口实例调用Paint将会有下面的影响
Controlc=newControl(); TextBoxt=newTextBox(); IControlic=c; IControlit=t; c.Paint();//影响Control.Paint(); t.Paint();//影响TextBox.Paint(); ic.Paint();//影响Control.Paint(); it.Paint();//影响Control.Paint();
但是,当一个接口方法被映射到一个类中的虚拟方法,派生类就不可能覆盖这个虚拟方法并且改变接口的实现函数。例如,把上面的声明重新写为
interfaceIControl{ voidPaint(); } classControl:IControl{ publicvirtualvoidPaint(){...} } classTextBox:Control{ publicoverridevoidPaint(){...} }
就会看到下面的结果:
Controlc=newControl(); TextBoxt=newTextBox(); IControlic=c; IControlit=t; c.Paint();//影响Control.Paint(); t.Paint();//影响TextBox.Paint(); ic.Paint();//影响Control.Paint(); it.Paint();//影响TextBox.Paint();
由于显式接口成员实现程序不能被声明为虚拟的,就不可能覆盖一个显式接口成员实现程序。一个显式接口成员实现程序调用另外一个方法是有效的,而另外的那个方法可以被声明为虚拟的以便让派生类可以覆盖它。例如:
interfaceIControl{ voidPaint(); } classControl:IControl{ voidIControl.Paint(){PaintControl();} protectedvirtualvoidPaintControl(){...} } classTextBox:Control{ protectedoverridevoidPaintControl(){...} }
这里,从Control继承的类可以通过覆盖方法PaintControl来对IControl.Paint的实现程序进行特殊化。
3、重新实现接口
我们已经介绍过,派生类可以对基类中已经定义的成员方法进行重载。类似的概念引入到类对接口的实现中来,叫做接口的重实现(re-implementation)。继承了接口实现的类可以对接口进行重实现。这个接口要求是在类定义的基类列表中出现过的。对接口的重实现也必须严格地遵守首次实现接口的规则,派生的接口映射不会对为接口的重实现所建立的接口映射产生任何影响。
下面的代码给出了接口重实现的例子:
interfaceIControl{ voidPaint(); classControl:IControl voidIControl.Paint(){…} classMyControl:Control,IControl publicvoidPaint(){} }
实际上就是:Control把IControl.Paint映射到了Control.IControl.Paint上,但这并不影响在MyControl中的重实现。在MyControl中的重实现中,IControl.Paint被映射到MyControl.Paint之上。
在接口的重实现时,继承而来的公有成员定义和继承而来的显式接口成员的定义参与到接口映射的过程。
usingSystem; interfaceIMethods{ voidF(); voidG(); voidH(); voidI(); } classBase:IMethods{ voidIMethods.F(){} voidIMethods.G(){} publicvoidH(){} publicvoidI(){} } classDerived:Base,IMethods{ publicvoidF(){} voidIMethods.H(){} }
这里,接口IMethods在Derived中的实现把接口方法映射到了Derived.F,Base.IMethods.G,Derived.IMethods.H,还有Base.I。前面我们说过,类在实现一个接口时,同时隐式地实现了该接口的所有父接口。同样,类在重实现一个接口时同时,隐式地重实现了该接口的所有父接口。
usingSystem; interfaceIBase{ voidF(); } interfaceIDerived:IBase{ voidG(); } classC:IDerived{ voidIBase.F(){ //对F进行实现的代码… } voidIDerived.G(){ //对G进行实现的代码… } } classD:C,IDerived{ publicvoidF(){ //对F进行实现的代码… } publicvoidG(){ //对G进行实现的代码…http://www.cnblogs.com/roucheng/ } }
这里,对IDerived的重实现也同样实现了对IBase的重实现,把IBase.F映射到了D.F。
4、映射接口
类必须为在基类表中列出的所有接口的成员提供具体的实现。在类中定位接口成员的实现称之为接口映射(interfacemapping)。
映射,数学上表示一一对应的函数关系。接口映射的含义也是一样,接口通过类来实现,那么对于在接口中定义的每一个成员,都应该对应着类的一个成员来为它提供具体的实现。
类的成员及其所映射的接口成员之间必须满足下列条件:
1、如果A和B都是成员方法,那么A和B的名称、类型、形参表(包括参数个数和每一个参数的类型)都应该是一致的。
2、如果A和B都是属性,那么A和B的名称、类型应当一致,而且A和B的访问器也是类似的。但如果A不是显式接口成员执行体,A允许增加自己的访问器。
3、如果A和B都是时间那么A和B的名称、类型应当一致。
4、如果A和B都是索引指示器,那么A和B的类型、形参表(包括参数个数和每一个参数的类型)应当一致。而且A和B的访问器也是类似的。但如果A不是显式接口成员执行体,A允许增加自己的访问器。
那么,对于一个接口成员,怎样确定由哪一个类的成员来实现呢?即一个接口成员映射的是哪一个类的成员?在这里,我们叙述一下接口映射的过程。假设类C实现了一个接口IInterface,Member是接口IInterface中的一个成员,在定位由谁来实现接口成员Member,即Member的映射过程是这样的:
1、如果C中存在着一个显式接口成员执行体,该执行体与接口IInterface及其成员Member相对应,则由它来实现Member成员。
2、如果条件(1)不满足,且C中存在着一个非静态的公有成员,该成员与接口成员Member相对应,则由它来实现Member成员。
3、如果上述条件仍不满足,则在类C定义的基类列表中寻找一个C的基类D,用D来代替C。
4、重复步骤1--3,遍历C的所有直接基类和非直接基类,直到找到一个满足条件的类的成员。
5、如果仍然没有找到,则报告错误。
下面是一个调用基类方法来实现接口成员的例子。类Class2实现了接口Interface1,类Class2的基类Class1的成员也参与了接口的映射,也就是说类Class2在对接口Interface1进行实现时,使用了类Class1提供的成员方法F来实现接口Interface1的成员方法F:
interfaceInterface1{ voidF(); } classClass1{ publicvoidF(){} publicvoidG(){} } classClass2:Class1,Interface1{ newpublicvoidG(){} }
注意:接口的成员包括它自己定义的成员,而且包括该接口所有父接口定义的成员。在接口映射时,不仅要对接口定义体中显式定义的所有成员进行映射,而且要对隐式地从父接口那里继承来的所有接口成员进行映射。
在进行接口映射时,还要注意下面两点:
1、在决定由类中的哪个成员来实现接口成员时,类中显式说明的接口成员比其它成员优先实现。
2、使用Private、protected和static修饰符的成员不能参与实现接口映射。例如:
interfaceICloneable{ objectClone(); } classC:ICloneable{ objectICloneable.Clone(){…} publicobjectClone(){…} }
例子中成员ICloneable.Clone称为接口ICloneable的成员Clone的实现者,因为它是显式说明的接口成员,比其它成员有着更高的优先权。
如果一个类实现了两个或两个以上名字、类型和参数类型都相同的接口,那么类中的一个成员就可能实现所有这些接口成员:
interfaceIControl{ voidPaint(); } interfaceIForm{ voidPaint(); } classPage:IControl,IForm{ publicvoidPaint(){…} }
这里,接口IControl和IForm的方法Paint都映射到了类Page中的Paint方法。当然也可以分别用显式的接口成员分别实现这两个方法:
interfaceIControl{ voidPaint(); } interfaceIForm{ voidPaint(); } classPage:IControl,IForm{ publicvoidIControl.Paint(){ //具体的接口实现代码 } publicvoidIForm.Paint(){ //具体的接口实现代码http://roucheng.cnblogs.com/ } }
上面的两种写法都是正确的。但是如果接口成员在继承中覆盖了父接口的成员,那么对该接口成员的实现就可能必须映射到显式接口成员执行体。看下面的例子:
interfaceIBase{ intP{get;} } interfaceIDerived:IBase{ newintP(); }
接口IDerived从接口IBase中继承,这时接口IDerived的成员方法覆盖了父接口的成员方法。因为这时存在着同名的两个接口成员,那么对这两个接口成员的实现如果不采用显式接口成员执行体,编译器将无法分辨接口映射。所以,如果某个类要实现接口IDerived,在类中必须至少定义一个显式接口成员执行体。采用下面这些写法都是合理的:
//一:对两个接口成员都采用显式接口成员执行体来实现
classC:IDerived{ intIBase.P get {//具体的接口实现代码} intIDerived.P(){ //具体的接口实现代码} }
//二:对Ibase的接口成员采用显式接口成员执行体来实现
classC:IDerived{ intIBase.P get{//具体的接口实现代码} publicintP(){ //具体的接口实现代码} }
//三:对IDerived的接口成员采用显式接口成员执行体来实现
classC:IDerived{ publicintP get{//具体的接口实现代码} intIDerived.P(){ //具体的接口实现代码} }
另一种情况是,如果一个类实现了多个接口,这些接口又拥有同一个父接口,这个父接口只允许被实现一次。
usingSystem; interfaceIControl{ voidPaint(); interfaceITextBox:IControl{ voidSetText(stringtext); } interfaceIListBox:IControl{ voidSetItems(string[]items); } classComboBox:IControl,ITextBox,IListBox{ voidIControl.Paint(){…} voidITextBox.SetText(stringtext){…} voidIListBox.SetItems(string[]items){…} }
上面的例子中,类ComboBox实现了三个接口:IControl,ITextBox和IListBox。如果认为ComboBox不仅实现了IControl接口,而且在实现ITextBox和IListBox的同时,又分别实现了它们的父接口IControl。实际上,对接口ITextBox和IListBox的实现,分享了对接口IControl的实现。
我们对C#的接口有了较全面的认识,基本掌握了怎样应用C#的接口编程,但事实上,C#的不仅仅应用于.NET平台,它同样支持以前的COM,可以实现COM类到.NET类的转换,如C#调用API。欲了解这方面的知识,请看下一节-接口转换。
第六节、接口转换
C#中不仅支持.Net平台,而且支持COM平台。为了支持COM和.Net,C#包含一种称为属性的独特语言特性。一个属性实际上就是一个C#类,它通过修饰源代码来提供元信息。属性使C#能够支持特定的技术,如COM和.Net,而不会干扰语言规范本身。C#提供将COM接口转换为C#接口的属性类。另一些属性类将COM类转换为C#类。执行这些转换不需要任何IDL或类工厂。
现在部署的任何COM组件都可以在接口转换中使用。通常情况下,所需的调整是完全自动进行的。
特别是,可以使用运行时可调用包装(RCW)从.NET框架访问COM组件。此包装将COM组件提供的COM接口转换为与.NET框架兼容的接口。对于OLE自动化接口,RCW可以从类型库中自动生成;对于非OLE自动化接口,开发人员可以编写自定义RCW,手动将COM接口提供的类型映射为与.NET框架兼容的类型。
使用ComImport引用COM组件
COMInterop提供对现有COM组件的访问,而不需要修改原始组件。使用ComImport引用COM组件常包括下面几个方面的问题:
1、创建COM对象。
2、确定COM接口是否由对象实现。
3、调用COM接口上的方法。
4、实现可由COM客户端调用的对象和接口。
创建COM类包装
要使C#代码引用COM对象和接口,需要在C#中包含COM接口的定义。完成此操作的最简单方法是使用TlbImp.exe(类型库导入程序),它是一个包括在.NET框架SDK中的命令行工具。TlbImp将COM类型库转换为.NET框架元数据,从而有效地创建一个可以从任何托管语言调用的托管包装。用TlbImp创建的.NET框架元数据可以通过/R编译器选项包括在C#内部版本中。如果使用VisualStudio开发环境,则只需添加对COM类型库的引用,将为您自动完成此转换。
TlbImp执行下列转换:
1、COMcoclass转换为具有无参数构造函数的C#类。
2、COM结构转换为具有公共字段的C#结构。
检查TlbImp输出的一种很好的方法是运行.NET框架SDK命令行工具Ildasm.exe(Microsoft中间语言反汇编程序)来查看转换结果。
虽然TlbImp是将COM定义转换为C#的首选方法,但也不是任何时候都可以使用它(例如,在没有COM定义的类型库时或者TlbImp无法处理类型库中的定义时,就不能使用该方法)。在这些情况下,另一种方法是使用C#属性在C#源代码中手动定义COM定义。创建C#源映射后,只需编译C#源代码就可产生托管包装。
执行COM映射需要理解的主要属性包括:
1、ComImport:它将类标记为在外部实现的COM类。
2、Guid:它用于为类或接口指定通用唯一标识符(UUID)。
3、InterfaceType,它指定接口是从IUnknown还是从IDispatch派生。
4、PreserveSig,它指定是否应将本机返回值从HRESULT转换为.NET框架异常。
声明COMcoclass
COMcoclass在C#中表示为类。这些类必须具有与其关联的ComImport属性。下列限制适用于这些类:
1、类不能从任何其他类继承。
2、类不能实现任何接口。
4、类还必须具有为其设置全局唯一标识符(GUID)的Guid属性。
以下示例在C#中声明一个coclass:
//声明一个COM类FilgraphManager [ComImport,Guid("E436EBB3-524F-11CE-9F53-0020AF0BA770")] classFilgraphManager {}
C#编译器将添加一个无参数构造函数,可以调用此构造函数来创建COMcoclass的实例。
创建COM对象
COMcoclass在C#中表示为具有无参数构造函数的类。使用new运算符创建该类的实例等效于在C#中调用CoCreateInstance。使用以上定义的类,就可以很容易地实例化此类:
classMainClass { publicstaticvoidMain() { FilgraphManagerfilg=newFilgraphManager(); } }
声明COM接口
COM接口在C#中表示为具有ComImport和Guid属性的接口。它不能在其基接口列表中包含任何接口,而且必须按照方法在COM接口中出现的顺序声明接口成员函数。
在C#中声明的COM接口必须包含其基接口的所有成员的声明,IUnknown和IDispatch的成员除外(.NET框架将自动添加这些成员)。从IDispatch派生的COM接口必须用InterfaceType属性予以标记。
从C#代码调用COM接口方法时,公共语言运行库必须封送与COM对象之间传递的参数和返回值。对于每个.NET框架类型均有一个默认类型,公共语言运行库将使用此默认类型在COM调用间进行封送处理时封送。例如,C#字符串值的默认封送处理是封送到本机类型LPTSTR(指向TCHAR字符缓冲区的指针)。可以在COM接口的C#声明中使用MarshalAs属性重写默认封送处理。
在COM中,返回成功或失败的常用方法是返回一个HRESULT,并在MIDL中有一个标记为"retval"、用于方法的实际返回值的out参数。在C#(和.NET框架)中,指示已经发生错误的标准方法是引发异常。
默认情况下,.NET框架为由其调用的COM接口方法在两种异常处理类型之间提供自动映射。
返回值更改为标记为retval的参数的签名(如果方法没有标记为retval的参数,则为void)。
标记为retval的参数从方法的参数列表中剥离。
任何非成功返回值都将导致引发System.COMException异常。
此示例显示用MIDL声明的COM接口以及用C#声明的同一接口(注意这些方法使用COM错误处理方法)。
下面是接口转换的C#程序:
usingSystem.Runtime.InteropServices; //声明一个COM接口IMediaControl [Guid("AB-AD-CE-BA-AFBA"), InterfaceType(ComInterfaceType.InterfaceIsDual)] interfaceIMediaControl//这里不能列出任何基接口 { voidRun(); voidPause(); voidStop(); voidGetState([In]intmsTimeout,[Out]outintpfs); voidRenderFile( [In,MarshalAs(UnmanagedType.BStr)]stringstrFilename); voidAddSourceFilter( [In,MarshalAs(UnmanagedType.BStr)]stringstrFilename, [Out,MarshalAs(UnmanagedType.Interface)]outobjectppUnk); [return:MarshalAs(UnmanagedType.Interface)] objectFilterCollection(); [return:MarshalAs(UnmanagedType.Interface)] objectRegFilterCollection(); voidStopWhenReady(); }
若要防止HRESULT翻译为COMException,请在C#声明中将PreserveSig(true)属性附加到方法。
下面是一个使用C#映射媒体播放机COM对象的程序。
程序清单2DemonCOM.cs
usingSystem; usingSystem.Runtime.InteropServices; namespaceQuartzTypeLib { //声明一个COM接口IMediaControl,此接口来源于媒体播放机COM类 [Guid("56A868B1-0AD4-11CE-B03A-0020AF0BA770"), InterfaceType(ComInterfaceType.InterfaceIsDual)] interfaceIMediaControl {//列出接口成员 voidRun(); voidPause(); voidStop(); voidGetState([In]intmsTimeout,[Out]outintpfs); voidRenderFile( [In,MarshalAs(UnmanagedType.BStr)]stringstrFilename); voidAddSourceFilter( [In,MarshalAs(UnmanagedType.BStr)]stringstrFilename, [Out,MarshalAs(UnmanagedType.Interface)] outobjectppUnk); [return:MarshalAs(UnmanagedType.Interface)] objectFilterCollection(); [return:MarshalAs(UnmanagedType.Interface)] objectRegFilterCollection(); voidStopWhenReady(); } //声明一个COM类: [ComImport,Guid("E436EBB3-524F-11CE-9F53-0020AF0BA770")] classFilgraphManager//此类不能再继承其它基类或接口 { //这里不能有任何代码,系统自动增加一个缺省的构造函数 } } classMainClass { publicstaticvoidMain(string[]args) { //命令行参数: if(args.Length!=1) { DisplayUsage(); return; } Stringfilename=args[0]; if(filename.Equals("/?")) { DisplayUsage(); return; } //声明FilgraphManager的实类对象: QuartzTypeLib.FilgraphManagergraphManager=newQuartzTypeLib.FilgraphManager(); //声明IMediaControl的实类对象:: QuartzTypeLib.IMediaControlmc=(QuartzTypeLib.IMediaControl)graphManager; //调用COM的方法: mc.RenderFile(filename); //运行文件. mc.Run(); //暂借停. Console.WriteLine("PressEntertocontinue."); Console.ReadLine(); } privatestaticvoidDisplayUsage() {//显示 Console.WriteLine("媒体播放机:播放AVI文件."); Console.WriteLine("使用方法:VIDEOPLAYER.EXE文件名"); } }
运行示例:
若要显示影片示例Clock.avi,请使用以下命令:
interop2%windir%/clock.avi
这将在屏幕上显示影片,直到按ENTER键停止。
在.NET框架程序中通过DllImport使用Win32API
.NET框架程序可以通过静态DLL入口点的方式来访问本机代码库。DllImport属性用于指定包含外部方法的实现的dll位置。
DllImport属性定义如下:
namespaceSystem.Runtime.InteropServices { [AttributeUsage(AttributeTargets.Method)] publicclassDllImportAttribute:System.Attribute { publicDllImportAttribute(stringdllName){...} publicCallingConventionCallingConvention; publicCharSetCharSet; publicstringEntryPoint; publicboolExactSpelling; publicboolPreserveSig; publicboolSetLastError; publicstringValue{get{...}} } }
说明:
1、DllImport只能放置在方法声明上。
2、DllImport具有单个定位参数:指定包含被导入方法的dll名称的dllName参数。
3、DllImport具有五个命名参数:
a、CallingConvention参数指示入口点的调用约定。如果未指定CallingConvention,则使用默认值CallingConvention.Winapi。
b、CharSet参数指示用在入口点中的字符集。如果未指定CharSet,则使用默认值CharSet.Auto。
c、EntryPoint参数给出dll中入口点的名称。如果未指定EntryPoint,则使用方法本身的名称。
d、ExactSpelling参数指示EntryPoint是否必须与指示的入口点的拼写完全匹配。如果未指定ExactSpelling,则使用默认值false。
e、PreserveSig参数指示方法的签名应当被保留还是被转换。当签名被转换时,它被转换为一个具有HRESULT返回值和该返回值的一个名为retval的附加输出参数的签名。如果未指定PreserveSig,则使用默认值true。
f、SetLastError参数指示方法是否保留Win32"上一错误"。如果未指定SetLastError,则使用默认值false。
4、它是一次性属性类。
5、此外,用DllImport属性修饰的方法必须具有extern修饰符。
下面是C#调用Win32MessageBox函数的示例:
usingSystem; usingSystem.Runtime.InteropServices; classMainApp {//通过DllImport引用user.dll类。MessageBox来自于user.dll类 [DllImport("user.dll",EntryPoint="MessageBox")] publicstaticexternintMessageBox(inthWnd,StringstrMessage,StringstrCaption,uintuiType); publicstaticvoidMain() { MessageBox(,"您好,这是PInvoke!",".NET",); } }
面向对象的编程语言几乎都用到了抽象类这一概念,抽象类为实现抽象事物提供了更大的灵活性。C#也不例外,C#通过覆盖虚接口的技术深化了抽象类的应用。欲了解这方面的知识,请看下一节-覆盖虚接口
第七节、覆盖虚接口
有时候我们需要表达一种抽象的东西,它是一些东西的概括,但我们又不能真正的看到它成为一个实体在我们眼前出现,为此面向对象的编程语言便有了抽象类的概念。C#作为一个面向对象的语言,必然也会引入抽象类这一概念。接口和抽象类使您可以创建组件交互的定义。通过接口,可以指定组件必须实现的方法,但不实际指定如何实现方法。抽象类使您可以创建行为的定义,同时提供用于继承类的一些公共实现。对于在组件中实现多态行为,接口和抽象类都是很有用的工具。
一个抽象类必须为类的基本类列表中列出的接口的所有成员提供实现程序。但是,一个抽象类被允许把接口方法映射到抽象方法中。例如
interfaceIMethods{ voidF(); voidG(); } abstractclassC:IMethods { publicabstractvoidF(); publicabstractvoidG(); }
这里,IMethods的实现函数把F和G映射到抽象方法中,它们必须在从C派生的非抽象类中被覆盖。
注意显式接口成员实现函数不能是抽象的,但是显式接口成员实现函数当然可以调用抽象方法。例如
interfaceIMethods { voidF(); voidG(); } abstractclassC:IMethods { voidIMethods.F(){FF();} voidIMethods.G(){GG();} protectedabstractvoidFF(); protectedabstractvoidGG(); }