C# 接口(五 实现接口)

第五节 实现接口

  1、显式实现接口成员

  为了实现接口,类可以定义显式接口成员执行体(Explicit interface member implementations)。显式接口成员执行体可以是一个方法、一个属性、一个事件或者是一个索引指示器的定义,定义与该成员对应的全权名应保持一致。

  1. using System;
  2. interface ICloneable
  3. {
  4.     object Clone();
  5. }
  6. interface IComparable
  7. {
  8.     int CompareTo(object other);
  9. }
  10. class ListEntry: ICloneable, IComparable 
  11. {
  12.     object ICloneable.Clone( ) {…}
  13.     int IComparable.CompareTo(object other) {…}

  上面的代码中ICloneable.Clone 和IComparable.CompareTo 就是显式接口成员执行体。

  说明:

  1、不能在方法调用、属性访问以及索引指示器访问中通过全权名访问显式接口成员执行体。事实上,显式接口成员执行体只能通过接口的实例,仅仅引用接口的成员名称来访问。

  2、显式接口成员执行体不能使用任何访问限制符,也不能加上abstract, virtual, override或static 修饰符。

  3、显式接口成员执行体和其他成员有着不同的访问方式。因为不能在方法调用、属性访问以及索引指示器访问中通过全权名访问,显式接口成员执行体在某种意义上是私有的。但它们又可以通过接口的实例访问,也具有一定的公有性质。

  4、只有类在定义时,把接口名写在了基类列表中,而且类中定义的全权名、类型和返回类型都与显式接口成员执行体完全一致时,显式接口成员执行体才是有效的,例如:

  1. class Shape: ICloneable
  2. {
  3.     object ICloneable.Clone( ) {…}
  4.     int IComparable.CompareTo(object other) {…}

  使用显式接口成员执行体通常有两个目的:

  1、因为显式接口成员执行体不能通过类的实例进行访问,这就可以从公有接口中把接口的实现部分单独分离开。如果一个类只在内部使用该接口,而类的使用者不会直接使用到该接口,这种显式接口成员执行体就可以起到作用。

  2、显式接口成员执行体避免了接口成员之间因为同名而发生混淆。如果一个类希望对名称和返回类型相同的接口成员采用不同的实现方式,这就必须要使用到显式接口成员执行体。如果没有显式接口成员执行体,那么对于名称和返回类型不同的接口成员,类也无法进行实现。

  下面的定义是无效的,因为Shape定义时基类列表中没有出现接口IComparable。

  1. class Shape: ICloneable
  2. {
  3.     object ICloneable.Clone( ) {…}
  4. }
  5. class Ellipse: Shape
  6. {
  7.     object ICloneable.Clone( ) {…}

  在Ellipse 中定义ICloneable.Clone是错误的,因为Ellipse即使隐式地实现了接口ICloneable,ICloneable仍然没有显式地出现在Ellipse定义的基类列表中。

  接口成员的全权名必须对应在接口中定义的成员。如下面的例子中,Paint的显式接口成员执行体必须写成IControl.Paint。

  1. using System;
  2. interface IControl
  3. {
  4.     void Paint( ) ;
  5. }
  6. interface ITextBox: IControl
  7. {
  8.     void SetText(string text) ;
  9. }
  10. class TextBox: ITextBox
  11. {
  12.     void IControl.Paint( ) {…}
  13.     void ITextBox.SetText(string text) {…}
  14. }

  实现接口的类可以显式实现该接口的成员。当显式实现某成员时,不能通过类实例访问该成员,而只能通过该接口的实例访问该成员。显式接口实现还允许程序员继承共享相同成员名的两个接口,并为每个接口成员提供一个单独的实现。

  下面例子中同时以公制单位和英制单位显示框的尺寸。Box类继承IEnglishDimensions和IMetricDimensions两个接口,它们表示不同的度量衡系统。两个接口有相同的成员名Length和Width。

  程序清单1 DemonInterface.cs

  1. interface IEnglishDimensions
  2. {
  3.     float Length();
  4.     float Width();
  5. }
  6. interface IMetricDimensions
  7. {
  8.     float Length();
  9.     float Width();
  10. }
  11. class Box : IEnglishDimensions, IMetricDimensions
  12. {
  13.     float lengthInches;
  14.     float widthInches;
  15.     public Box(float length, float width)
  16.     {
  17.         lengthInches = length;
  18.         widthInches = width;
  19.     }
  20.     float IEnglishDimensions.Length()
  21.     {
  22.         return lengthInches;
  23.     }
  24.     float IEnglishDimensions.Width()
  25.     {
  26.         return widthInches;
  27.     }
  28.     float IMetricDimensions.Length()
  29.     {
  30.         return lengthInches * 2.54f;
  31.     }
  32.     float IMetricDimensions.Width()
  33.     {
  34.         return widthInches * 2.54f;
  35.     }
  36.     public static void Main()
  37.     {
  38.         //定义一个实类对象 "myBox"::
  39.         Box myBox = new Box(30.0f, 20.0f);
  40.         // 定义一个接口" eDimensions"::
  41.         IEnglishDimensions eDimensions = (IEnglishDimensions)myBox;
  42.         IMetricDimensions mDimensions = (IMetricDimensions)myBox;
  43.         // 输出:
  44.         System.Console.WriteLine(" Length(in): {0}", eDimensions.Length());
  45.         System.Console.WriteLine(" Width (in): {0}", eDimensions.Width());
  46.         System.Console.WriteLine(" Length(cm): {0}", mDimensions.Length());
  47.         System.Console.WriteLine(" Width (cm): {0}", mDimensions.Width());
  48.     }
  49. }

  输出:Length(in): 30,Width (in): 20,Length(cm): 76.2,Width (cm): 50.8

  代码讨论:如果希望默认度量采用英制单位,请正常实现Length和Width这两个方法,并从IMetricDimensions接口显式实现Length和Width 方法:

  1. public float Length( )
  2. {
  3.     return lengthInches ;
  4. }
  5. public float Width( )
  6. {
  7.     return widthInches;
  8. }
  9. float IMetricDimensions.Length( ) 
  10. {
  11.     return lengthInches * 2.54f ;
  12. }
  13. float IMetricDimensions.Width( )
  14. {
  15.     return widthInches * 2.54f ;

  这种情况下,可以从类实例访问英制单位,而从接口实例访问公制单位:

  1. System.Console.WriteLine("Length(in): {0}", myBox.Length( ));
  2. System.Console.WriteLine("Width (in): {0}", myBox.Width( )); 
  3. System.Console.WriteLine("Length(cm): {0}", mDimensions.Length( ));
  4. System.Console.WriteLine("Width (cm): {0}", mDimensions.Width( )); 

  2、继承接口实现

  接口具有不变性,但这并不意味着接口不再发展。类似于类的继承性,接口也可以继承和发展。

  注意:接口继承和类继承不同,首先,类继承不仅是说明继承,而且也是实现继承;而接口继承只是说明继承。也就是说,派生类可以继承基类的方法实现,而派生的接口只继承了父接口的成员方法说明,而没有继承父接口的实现,其次,C#中类继承只允许单继承,但是接口继承允许多继承,一个子接口可以有多个父接口。

  接口可以从零或多个接口中继承。从多个接口中继承时,用":"后跟被继承的接口名字,多个接口名之间用","分割。被继承的接口应该是可以访问得到的,比如从private 类型或internal 类型的接口中继承就是不允许的。接口不允许直接或间接地从自身继承。和类的继承相似,接口的继承也形成接口之间的层次结构。

  请看下面的例子:

  1. using System ;
  2. interface IControl
  3. {
  4.     void Paint();
  5. }
  6. interface ITextBox : IControl
  7. {
  8.     void SetText(string text);
  9. }
  10. interface IListBox : IControl
  11. {
  12.     void SetItems(string[] items);
  13. }
  14. interface IComboBox : ITextBox, IListBox { }

  对一个接口的继承也就继承了接口的所有成员,上面的例子中接口ITextBox和IListBox都从接口IControl中继承,也就继承了接口IControl的Paint方法。接口IComboBox从接口ITextBox和IListBox中继承,因此它应该继承了接口ITextBox的SetText方法和IListBox的SetItems方法,还有IControl的Paint方法。
一个类继承了所有被它的基本类提供的接口实现程序。

  不通过显式的实现一个接口,一个派生类不能用任何方法改变它从它的基本类继承的接口映射。例如,在声明中

  1. interface IControl
  2. {
  3.     void Paint();
  4. }
  5. class Control : IControl
  6. {
  7.     public void Paint() {...}
  8. }
  9. class TextBox : Control
  10. {
  11.     new public void Paint() {...}
  12. }

  TextBox中的方法Paint隐藏了Control中的方法Paint,但是没有改变从Control.Paint到IControl.Paint的映射,而通过类实例和接口实例调用Paint将会有下面的影响

  1. Control c = new Control( ) ;
  2. TextBox t = new TextBox( ) ;
  3. IControl ic = c ;
  4. IControl it = t ;
  5. c.Paint( ) ; // 影响Control.Paint( ) ;
  6. t.Paint( ) ; // 影响TextBox.Paint( ) ;
  7. ic.Paint( ) ; // 影响Control.Paint( ) ;
  8. it.Paint( ) ; // 影响Control.Paint( ) ; 

  但是,当一个接口方法被映射到一个类中的虚拟方法,派生类就不可能覆盖这个虚拟方法并且改变接口的实现函数。例如,把上面的声明重新写为

  1. interface IControl
  2. {
  3.     void Paint();
  4. }
  5. class Control : IControl
  6. {
  7.     public virtual void Paint() {...}
  8. }
  9. class TextBox : Control
  10. {
  11.     public override void Paint() {...}
  12. }

  就会看到下面的结果:

  1. Control c = new Control( ) ;
  2. TextBox t = new TextBox( ) ;
  3. IControl ic = c ;
  4. IControl it = t ;
  5. c.Paint( ) ; // 影响Control.Paint( );
  6. t.Paint( ) ; // 影响TextBox.Paint( );
  7. ic.Paint( ) ; // 影响Control.Paint( );
  8. it.Paint( ) ; // 影响TextBox.Paint( );  

  由于显式接口成员实现程序不能被声明为虚拟的,就不可能覆盖一个显式接口成员实现程序。一个显式接口成员实现程序调用另外一个方法是有效的,而另外的那个方法可以被声明为虚拟的以便让派生类可以覆盖它。例如:

  1. interface IControl
  2. {
  3.     void Paint();
  4. }
  5. class Control : IControl
  6. {
  7.     void IControl.Paint() { PaintControl(); }
  8.     protected virtual void PaintControl() {...}
  9. }
  10. class TextBox : Control
  11. {
  12.     protected override void PaintControl() {...}
  13. }

  这里,从Control继承的类可以通过覆盖方法PaintControl来对IControl.Paint的实现程序进行特殊化。

  3、重新实现接口

  我们已经介绍过,派生类可以对基类中已经定义的成员方法进行重载。类似的概念引入到类对接口的实现中来,叫做接口的重实现(re-implementation)。继承了接口实现的类可以对接口进行重实现。这个接口要求是在类定义的基类列表中出现过的。对接口的重实现也必须严格地遵守首次实现接口的规则,派生的接口映射不会对为接口的重实现所建立的接口映射产生任何影响。

  下面的代码给出了接口重实现的例子:

  1. interface IControl
  2. {
  3.     void Paint();
  4. }
  5. class Control : IControl
  6. {
  7.     void IControl.Paint() { }
  8. }
  9. class MyControl : Control, IControl
  10. {
  11.     public void Paint() { }
  12. }

  实际上就是:Control把IControl.Paint映射到了Control.IControl.Paint上,但这并不影响在MyControl中的重实现。在MyControl中的重实现中,IControl.Paint被映射到MyControl.Paint之上。

  在接口的重实现时,继承而来的公有成员定义和继承而来的显式接口成员的定义参与到接口映射的过程。

  1. using System ;
  2. interface IMethods
  3. {
  4.     void F();
  5.     void G();
  6.     void H();
  7.     void I();
  8. }
  9. class Base : IMethods
  10. {
  11.     void IMethods.F() { }
  12.     void IMethods.G() { }
  13.     public void H() { }
  14.     public void I() { }
  15. }
  16. class Derived : Base, IMethods
  17. {
  18.     public void F() { }
  19.     void IMethods.H() { }
  20. }

  这里,接口IMethods在Derived中的实现把接口方法映射到了Derived.F,Base.IMethods.G, Derived.IMethods.H, 还有Base.I。前面我们说过,类在实现一个接口时,同时隐式地实现了该接口的所有父接口。同样,类在重实现一个接口时同时,隐式地重实现了该接口的所有父接口。

  1. using System ;
  2. interface IBase
  3. {
  4.     void F();
  5. }
  6. interface IDerived : IBase
  7. {
  8.     void G();
  9. }
  10. class C : IDerived
  11. {
  12.     void IBase.F()
  13.     {
  14.         //对F 进行实现的代码…
  15.     }
  16.     void IDerived.G()
  17.     {
  18.         //对G 进行实现的代码…
  19.     }
  20. }
  21. class D : C, IDerived
  22. {
  23.     public void F()
  24.     {
  25.         //对F 进行实现的代码…
  26.     }
  27.     public void G()
  28.     {
  29.         //对G 进行实现的代码…
  30.     }
  31. }

  这里,对IDerived的重实现也同样实现了对IBase的重实现,把IBase.F映射到了D.F。

  4、映射接口

  类必须为在基类表中列出的所有接口的成员提供具体的实现。在类中定位接口成员的实现称之为接口映射(interface mapping )。

  映射,数学上表示一一对应的函数关系。接口映射的含义也是一样,接口通过类来实现,那么对于在接口中定义的每一个成员,都应该对应着类的一个成员来为它提供具体的实现。

  类的成员及其所映射的接口成员之间必须满足下列条件:

  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:

  1. interface Interface1
  2. {
  3.     void F();
  4. }
  5. class Class1
  6. {
  7.     public void F() { }
  8.     public void G() { }
  9. }
  10. class Class2 : Class1, Interface1
  11. {
  12.     new public void G() { }
  13. }

  注意:接口的成员包括它自己定义的成员,而且包括该接口所有父接口定义的成员。在接口映射时,不仅要对接口定义体中显式定义的所有成员进行映射,而且要对隐式地从父接口那里继承来的所有接口成员进行映射。
  在进行接口映射时,还要注意下面两点:

  1、在决定由类中的哪个成员来实现接口成员时,类中显式说明的接口成员比其它成员优先实现。

  2、使用Private、protected和static修饰符的成员不能参与实现接口映射。例如:

  1. interface ICloneable 
  2. {
  3.  object Clone( ) ;
  4. }
  5. class C: ICloneable 
  6. {
  7.  object ICloneable.Clone( ) {…}
  8.  public object Clone( ) {…}

  例子中成员ICloneable.Clone称为接口ICloneable的成员Clone的实现者,因为它是显式说明的接口成员,比其它成员有着更高的优先权。

  如果一个类实现了两个或两个以上名字、类型和参数类型都相同的接口,那么类中的一个成员就可能实现所有这些接口成员:

  1. interface IControl 
  2. {
  3.  void Paint( ) ;
  4. }
  5. interface IForm 
  6. {
  7.  void Paint( ) ;
  8. }
  9. class Page: IControl, IForm
  10. {
  11.  public void Paint( ) {…}
  12. }  

  这里,接口IControl和IForm的方法Paint都映射到了类Page中的Paint方法。当然也可以分别用显式的接口成员分别实现这两个方法:

  1. interface IControl
  2. {
  3.     void Paint();
  4. }
  5. interface IForm
  6. {
  7.     void Paint();
  8. }
  9. class Page : IControl, IForm
  10. {
  11.     public void IControl.Paint()
  12.     {
  13.         //具体的接口实现代码
  14.     }
  15.     public void IForm.Paint()
  16.     {
  17.         //具体的接口实现代码
  18.     }
  19. }

  上面的两种写法都是正确的。但是如果接口成员在继承中覆盖了父接口的成员,那么对该接口成员的实现就可能必须映射到显式接口成员执行体。看下面的例子:

  1. interface IBase
  2. {
  3.     int P { get; }
  4. }
  5. interface IDerived : IBase
  6. {
  7.     new int P();
  8. }

  接口IDerived从接口IBase中继承,这时接口IDerived的成员方法覆盖了父接口的成员方法。因为这时存在着同名的两个接口成员,那么对这两个接口成员的实现如果不采用显式接口成员执行体,编译器将无法分辨接口映射。所以,如果某个类要实现接口IDerived,在类中必须至少定义一个显式接口成员执行体。采用下面这些写法都是合理的:

  1. interface IBase
  2. {
  3.     int P { get;}
  4. }
  5. interface IDerived:IBase
  6. {
  7.     new int P();
  8. }
  9. //一:对两个接口成员都采用显式接口成员执行体来实现
  10. class C : IDerived
  11. {
  12.     int IBase.P
  13.     {
  14.         get { }//具体的接口实现代码
  15.     }
  16.     int IDerived.P() { }//具体的接口实现代码
  17. }
  18. //二:对Ibase 的接口成员采用显式接口成员执行体来实现
  19. class D : IDerived
  20. {
  21.     int IBase.P
  22.     {
  23.         get { }//具体的接口实现代码
  24.     }
  25.     public int P() { }//具体的接口实现代码
  26. }
  27. //三:对IDerived 的接口成员采用显式接口成员执行体来实现
  28. class E : IDerived
  29. {
  30.     public int P
  31.     {
  32.         get { }//具体的接口实现代码
  33.     }
  34.     int IDerived.P() { }//具体的接口实现代码
  35. }

  另一种情况是,如果一个类实现了多个接口,这些接口又拥有同一个父接口,这个父接口只允许被实现一次。

  1. using System ;
  2. interface IControl
  3. {
  4.     void Paint();
  5. }
  6. interface ITextBox : IControl
  7. {
  8.     void SetText(string text);
  9. }
  10. interface IListBox : IControl
  11. {
  12.     void SetItems(string[] items);
  13. }
  14. class ComboBox : IControl, ITextBox, IListBox
  15. {
  16.     void IControl.Paint() { }
  17.     void ITextBox.SetText(string text) { }
  18.     void IListBox.SetItems(string[] items) { }
  19. }

  上面的例子中,类ComboBox实现了三个接口:IControl,ITextBox和IListBox。如果认为ComboBox不仅实现了IControl接口,而且在实现ITextBox和IListBox的同时,又分别实现了它们的父接口IControl。实际上,对接口ITextBox 和IListBox 的实现,分享了对接口IControl 的实现。

  我们对C#的接口有了较全面的认识,基本掌握了怎样应用C#的接口编程,但事实上,C#的不仅仅应用于.NET平台,它同样支持以前的COM,可以实现COM类到.NET类的转换,如C#调用API。欲了解这方面的知识,请看下一节-接口转换。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值