兰保明

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

假设你设计一个和人交流的程序。
先建立一个接口
interface 人 //定义接口,它代表一个人,
{void Hello(); }//接口虚函数,用来跟这个人说话

但不同的人有不用的交流方式,具体方式用类来实现,比如。
class 美国人:人 //继承接口“人”
然后,类里实例化接口函数
void Hello(){说hi;}

class 中国人:人 //继承接口“人”
然后,类里实例化接口函数
void Hello(){说你好;}

class SB:人 //sb也是人
实现 Hello{说xxxxx;}

最后你的程序运行时,就用接口“人”就可以了,
因为不管遇到什么人(美国人,中国人,还是sb),都可以和他们交流了,这就是接口的意义!!! 
  
基于Visual C#的接口基础教程

     组件化程序设计方法继承并发展了面向对象的程序设计方法。它把对象技术应用于系统设计,对面向对象的程序设计的实现过程作了进一步的抽象。
  接口是是组件化编程的一个重要内容,熟练掌握和使用接口将大大减轻编程的工作量,提高代码的可重用性,同时也能使你更深入的理解面向对象的思想。

第一节 接口慨述

  接口(interface)用来定义一种程序的协定。实现接口的类或者结构要与接口的定义严格一致。有了这个协定,就可以抛开编程语言的限制(理论上)。接口可以从多个基接口继承,而类或结构可以实现多个接口。接口可以包含方法、属性、事件和索引器。接口本身不提供它所定义的成员的实现。接口只指定实现该接口的类或接口必须提供的成员。
      接口好比一种模版,这种模版定义了对象必须实现的方法,其目的就是让这些方法可以作为接口实例被引用。接口不能被实例化。类可以实现多个接口并且通过这些实现的接口被索引。接口变量只能索引实现该接口的类的实例。例子:

 

interface IMyExample {
 string this[int index] { get ; set ; }
 event EventHandler Even ;
 void Find(int value) ;
 string Point { get ; set ; }
}
public delegate void EventHandler(object sender, Event e) ; 

 

  上面例子中的接口包含一个索引this、一个事件Even、一个方法Find和一个属性Point。

  接口可以支持多重继承。就像在下例中,接口"IComboBox"同时从"ITextBox"和"IListBox"继承。

 

interface IControl {
void Paint( ) ;
}
interface ITextBox: IControl {
void SetText(string text) ;
}
interface IListBox: IControl {
void SetItems(string[] items) ;
}
interface IComboBox: ITextBox, IListBox { } 

 

  类和结构可以多重实例化接口。就像在下例中,类"EditBox"继承了类"Control",同时从"IDataBound"和"IControl"继承。

 

interface IDataBound {
 void Bind(Binder b) ;
}
public class EditBox: Control, IControl, IDataBound {
 public void Paint( ) ;
 public void Bind(Binder b) {...}

 

  在上面的代码中,"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] interface identifier [:base-list] {interface-body}[;]


  说明:
       1、attributes(可选):附加的定义性信息。

  2、modifiers(可选): 允许使用的修饰符有 new 和四个访问修饰符。分别是:new、public、protected、internal、 private。在一个接口定义中同一修饰符不允许出现多次,new 修饰符只能出现在嵌套接口中,表示覆盖了继承而来的同名成员。The public, protected, internal, and private 修饰符定义了对接口的访问权限。

  3、指示器和事件。

  4、identifier:接口名称。

  5、base-list(可选):包含一个或多个显式基接口的列表,接口间由逗号分隔。

  6、interface-body:对接口成员的定义。

  7、接口可以是命名空间或类的成员,并且可以包含下列成员的签名: 方法、属性、索引器 。

  8、一个接口可从一个或多个基接口继承。

        接口这个概念在C#和Java中非常相似。接口的关键词是interface,一个接口可以扩展一个或者多个其他接口。按照惯例,接口的名字以大写字母"I"开头。下面的代码是C#接口的一个例子,它与Java中的接口完全一样:


 

interface IShape {
 void Draw ( ) ;
}


  如果你从两个或者两个以上的接口派生,父接口的名字列表用逗号分隔,如下面的代码所示:


interface INewInterface: IParent1, IParent2 { } 


  然而,与Java不同,C#中的接口不能包含域(Field)。另外还要注意,在C#中,接口内的所有方法默认都是公用方法。在Java中,方法定义可以带有public修饰符(即使这并非必要),但在C#中,显式为接口的方法指定public修饰符是非法的。例如,下面的C#接口将产生一个编译错误。


interface IShape { public void Draw( ) ; }


  下面的例子定义了一个名为IControl 的接口,接口中包含一个成员方法Paint:


interface IControl {
 void Paint( ) ;


  在下例中,接口 IInterface从两个基接口 IBase1 和 IBase2 继承:


interface IInterface: IBase1, IBase2 {
 void Method1( ) ;
 void Method2( ) ;


  接口可由类实现。实现的接口的标识符出现在类的基列表中。例如:


class Class1: Iface1, Iface2 {
 // class 成员。
}


  类的基列表同时包含基类和接口时,列表中首先出现的是基类。例如:


class ClassA: BaseClass, Iface1, Iface2 {
 // class成员。
}


  以下的代码段定义接口IFace,它只有一个方法:


interface IFace {
 void ShowMyFace( ) ;
}


  不能从这个定义实例化一个对象,但可以从它派生一个类。因此,该类必须实现ShowMyFace抽象方法:


class CFace:IFace
{
 public void ShowMyFace( ) {
  Console.WriteLine(" implementation " ) ;
 }

基接口

  一个接口可以从零或多个接口继承,那些被称为这个接口的显式基接口。当一个接口有比零多的显式基接口时,那么在接口的定义中的形式为,接口标识符后面跟着由一个冒号":"和一个用逗号","分开的基接口标识符列表。

  接口基:

  :接口类型列表说明:

  1、一个接口的显式基接口必须至少同接口本身一样可访问。例如,在一个公共接口的基接口中指定一个私有或内部的接口是错误的。

  2、一个接口直接或间接地从它自己继承是错误的。

  3、接口的基接口都是显式基接口,并且是它们的基接口。换句话说,基接口的集合完全由显式基接口和它们的显式基接口等等组成。在下面的例子中
interface IControl {
 void Paint( ) ;
}
interface ITextBox: IControl {
 void SetText(string text) ;
}
interface IListBox: IControl {
 void SetItems(string[] items) ;
}
interface IComboBox: 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 这些成员:

 

interface IMyTest{
 string this[int index] { get; set; }
 event EventHandler E ;
 void F(int value) ;
 string P { get; set; }
}
public delegate void EventHandler(object sender, EventArgs e) ;

 

  下面例子中接口IStringList包含每个可能类型成员的接口:一个方法,一个属性,一个事件和一个索引。

 

public delegate void StringListEvent(IStringList sender);
public interface IStringList
{
 void Add(string s);
 int Count { get; }
 event StringListEvent Changed;
 string this[int index] { get; set; }
}

 

  接口成员的全权名

  使用接口成员也可采用全权名(fully qualified name)。接口的全权名称是这样构成的。接口名加小圆点"." 再跟成员名比如对于下面两个接口:

 

interface IControl {
 void Paint( ) ;
}
interface ITextBox: IControl {
 void GetText(string text) ;
}

 

  其中Paint 的全权名是IControl.Paint,GetText的全权名是ITextBox. GetText。当然,全权名中的成员名称必须是在接口中已经定义过的,比如使用ITextBox.Paint.就是不合理的。

  如果接口是名字空间的成员,全权名还必须包含名字空间的名称。

 

namespace System
{
 public interface IDataTable {
  object Clone( ) ;
 }
}

 

  那么Clone方法的全权名是System. IDataTable.Clone。

  定义好了接口,接下来就是怎样访问接口,请看下一节--访问接口

第四节、访问接口

  对接口成员的访问

  对接口方法的调用和采用索引指示器访问的规则与类中的情况也是相同的。如果底层成员的命名与继承而来的高层成员一致,那么底层成员将覆盖同名的高层成员。但由于接口支持多继承,在多继承中,如果两个父接口含有同名的成员,这就产生了二义性(这也正是C#中取消了类的多继承机制的原因之一),这时需要进行显式的定义:

 

using System ;
interface ISequence {
 int Count { get; set; }
}
interface IRing {
 void Count(int i) ;
}
interface IRingSequence: ISequence, IRing { }
 class CTest {
  void Test(IRingSequence rs) {
   //rs.Count(1) ; 错误, Count 有二义性
   //rs.Count = 1; 错误, Count 有二义性
   ((ISequence)rs).Count = 1; // 正确
   ((IRing)rs).Count(1) ; // 正确调用IRing.Count
  }
}

 

  上面的例子中,前两条语句rs .Count(1)和rs .Count = 1会产生二义性,从而导致编译时错误,因此必须显式地给rs 指派父接口类型,这种指派在运行时不会带来额外的开销。

  再看下面的例子:

 

using System ;
interface IInteger {
 void Add(int i) ;
}
interface IDouble {
 void Add(double d) ;
}
interface INumber: IInteger, IDouble {}
 class CMyTest {
 void Test(INumber Num) {
  // Num.Add(1) ; 错误
  Num.Add(1.0) ; // 正确
  ((IInteger)n).Add(1) ; // 正确
  ((IDouble)n).Add(1) ; // 正确
 }
}

 

  调用Num.Add(1) 会导致二义性,因为候选的重载方法的参数类型均适用。但是,调用Num.Add(1.0) 是允许的,因为1.0 是浮点数参数类型与方法IInteger.Add()的参数类型不一致,这时只有IDouble.Add 才是适用的。不过只要加入了显式的指派,就决不会产生二义性。

  接口的多重继承的问题也会带来成员访问上的问题。例如:

 

interface IBase {
 void FWay(int i) ;
}
interface ILeft: IBase {
 new void FWay (int i) ;
}
interface IRight: IBase
{ void G( ) ; }
interface IDerived: ILeft, IRight { }
class CTest {
 void Test(IDerived d) {
  d. FWay (1) ; // 调用ILeft. FWay
  ((IBase)d). FWay (1) ; // 调用IBase. FWay
  ((ILeft)d). FWay (1) ; // 调用ILeft. FWay
  ((IRight)d). FWay (1) ; // 调用IBase. FWay
 }
}

 

  上例中,方法IBase.FWay在派生的接口ILeft中被Ileft的成员方法FWay覆盖了。所以对d. FWay (1)的调用实际上调用了。虽然从IBase-> IRight-> IDerived这条继承路径上来看,ILeft.FWay方法是没有被覆盖的。我们只要记住这一点:一旦成员被覆盖以后,所有对其的访问都被覆盖以后的成员"拦截"了。

  类对接口的实现

  前面我们已经说过,接口定义不包括方法的实现部分。接口可以通过类或结构来实现。我们主要讲述通过类来实现接口。用类来实现接口时,接口的名称必须包含在类定义中的基类列表中。

  下面的例子给出了由类来实现接口的例子。其中ISequence 为一个队列接口,提供了向队列尾部添加对象的成员方法Add( ),IRing 为一个循环表接口,提供了向环中插入对象的方法Insert(object obj),方法返回插入的位置。类RingSquence 实现了接口ISequence 和接口IRing。

 

using System ;
interface ISequence {
 object Add( ) ;
}
interface ISequence {
 object Add( ) ;
}
interface IRing {
 int Insert(object obj) ;
}
class RingSequence: ISequence, IRing
{
 public object Add( ) {…}
 public int Insert(object obj) {…}
}

 

  如果类实现了某个接口,类也隐式地继承了该接口的所有父接口,不管这些父接口有没有在类定义的基类表中列出。看下面的例子:

 

using System ;
interface IControl {
 void Paint( );
}
interface ITextBox: IControl {
 void SetText(string text);
}
interface IListBox: IControl {
 void SetItems(string[] items);
}
interface IComboBox: ITextBox, IListBox { }

 

  这里, 接口IcomboBox继承了ItextBox和IlistBox。类TextBox不仅实现了接口ITextBox,还实现了接口ITextBox 的父接口IControl。

  前面我们已经看到,一个类可以实现多个接口。再看下面的例子:

 

interface IDataBound {
 void Bind(Binder b);
}
public class EditBox: Control, IControl, IDataBound {
 public void Paint( );
 public void Bind(Binder b) {...}

 

  类EditBox从类Control中派生并且实现了Icontrol和IdataBound。在前面的例子中接口Icontrol中的Paint方法和IdataBound接口中的Bind方法都用类EditBox中的公共成员实现。C#提供一种实现这些方法的可选择的途径,这样可以使执行这些的类避免把这些成员设定为公共的。接口成员可以用有效的名称来实现。例如,类EditBox可以改作方法Icontrol.Paint和IdataBound.Bind来来实现。

 

public class EditBox: IControl, IDataBound {
 void IControl.Paint( ) {...}
 void IDataBound.Bind(Binder b) {...}
}

 

  因为通过外部指派接口成员实现了每个成员,所以用这种方法实现的成员称为外部接口成员。外部接口成员可以只是通过接口来调用。例如,Paint方法中EditBox的实现可以只是通过创建Icontrol接口来调用。

 

class Test {
 static void Main( ) {
  EditBox editbox = new EditBox( );
  editbox.Paint( ); //错误: EditBox 没有Paint 事件
  IControl control = editbox;
  control.Paint( ); // 调用 EditBox的Paint事件
 }
}

 

  上例中,类EditBox 从Control 类继承并同时实现了IControl and IDataBound 接口。EditBox 中的Paint 方法来自IControl 接口,Bind 方法来自IDataBound 接口,二者在EditBox 类中都作为公有成员实现。当然,在C# 中我们也可以选择不作为公有成员实现接口。

  如果每个成员都明显地指出了被实现的接口,通过这种途径被实现的接口我们称之为显式接口成员(explicit interface member)。 用这种方式我们改写上面的例子:

 

public class EditBox: IControl, IDataBound {
 void IControl.Paint( ) {…}
 void IDataBound.Bind(Binder b) {…}
}

 

  显式接口成员只能通过接口调用。例如:

 

class CTest {
 static void Main( ) {
  EditBox editbox = new EditBox( ) ;
  editbox.Paint( ) ; //错误:不同的方法
  IControl control = editbox;
  control.Paint( ) ; //调用 EditBox的Paint方法
 }
}

 

  上述代码中对editbox.Paint( )的调用是错误的,因为editbox 本身并没有提供这一方法。control.Paint( )是正确的调用方式。

  注释:接口本身不提供所定义的成员的实现,它仅仅说明这些成员,这些成员必须依靠实现接口的类或其它接口的支持。

  知道了怎样访问接口,我们还要知道怎样实现接口,要实现C#的接口,请看下一节-实现接口

第五节、实现接口

  1、显式实现接口成员

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

 

using System ;
interface ICloneable {
 object Clone( ) ;
}
interface IComparable {
 int CompareTo(object other) ;
}
class ListEntry: ICloneable, IComparable {
 object ICloneable.Clone( ) {…}
 int IComparable.CompareTo(object other) {…}
}

 

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

       说明:

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

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

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

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

 

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

 


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

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

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

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

 

class Shape: ICloneable
{
object ICloneable.Clone( ) {…}
}
class Ellipse: Shape
{
object ICloneable.Clone( ) {…}
}

 

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

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

 

using System ;
interface IControl
{
 void Paint( ) ;
}
interface ITextBox: IControl
{
 void SetText(string text) ;
}
class TextBox: ITextBox
{
 void IControl.Paint( ) {…}
 void ITextBox.SetText(string text) {…}
}
 

 

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

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

  程序清单1 DemonInterface.cs

 

interface IEnglishDimensions {
float Length ( ) ;
float Width ( ) ;
}
interface IMetricDimensions {
float Length ( ) ;
float Width ( ) ;
}
class Box : IEnglishDimensions, IMetricDimensions {
float lengthInches ;
float widthInches ;
public Box(float length, float width) {
lengthInches = length ;
widthInches = width ;
}
float IEnglishDimensions.Length( ) {
return lengthInches ;
}
float IEnglishDimensions.Width( ) {
return widthInches ;
}
float IMetricDimensions.Length( ) {
return lengthInches * 2.54f ;
}
float IMetricDimensions.Width( ) {
return widthInches * 2.54f ;
}
public static void Main( ) {
//定义一个实类对象 "myBox"::
Box myBox = new Box(30.0f, 20.0f);
// 定义一个接口" eDimensions"::
IEnglishDimensions eDimensions = (IEnglishDimensions) myBox;
IMetricDimensions mDimensions = (IMetricDimensions) myBox;
// 输出:
System.Console.WriteLine(" Length(in): {0}", eDimensions.Length( ));
System.Console.WriteLine(" Width (in): {0}", eDimensions.Width( ));
System.Console.WriteLine(" Length(cm): {0}", mDimensions.Length( ));
System.Console.WriteLine(" Width (cm): {0}", mDimensions.Width( ));
}
}

 

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

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

 

public float Length( ) {
return lengthInches ;
}
public float Width( ){
return widthInches;
}
float IMetricDimensions.Length( ) {
return lengthInches * 2.54f ;
}
float IMetricDimensions.Width( ) {
return widthInches * 2.54f ;
}

 

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

 

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( )) ;

 

  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、显式实现接口成员


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


using System ;
interface ICloneable {
object Clone( ) ;
}
interface IComparable {
int CompareTo(object other) ;
}
class ListEntry: ICloneable, IComparable {
object ICloneable.Clone( ) {…}
int IComparable.CompareTo(object other) {…}
}


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


说明:


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


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


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


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


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


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


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


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


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


class Shape: ICloneable
{
object ICloneable.Clone( ) {…}
}
class Ellipse: Shape
{
object ICloneable.Clone( ) {…}
}


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


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


using System ;
interface IControl
{
void Paint( ) ;
}
interface ITextBox: IControl
{
void SetText(string text) ;
}
class TextBox: ITextBox
{
void IControl.Paint( ) {…}
void ITextBox.SetText(string text) {…}
}


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


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


程序清单1 DemonInterface.cs


interface IEnglishDimensions {
float Length ( ) ;
float Width ( ) ;
}
interface IMetricDimensions {
float Length ( ) ;
float Width ( ) ;
}
class Box : IEnglishDimensions, IMetricDimensions {
float lengthInches ;
float widthInches ;
public Box(float length, float width) {
lengthInches = length ;
widthInches = width ;
}
float IEnglishDimensions.Length( ) {
return lengthInches ;
}
float IEnglishDimensions.Width( ) {
return widthInches ;
}
float IMetricDimensions.Length( ) {
return lengthInches * 2.54f ;
}
float IMetricDimensions.Width( ) {
return widthInches * 2.54f ;
}
public static void Main( ) {
//定义一个实类对象 "myBox"::
Box myBox = new Box(30.0f, 20.0f);
// 定义一个接口" eDimensions"::
IEnglishDimensions eDimensions = (IEnglishDimensions) myBox;
IMetricDimensions mDimensions = (IMetricDimensions) myBox;
// 输出:
System.Console.WriteLine(" Length(in): {0}", eDimensions.Length( ));
System.Console.WriteLine(" Width (in): {0}", eDimensions.Width( ));
System.Console.WriteLine(" Length(cm): {0}", mDimensions.Length( ));
System.Console.WriteLine(" Width (cm): {0}", mDimensions.Width( ));
}
}


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


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


public float Length( ) {
return lengthInches ;
}
public float Width( ){
return widthInches;
}
float IMetricDimensions.Length( ) {
return lengthInches * 2.54f ;
}
float IMetricDimensions.Width( ) {
return widthInches * 2.54f ;
}


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


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 类型的接口中继承就是不允许的。接口不允许直接或间接地从自身继承。和类的继承相似,接口的继承也形成接口之间的层次结构。


请看下面的例子:


using System ;
interface IControl {
void Paint( ) ;
}
interface ITextBox: IControl {
void SetText(string text) ;
}
interface IListBox: IControl {
void SetItems(string[] items) ;
}
interface IComboBox: ITextBox, IListBox { }


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


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


interface IControl {
void Paint( );
}
class Control: IControl {
public void Paint( ) {...}
}
class TextBox: Control {
new public void Paint( ) {...}
}


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


Control c = new Control( ) ;
TextBox t = new TextBox( ) ;
IControl ic = c ;
IControl it = t ;
c.Paint( ) ; // 影响Control.Paint( ) ;
t.Paint( ) ; // 影响TextBox.Paint( ) ;
ic.Paint( ) ; // 影响Control.Paint( ) ;
it.Paint( ) ; // 影响Control.Paint( ) ;


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


interface IControl {
void Paint( ) ;
}
class Control: IControl {
public virtual void Paint( ) {...}
}
class TextBox: Control {
public override void Paint( ) {...}
}


就会看到下面的结果:


Control c = new Control( ) ;
TextBox t = new TextBox( ) ;
IControl ic = c ;
IControl it = t ;
c.Paint( ) ; // 影响Control.Paint( );
t.Paint( ) ; // 影响TextBox.Paint( );
ic.Paint( ) ; // 影响Control.Paint( );
it.Paint( ) ; // 影响TextBox.Paint( );


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


interface IControl {
void Paint( ) ;
}
class Control: IControl {
void IControl.Paint( ) { PaintControl( ); }
protected virtual void PaintControl( ) {...}
}
class TextBox: Control {
protected override void PaintControl( ) {...}
}


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


3、重新实现接口


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


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


interface IControl {
void Paint( ) ;
class Control: IControl
void IControl.Paint( ) {…}
class MyControl: Control, IControl
public void Paint( ) {}
}


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


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


using System ;
interface IMethods {
void F( ) ;
void G( ) ;
void H( ) ;
void I( ) ;
}
class Base: IMethods {
void IMethods.F( ) { }
void IMethods.G( ) { }
public void H( ) { }
public void I( ) { }
}
class Derived: Base, IMethods {
public void F( ) { }
void IMethods.H( ) { }
}


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


using System ;
interface IBase {
void F( ) ;
}
interface IDerived: IBase {
void G( ) ;
}
class C: IDerived {
void IBase.F( ) {
//对F 进行实现的代码…
}
void IDerived.G( ) {
//对G 进行实现的代码…
}
}
class D: C, IDerived {
public void F( ) {
//对F 进行实现的代码…
}
public void G( ) {
//对G 进行实现的代码…
}
}


这里,对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:


interface Interface1 {
void F( ) ;
}
class Class1 {
public void F( ) { }
public void G( ) { }
}
class Class2: Class1, Interface1 {
new public void G( ) {}
}


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


在进行接口映射时,还要注意下面两点:


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


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


interface ICloneable {
object Clone( ) ;
}
class C: ICloneable {
object ICloneable.Clone( ) {…}
public object Clone( ) {…}
}


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


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


interface IControl {
void Paint( ) ;
}
interface IForm {
void Paint( ) ;
}
class Page: IControl, IForm {
public void Paint( ) {…}
}


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


interface IControl {
void Paint( ) ;
}
interface IForm {
void Paint( ) ;
}
class Page: IControl, IForm {
public void IControl.Paint( ) {
//具体的接口实现代码
}
public void IForm.Paint( ) {
//具体的接口实现代码
}
}


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


interface IBase {
int P { get; }
}
interface IDerived: IBase {
new int P( ) ;
}


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


//一:对两个接口成员都采用显式接口成员执行体来实现
lass C: IDerived {
int IBase.P
get
{ //具体的接口实现代码 }
int IDerived.P( ){
//具体的接口实现代码 }
}
//二:对Ibase 的接口成员采用显式接口成员执行体来实现
class C: IDerived {
int IBase.P
get {//具体的接口实现代码}
public int P( ){
//具体的接口实现代码 }
}
//三:对IDerived 的接口成员采用显式接口成员执行体来实现
class C: IDerived{
public int P
get {//具体的接口实现代码}
int IDerived.P( ){
//具体的接口实现代码}
}


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


using System ;
interface IControl {
void Paint( ) ;
interface ITextBox: IControl {
void SetText(string text) ;
}
interface IListBox: IControl {
void SetItems(string[] items) ;
}
class ComboBox: IControl, ITextBox, IListBox {
void IControl.Paint( ) {…}
void ITextBox.SetText(string text) {…}
void IListBox.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#接口基础(6) 2009-07-30 16:44 第四节、访问接口

对接口成员的访问


对接口方法的调用和采用索引指示器访问的规则与类中的情况也是相同的。如果底层成员的命名与继承而来的高层成员一致,那么底层成员将覆盖同名的高层成 员。但由于接口支持多继承,在多继承中,如果两个父接口含有同名的成员,这就产生了二义性(这也正是C#中取消了类的多继承机制的原因之一),这时需要进 行显式的定义:

 

using System ;
interface ISequence {
int Count { get; set; }
}
interface IRing {
void Count(int i) ;
}
interface IRingSequence: ISequence, IRing { }
class CTest {
void Test(IRingSequence rs) {
//rs.Count(1) ; 错误, Count 有二义性
//rs.Count = 1; 错误, Count 有二义性
((ISequence)rs).Count = 1; // 正确
((IRing)rs).Count(1) ; // 正确调用IRing.Count
}
}


上面的例子中,前两条语句rs .Count(1)和rs .Count = 1会产生二义性,从而导致编译时错误,因此必须显式地给rs 指派父接口类型,这种指派在运行时不会带来额外的开销。


再看下面的例子:


using System ;
interface IInteger {
void Add(int i) ;
}
interface IDouble {
void Add(double d) ;
}
interface INumber: IInteger, IDouble {}
class CMyTest {
void Test(INumber Num) {
// Num.Add(1) ; 错误
Num.Add(1.0) ; // 正确
((IInteger)n).Add(1) ; // 正确
((IDouble)n).Add(1) ; // 正确
}
}


调用Num.Add(1) 会导致二义性,因为候选的重载方法的参数类型均适用。但是,调用Num.Add(1.0) 是允许的,因为1.0 是浮点数参数类型与方法IInteger.Add()的参数类型不一致,这时只有IDouble.Add 才是适用的。不过只要加入了显式的指派,就决不会产生二义性。


接口的多重继承的问题也会带来成员访问上的问题。例如:


interface IBase {
void FWay(int i) ;
}
interface ILeft: IBase {
new void FWay (int i) ;
}
interface IRight: IBase
{ void G( ) ; }
interface IDerived: ILeft, IRight { }
class CTest {
void Test(IDerived d) {
d. FWay (1) ; // 调用ILeft. FWay
((IBase)d). FWay (1) ; // 调用IBase. FWay
((ILeft)d). FWay (1) ; // 调用ILeft. FWay
((IRight)d). FWay (1) ; // 调用IBase. FWay
}
}


上例中,方法IBase.FWay在派生的接口ILeft中被Ileft的成员方法FWay覆盖了。所以对d. FWay (1)的调用实际上调用了。虽然从IBase-> IRight-> IDerived这条继承路径上来看,ILeft.FWay方法是没有被覆盖的。我们只要记住这一点:一旦成员被覆盖以后,所有对其的访问都被覆盖以后的 成员"拦截"了。


类对接口的实现


前面我们已经说过,接口定义不包括方法的实现部分。接口可以通过类或结构来实现。我们主要讲述通过类来实现接口。用类来实现接口时,接口的名称必须包含在类定义中的基类列表中。


下面的例子给出了由类来实现接口的例子。其中ISequence 为一个队列接口,提供了向队列尾部添加对象的成员方法Add( ),IRing 为一个循环表接口,提供了向环中插入对象的方法Insert(object obj),方法返回插入的位置。类RingSquence 实现了接口ISequence 和接口IRing。


using System ;
interface ISequence {
object Add( ) ;
}
interface ISequence {
object Add( ) ;
}
interface IRing {
int Insert(object obj) ;
}
class RingSequence: ISequence, IRing
{
public object Add( ) {…}
public int Insert(object obj) {…}
}


如果类实现了某个接口,类也隐式地继承了该接口的所有父接口,不管这些父接口有没有在类定义的基类表中列出。看下面的例子:


using System ;
interface IControl {
void Paint( );
}
interface ITextBox: IControl {
void SetText(string text);
}
interface IListBox: IControl {
void SetItems(string[] items);
}
interface IComboBox: ITextBox, IListBox { }


这里, 接口IcomboBox继承了ItextBox和IlistBox。类TextBox不仅实现了接口ITextBox,还实现了接口ITextBox 的父接口IControl。


前面我们已经看到,一个类可以实现多个接口。再看下面的例子:


interface IDataBound {
void Bind(Binder b);
}
public class EditBox: Control, IControl, IDataBound {
public void Paint( );
public void Bind(Binder b) {...}
}


类EditBox从类Control中派生并且实现了Icontrol和IdataBound。在前面的例子中接口Icontrol中的Paint方 法和IdataBound接口中的Bind方法都用类EditBox中的公共成员实现。C#提供一种实现这些方法的可选择的途径,这样可以使执行这些的类 避免把这些成员设定为公共的。接口成员可以用有效的名称来实现。例如,类EditBox可以改作方法Icontrol.Paint和 IdataBound.Bind来来实现。


public class EditBox: IControl, IDataBound {
void IControl.Paint( ) {...}
void IDataBound.Bind(Binder b) {...}
}


因为通过外部指派接口成员实现了每个成员,所以用这种方法实现的成员称为外部接口成员。外部接口成员可以只是通过接口来调用。例如,Paint方法中EditBox的实现可以只是通过创建Icontrol接口来调用。


class Test {
static void Main( ) {
EditBox editbox = new EditBox( );
editbox.Paint( ); //错误: EditBox 没有Paint 事件
IControl control = editbox;
control.Paint( ); // 调用 EditBox的Paint事件
}
}


上例中,类EditBox 从Control 类继承并同时实现了IControl and IDataBound 接口。EditBox 中的Paint 方法来自IControl 接口,Bind 方法来自IDataBound 接口,二者在EditBox 类中都作为公有成员实现。当然,在C# 中我们也可以选择不作为公有成员实现接口。


如果每个成员都明显地指出了被实现的接口,通过这种途径被实现的接口我们称之为显式接口成员(explicit interface member)。 用这种方式我们改写上面的例子:


public class EditBox: IControl, IDataBound {
void IControl.Paint( ) {…}
void IDataBound.Bind(Binder b) {…}
}


显式接口成员只能通过接口调用。例如:


class CTest {
static void Main( ) {
EditBox editbox = new EditBox( ) ;
editbox.Paint( ) ; //错误:不同的方法
IControl control = editbox;
control.Paint( ) ; //调用 EditBox的Paint方法
}
}


上述代码中对editbox.Paint( )的调用是错误的,因为editbox 本身并没有提供这一方法。control.Paint( )是正确的调用方式。


注释:接口本身不提供所定义的成员的实现,它仅仅说明这些成员,这些成员必须依靠实现接口的类或其它接口的支持。


知道了怎样访问接口,我们还要知道怎样实现接口,要实现C#的接口,请看下一节-实现接口


类对接口的实现


前面我们已经说过,接口定义不包括方法的实现部分。接口可以通过类或结构来实现。我们主要讲述通过类来实现接口。用类来实现接口时,接口的名称必须包含在类定义中的基类列表中。


下面的例子给出了由类来实现接口的例子。其中ISequence 为一个队列接口,提供了向队列尾部添加对象的成员方法Add( ),IRing 为一个循环表接口,提供了向环中插入对象的方法Insert(object obj),方法返回插入的位置。类RingSquence 实现了接口ISequence 和接口IRing。


using System ;
interface ISequence {
object Add( ) ;
}
interface ISequence {
object Add( ) ;
}
interface IRing {
int Insert(object obj) ;
}
class RingSequence: ISequence, IRing
{
public object Add( ) {…}
public int Insert(object obj) {…}
}


如果类实现了某个接口,类也隐式地继承了该接口的所有父接口,不管这些父接口有没有在类定义的基类表中列出。看下面的例子:


using System ;
interface IControl {
void Paint( );
}
interface ITextBox: IControl {
void SetText(string text);
}
interface IListBox: IControl {
void SetItems(string[] items);
}
interface IComboBox: ITextBox, IListBox { }


这里, 接口IcomboBox继承了ItextBox和IlistBox。类TextBox不仅实现了接口ITextBox,还实现了接口ITextBox 的父接口IControl。


前面我们已经看到,一个类可以实现多个接口。再看下面的例子:


interface IDataBound {
void Bind(Binder b);
}
public class EditBox: Control, IControl, IDataBound {
public void Paint( );
public void Bind(Binder b) {...}
}


类EditBox从类Control中派生并且实现了Icontrol和IdataBound。在前面的例子中接口Icontrol中的Paint方 法和IdataBound接口中的Bind方法都用类EditBox中的公共成员实现。C#提供一种实现这些方法的可选择的途径,这样可以使执行这些的类 避免把这些成员设定为公共的。接口成员可以用有效的名称来实现。例如,类EditBox可以改作方法Icontrol.Paint和 IdataBound.Bind来来实现。


public class EditBox: IControl, IDataBound {
void IControl.Paint( ) {...}
void IDataBound.Bind(Binder b) {...}
}


因为通过外部指派接口成员实现了每个成员,所以用这种方法实现的成员称为外部接口成员。外部接口成员可以只是通过接口来调用。例如,Paint方法中EditBox的实现可以只是通过创建Icontrol接口来调用。


class Test {
static void Main( ) {
EditBox editbox = new EditBox( );
editbox.Paint( ); //错误: EditBox 没有Paint 事件
IControl control = editbox;
control.Paint( ); // 调用 EditBox的Paint事件
}
}


上例中,类EditBox 从Control 类继承并同时实现了IControl and IDataBound 接口。EditBox 中的Paint 方法来自IControl 接口,Bind 方法来自IDataBound 接口,二者在EditBox 类中都作为公有成员实现。当然,在C# 中我们也可以选择不作为公有成员实现接口。


如果每个成员都明显地指出了被实现的接口,通过这种途径被实现的接口我们称之为显式接口成员(explicit interface member)。 用这种方式我们改写上面的例子:


public class EditBox: IControl, IDataBound {
void IControl.Paint( ) {…}
void IDataBound.Bind(Binder b) {…}
}


显式接口成员只能通过接口调用。例如:


class CTest {
static void Main( ) {
EditBox editbox = new EditBox( ) ;
editbox.Paint( ) ; //错误:不同的方法
IControl control = editbox;
control.Paint( ) ; //调用 EditBox的Paint方法
}
}


上述代码中对editbox.Paint( )的调用是错误的,因为editbox 本身并没有提供这一方法。control.Paint( )是正确的调用方式。


注释:接口本身不提供所定义的成员的实现,它仅仅说明这些成员,这些成员必须依靠实现接口的类或其它接口的支持。
 

 第一节 接口慨述

接口(interface)用来定义一种程序的协定。实现接口的类或者结构要与接口的定义严格一致。
有了这个协定,就可以抛开编程语言的限制(理论 上)。接口可以从多个基接口继承,
而类或结构可以实现多个接口。接口可以包含方法、属性、事件和索引器。
接口本身不提供它所定义的成员的实现。接口只指定 实现该接口的类或接口必须提供的成员。


接口好比一种模版,这种模版定义了对象必须实现的方法,其目的就是让这些方法可以作为接口实例被引用。
接口不能被实例化。类可以实现多个接口并且通过这些实现的接口被索引。
接口变量只能索引实现该接口的类的实例。例子:


interface IMyExample {
string this[int index] { get ; set ; }
event EventHandler Even ;
void Find(int value) ;
string Point { get ; set ; }
}
public delegate void EventHandler(object sender, Event e) ;


上面例子中的接口包含一个索引this、一个事件Even、一个方法Find和一个属性Point。


接口可以支持多重继承。就像在下例中,接口"IComboBox"同时从"ITextBox"和"IListBox"继承。


interface IControl {
void Paint( ) ;
}
interface ITextBox: IControl {
void SetText(string text) ;
}
interface IListBox: IControl {
void SetItems(string[] items) ;
}
interface IComboBox: ITextBox, IListBox { }


类和结构可以多重实例化接口。就像在下例中,类"EditBox"继承了类"Control",
同时从"IDataBound"和"IControl"继承。


interface IDataBound {
void Bind(Binder b) ;
}
public class EditBox: Control, IControl, IDataBound {
public void Paint( ) ;
public void Bind(Binder b) {...}
}


在上面的代码中,"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] interface identifier [:base-list] {interface-body}[;]


说明:


1、attributes(可选):附加的定义性信息。


2、modifiers(可选): 允许使用的修饰符有 new 和四个访问修饰符。分别是:new、public、protected、internal、 private。

在一个接口定义中同一修饰符不允许出现多次,new 修饰符只能出现在嵌套接口中,表示覆盖了继承而来的同名成员。

The public, protected, internal, and private 修饰符定义了对接口的访问权限。


3、指示器和事件。


4、identifier:接口名称。


5、base-list(可选):包含一个或多个显式基接口的列表,接口间由逗号分隔。


6、interface-body:对接口成员的定义。


7、接口可以是命名空间或类的成员,并且可以包含下列成员的签名: 方法、属性、索引器 。


8、一个接口可从一个或多个基接口继承。 


接口这个概念在C#和Java中非常相似。接口的关键词是interface,一个接口可以扩展一个或者多个其他接口。

按照惯例,接口的名字以大写字母"I"开头。下面的代码是C#接口的一个例子,它与Java中的接口完全一样:

 

interface IShape
{
void Draw ( ) ;
}


如果你从两个或者两个以上的接口派生,父接口的名字列表用逗号分隔,如下面的代码所示:

 

interface INewInterface: IParent1, IParent2
 {
 }


然而,与Java不同,C#中的接口不能包含域(Field)。另外还要注意,在C#中,接口内的所有方法默认都是公用方法。

在Java中,方法定义 可以带有public修饰符(即使这并非必要),但在C#中,显式为接口的方法指定public修饰符是非法的。

例如,下面的C#接口将产生一个编译错 误。

 

interface IShape { public void Draw( ) ; }


下面的例子定义了一个名为IControl 的接口,接口中包含一个成员方法Paint:

 

interface IControl
 {
void Paint( ) ;
}


在下例中,接口 IInterface从两个基接口 IBase1 和 IBase2 继承:

 

interface IInterface: IBase1, IBase2
{
void Method1( ) ;
void Method2( ) ;
}


接口可由类实现。实现的接口的标识符出现在类的基列表中。例如:

 

class Class1: Iface1, Iface2
 {
// class 成员。
}


类的基列表同时包含基类和接口时,列表中首先出现的是基类。例如:

 

class ClassA: BaseClass, Iface1, Iface2
 {
// class成员。
}


以下的代码段定义接口IFace,它只有一个方法:

 

interface IFace
 {
void ShowMyFace( ) ;
}


不能从这个定义实例化一个对象,但可以从它派生一个类。因此,该类必须实现ShowMyFace抽象方法:

 

class CFace:IFace
{
public void ShowMyFace( )
 {
Console.WriteLine(" implementation " ) ;
}
}


基接口


一个接口可以从零或多个接口继承,那些被称为这个接口的显式基接口。

当一个接口有比零多的显式基接口时,那么在接口的定义中的形式为,

接口标识符后面跟着由一个冒号":"和一个用逗号","分开的基接口标识符列表。


接口基:


:接口类型列表说明:


1、一个接口的显式基接口必须至少同接口本身一样可访问。例如,在一个公共接口的基接口中指定一个私有或内部的接口是错误的。


2、一个接口直接或间接地从它自己继承是错误的。


3、接口的基接口都是显式基接口,并且是它们的基接口。

换句话说,基接口的集合完全由显式基接口和它们的显式基接口等等组成。在下面的例子中
interface IControl {
void Paint( ) ;
}
interface ITextBox: IControl {
void SetText(string text) ;
}
interface IListBox: IControl {
void SetItems(string[] items) ;
}
interface IComboBox: 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 这些成员:


interface IMyTest{
string this[int index] { get; set; }
event EventHandler E ;
void F(int value) ;
string P { get; set; }
}
public delegate void EventHandler(object sender, EventArgs e) ;


下面例子中接口IStringList包含每个可能类型成员的接口:一个方法,一个属性,一个事件和一个索引。


public delegate void StringListEvent(IStringList sender);
public interface IStringList
{
void Add(string s);
int Count { get; }
event StringListEvent Changed;
string this[int index] { get; set; }
}


接口成员的全权名


使用接口成员也可采用全权名(fully qualified name)。接口的全权名称是这样构成的。

接口名加小圆点"." 再跟成员名比如对于下面两个接口:


interface IControl {
void Paint( ) ;
}
interface ITextBox: IControl {
void GetText(string text) ;
}


其中Paint 的全权名是IControl.Paint,GetText的全权名是ITextBox. GetText。当然,全权名中的成员名称必须是在接口中已经定义过的,比如使用ITextBox.Paint.就是不合理的。


如果接口是名字空间的成员,全权名还必须包含名字空间的名称。


namespace System
{
public interface IDataTable {
object Clone( ) ;
}
}


那么Clone方法的全权名是System. IDataTable.Clone。


定义好了接口,接下来就是怎样访问接口,请看下一节--访问接口 

 

假设你设计一个和人交流的程序。
先建立一个接口 interface 人 //定义接口,它代表一个人,

{void Hello(); }//接口虚函数,用来跟这个人说话 但不同的人有不用的交流方式,具体方式用类来实现,比如。

 class 美国人:人 //继承接口“人” 然后,类里实例化接口函数

void Hello(){说hi;}

class 中国人:人 //继承接口“人” 然后,类里实例化接口函数 void Hello(){说你好;}

 class SB:人 //sb也是人 实现 Hello{说xxxxx;} 最后你的程序运行时,就用接口“人”就可以了,

因为不管遇到什么人(美国人,中国人,还是sb),都可以和他们交流了,这就是接口的意义!!!
 


1、显式实现接口成员

为了实现接口,类可以定义显式接口成员执行体(Explicit interface member implementations)。

显式接口成员执行体可以是一个方法、一个属性、一个事件或者是一个索引指示器的定义,定义与该成员对应的全权名应保持一致。


using System ;
interface ICloneable
{
object Clone( ) ;
}

interface IComparable
{
int CompareTo(object other) ;
}
class ListEntry: ICloneable, IComparable
{
object ICloneable.Clone( )
 {…}

int IComparable.CompareTo(object other)
{…}

}


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


说明:


1、不能在方法调用、属性访问以及索引指示器访问中通过全权名访问显式接口成员执行体。

事实上,显式接口成员执行体只能通过接口的实例,仅仅引用接口的成员名称来访问。


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


3、显式接口成员执行体和其他成员有着不同的访问方式。因为不能在方法调用、

属性访问以及索引指示器访问中通过全权名访问,显式接口成员执行体在某种意义上是私有的。

但它们又可以通过接口的实例访问,也具有一定的公有性质。


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


class Shape: ICloneable
{
object ICloneable.Clone( )
{…}

int IComparable.CompareTo(object other)
 {…}
}


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


1、因为显式接口成员执行体不能通过类的实例进行访问,这就可以从公有接口中把接口的实现部分单独分离开。

如果一个类只在内部使用该接口,而类的使用者不会直接使用到该接口,这种显式接口成员执行体就可以起到作用。


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


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


class Shape: ICloneable
{
object ICloneable.Clone( ) {…}
}
class Ellipse: Shape
{
object ICloneable.Clone( ) {…}
}


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


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


using System ;
interface IControl
{
void Paint( ) ;
}
interface ITextBox: IControl
{
void SetText(string text) ;
}
class TextBox: ITextBox
{
void IControl.Paint( ) {…}
void ITextBox.SetText(string text) {…}
}

 

实现接口的类可以显式实现该接口的成员。当显式实现某成员时,不能通过类实例访问该成员,

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


下面例子中同时以公制单位和英制单位显示框的尺寸。Box类继承 IEnglishDimensions和 IMetricDimensions两个接口,

它们表示不同的度量衡系统。两个接口有相同的成员名 Length 和 Width。


程序清单1 DemonInterface.cs


interface IEnglishDimensions {
float Length ( ) ;
float Width ( ) ;
}
interface IMetricDimensions {
float Length ( ) ;
float Width ( ) ;
}
class Box : IEnglishDimensions, IMetricDimensions {
float lengthInches ;
float widthInches ;
public Box(float length, float width) {
lengthInches = length ;
widthInches = width ;
}
float IEnglishDimensions.Length( ) {
return lengthInches ;
}
float IEnglishDimensions.Width( ) {
return widthInches ;
}
float IMetricDimensions.Length( ) {
return lengthInches * 2.54f ;
}
float IMetricDimensions.Width( ) {
return widthInches * 2.54f ;
}
public static void Main( ) {
//定义一个实类对象 "myBox"::
Box myBox = new Box(30.0f, 20.0f);
// 定义一个接口" eDimensions"::
IEnglishDimensions eDimensions = (IEnglishDimensions) myBox;
IMetricDimensions mDimensions = (IMetricDimensions) myBox;
// 输出:
System.Console.WriteLine(" Length(in): {0}", eDimensions.Length( ));
System.Console.WriteLine(" Width (in): {0}", eDimensions.Width( ));
System.Console.WriteLine(" Length(cm): {0}", mDimensions.Length( ));
System.Console.WriteLine(" Width (cm): {0}", mDimensions.Width( ));
}
}


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


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

并从 IMetricDimensions 接口显式实现 Length 和 Width 方法:


public float Length( ) {
return lengthInches ;
}
public float Width( ){
return widthInches;
}
float IMetricDimensions.Length( ) {
return lengthInches * 2.54f ;
}
float IMetricDimensions.Width( ) {
return widthInches * 2.54f ;
}


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


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 类型的接口中继承就是不允许的。接口不允许直接或间接地从自身继承。
和类的继承相似,接口的继承也形成接口之间的层次结构。


请看下面的例子:


using System ;
interface IControl {
void Paint( ) ;
}
interface ITextBox: IControl {
void SetText(string text) ;
}
interface IListBox: IControl {
void SetItems(string[] items) ;
}
interface IComboBox: ITextBox, IListBox { }


对一个接口的继承也就继承了接口的所有成员,上面的例子中接口ITextBox和IListBox都从接口IControl中继承,

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


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


interface IControl {
void Paint( );
}
class Control: IControl {
public void Paint( ) {...}
}
class TextBox: Control {
new public void Paint( ) {...}
}


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


Control c = new Control( ) ;
TextBox t = new TextBox( ) ;
IControl ic = c ;
IControl it = t ;
c.Paint( ) ; // 影响Control.Paint( ) ;
t.Paint( ) ; // 影响TextBox.Paint( ) ;
ic.Paint( ) ; // 影响Control.Paint( ) ;
it.Paint( ) ; // 影响Control.Paint( ) ;


但是,当一个接口方法被映射到一个类中的虚拟方法,派生类就不可能覆盖这个虚拟方法并且改变接口的实现函数。

例如,把上面的声明重新写为


interface IControl {
void Paint( ) ;
}
class Control: IControl {
public virtual void Paint( ) {...}
}
class TextBox: Control {
public override void Paint( ) {...}
}


就会看到下面的结果:


Control c = new Control( ) ;
TextBox t = new TextBox( ) ;
IControl ic = c ;
IControl it = t ;
c.Paint( ) ; // 影响Control.Paint( );
t.Paint( ) ; // 影响TextBox.Paint( );
ic.Paint( ) ; // 影响Control.Paint( );
it.Paint( ) ; // 影响TextBox.Paint( );


由于显式接口成员实现程序不能被声明为虚拟的,就不可能覆盖一个显式接口成员实现程序。

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


interface IControl {
void Paint( ) ;
}
class Control: IControl {
void IControl.Paint( ) { PaintControl( ); }
protected virtual void PaintControl( ) {...}
}
class TextBox: Control {
protected override void PaintControl( ) {...}
}


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


3、重新实现接口


我们已经介绍过,派生类可以对基类中已经定义的成员方法进行重载。类似的概念引入到类对接口的实现中来,

叫做接口的重实现(re- implementation)。继承了接口实现的类可以对接口进行重实现。

这个接口要求是在类定义的基类列表中出现过的。对接口的重实现也必须严格地遵 守首次实现接口的规则,

派生的接口映射不会对为接口的重实现所建立的接口映射产生任何影响。


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


interface IControl {
void Paint( ) ;
class Control: IControl
void IControl.Paint( ) {…}
class MyControl: Control, IControl
public void Paint( ) {}
}


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

在MyControl中的重实现中,IControl.Paint被映射到MyControl.Paint 之上。


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


using System ;
interface IMethods {
void F( ) ;
void G( ) ;
void H( ) ;
void I( ) ;
}
class Base: IMethods {
void IMethods.F( ) { }
void IMethods.G( ) { }
public void H( ) { }
public void I( ) { }
}
class Derived: Base, IMethods {
public void F( ) { }
void IMethods.H( ) { }
}


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


using System ;
interface IBase {
void F( ) ;
}
interface IDerived: IBase {
void G( ) ;
}
class C: IDerived {
void IBase.F( ) {
//对F 进行实现的代码…
}
void IDerived.G( ) {
//对G 进行实现的代码…
}
}
class D: C, IDerived {
public void F( ) {
//对F 进行实现的代码…
}
public void G( ) {
//对G 进行实现的代码…
}
}


这里,对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:


interface Interface1 {
void F( ) ;
}
class Class1 {
public void F( ) { }
public void G( ) { }
}
class Class2: Class1, Interface1 {
new public void G( ) {}
}


注意:接口的成员包括它自己定义的成员,而且包括该接口所有父接口定义的成员。在接口映射时,

不仅要对接口定义体中显式定义的所有成员进行映射,而且要对隐式地从父接口那里继承来的所有接口成员进行映射。


在进行接口映射时,还要注意下面两点:


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


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


interface ICloneable {
object Clone( ) ;
}
class C: ICloneable {
object ICloneable.Clone( ) {…}
public object Clone( ) {…}
}


例子中成员ICloneable.Clone 称为接口ICloneable 的成员Clone 的实现者,因为它是显式说明的接口成员,

比其它成员有着更高的优先权。


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


interface IControl {
void Paint( ) ;
}
interface IForm {
void Paint( ) ;
}
class Page: IControl, IForm {
public void Paint( ) {…}
}


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


interface IControl {
void Paint( ) ;
}
interface IForm {
void Paint( ) ;
}
class Page: IControl, IForm {
public void IControl.Paint( ) {
//具体的接口实现代码
}
public void IForm.Paint( ) {
//具体的接口实现代码
}
}


上面的两种写法都是正确的。但是如果接口成员在继承中覆盖了父接口的成员,

那么对该接口成员的实现就可能必须映射到显式接口成员执行体。看下面的例子:


interface IBase {
int P { get; }
}
interface IDerived: IBase {
new int P( ) ;
}


接口IDerived从接口IBase中继承,这时接口IDerived 的成员方法覆盖了父接口的成员方法。

因为这时存在着同名的两个接口成员,那么对这两个接口成员的实现如果不采用显式接口成员执行体,

编译器将无法分辨接口 映射。所以,如果某个类要实现接口IDerived,在类中必须至少定义一个显式接口成员执行体。

采用下面这些写法都是合理的:


//一:对两个接口成员都采用显式接口成员执行体来实现
lass C: IDerived {
int IBase.P
get
{ //具体的接口实现代码 }
int IDerived.P( ){
//具体的接口实现代码 }
}
//二:对Ibase 的接口成员采用显式接口成员执行体来实现
class C: IDerived {
int IBase.P
get {//具体的接口实现代码}
public int P( ){
//具体的接口实现代码 }
}
//三:对IDerived 的接口成员采用显式接口成员执行体来实现
class C: IDerived{
public int P
get {//具体的接口实现代码}
int IDerived.P( ){
//具体的接口实现代码}
}


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


using System ;
interface IControl {
void Paint( ) ;
interface ITextBox: IControl {
void SetText(string text) ;
}
interface IListBox: IControl {
void SetItems(string[] items) ;
}
class ComboBox: IControl, ITextBox, IListBox {
void IControl.Paint( ) {…}
void ITextBox.SetText(string text) {…}
void IListBox.SetItems(string[] items) {…}
}


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


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

 
对于各位使用面向对象编程语言的程序员来说,“接口”这个名词一定不陌生,

但是不知各位有没有这样的疑惑:接口有什么用途?它和抽象类有什么区别?能不能用抽象类代替接口呢?而且,

作为程序员,一定经常听到“面向接口编程”这个短语,

那么它是什么意思?有什么思想内涵?和面向对象编程是什么关系?本文将一一解答这些疑问。
 
  1.面向接口编程和面向对象编程是什么关系

  首先,面向接口编程和面向对象编程并不是平级的,
它并不是比面向对象编程更先进的一种独立的编程思想,而是附属于面向对象思想体系,
属于其一部分。或者说,它是面向对象编程体系中的思想精髓之一。

  2.接口的本质

  接口,在表面上是由几个没有主体代码的方法定义组成的集合体,有唯一的名称,
可以被类或其他接口所实现(或者也可以说继承)。它在形式上可能是如下的样子:

  

以下是引用片段:
interface InterfaceName
  {
  void Method1();
  void Method2(int para1);
  void Method3(string para2,string para3);
  }
  那么,接口的本质是什么呢?或者说接口存在的意义是什么。我认为可以从以下两个视角考虑:
  1)接口是一组规则的集合,它规定了实现本接口的类或接口必须拥有的一组规则。
体现了自然界“如果你是……则必须能……”的理念。

  例如,在自然界中,人都能吃饭,即“如果你是人,则必须能吃饭”。
那么模拟到计算机程序中,就应该有一个IPerson(习惯上,接口名由 “I”开头)接口,
并有一个方法叫Eat(),然后我们规定,每一个表示“人”的类,必须实现IPerson接口,
这就模拟了自然界“如果你是人,则必须能吃饭”这条规则。

  从这里,我想各位也能看到些许面向对象思想的东西。面向对象思想的核心之一,就是模拟真实世界,
把真实世界中的事物抽象成类,整个程序靠各个类的实例互相通信、互相协作完成系统功能,
这非常符合真实世界的运行状况,也是面向对象思想的精髓。

  2)接口是在一定粒度视图上同类事物的抽象表示。注意这里我强调了在一定粒度视图上,
因为“同类事物”这个概念是相对的,它因为粒度视图不同而不同。

  例如,在我的眼里,我是一个人,和一头猪有本质区别,我可以接受我和我同学是同类这个说法,
但绝不能接受我和一头猪是同类。但是,如果在一个动物学家眼里,我和猪应该是同类,因为我们都是动物,
他可以认为“人”和“猪”都实现了IAnimal这个接口,而他在研究动物行为时,不会把我和猪分开对待,
而会从“动物”这个较大的粒度上研究,但他会认为我和一棵树有本质区别。

  现在换了一个遗传学家,情况又不同了,因为生物都能遗传,所以在他眼里,我不仅和猪没区别,
和一只蚊子、一个细菌、一颗树、一个蘑菇乃至一个 SARS病毒都没什么区别,
因为他会认为我们都实现了IDescendable这个接口(注:descend vi. 遗传),即我们都是可遗传的东西,
他不会分别研究我们,而会将所有生物作为同类进行研究,在他眼里没有人和病毒之分,
只有可遗传的物质和不可遗传的物质。但至少,我和一块石头还是有区别的。

  可不幸的事情发生了,某日,地球上出现了一位伟大的人,他叫列宁,他在熟读马克思、
恩格斯的辩证唯物主义思想巨著后,颇有心得,于是他下了一个著名的定义:所谓物质,
就是能被意识所反映的客观实在。至此,我和一块石头、一丝空气、
一条成语和传输手机信号的电磁场已经没什么区别了,因为在列宁的眼里,
我们都是可以被意识所反映的客观实在。如果列宁是一名程序员,他会这么说:
所谓物质,就是所有同时实现了“IReflectabe”和“IEsse” 两个接口的类所生成的实例。
(注:reflect v. 反映 esse n. 客观实在)

  也许你会觉得我上面的例子像在瞎掰,但是,这正是接口得以存在的意义。
面向对象思想和核心之一叫做多态性,

什么叫多态性?说白了就是在某个粒度视图层面上对同类事物不加区别的对待而统一处理。

而之所以敢这样做,就是因为有接口的存在。像那个遗传学家,他明白所有生物都实现了 IDescendable接口,

那只要是生物,一定有Descend()这个方法,于是他就可以统一研究,而不至于分别研究每一种生物而最终累死。


  可能这里还不能给你一个关于接口本质和作用的直观印象。那么在后文的例子和对几个设计模式的解析中,
你将会更直观体验到接口的内涵。

  3.面向接口编程综述

  通过上文,我想大家对接口和接口的思想内涵有了一个了解,那么什么是面向接口编程呢?我个人的定义是:

在系统分析和架构中,分清层次和依赖关系,每个层次不是直接向其上层提供服务(即不是直接实例化在上层中),

而是通过定义一组接口,仅向上层暴露其接口功能,上层对于下层仅仅是接口依赖,而不依赖具体类。


  这样做的好处是显而易见的,首先对系统灵活性大有好处。当下层需要改变时,只要接口及接口功能不变
,则上层不用做任何修改。甚至可以在不改动上层代码时将下层整个替换掉,
就像我们将一个WD的60G硬盘换成一个希捷的160G的硬盘,计算机其他地方不用做任何改动,而是把原硬盘拔下来、
新硬盘插上就行了,因为计算机其他部分不依赖具体硬盘,而只依赖一个IDE接口,
只要硬盘实现了这个接口,就可以替换上去。从这里看,程序中的接口和现实中的接口极为相似,
所以我一直认为,接口(interface)这个词用的真是神似!

  使用接口的另一个好处就是不同部件或层次的开发人员可以并行开工,就像造硬盘的不用等造CPU的,
也不用等造显示器的,只要接口一致,设计合理,完全可以并行进行开发,从而提高效率。

  本篇文章先到这里。最后我想再啰嗦一句:面向对象的精髓是模拟现实,这也可以说是我这篇文章的灵魂。
所以,多从现实中思考面向对象的东西,对提高系统分析设计能力大有脾益。

  下篇文章,我将用一个实例来展示接口编程的基本方法。

  而第三篇,我将解析经典设计模式中的一些面向接口编程思想,并解析一下.NET分层架构中的面向接口思想。

  对本文的补充:


  1.关于“面向接口编程”中的“接口”与具体面向对象语言中“接口”两个词

  看到有朋友提出“面向接口编程”中的“接口”二字应该比单纯编程语言中的interface范围更大。
我经过思考,觉得很有道理。这里我写的确实不太合理。我想,面向对象语言中的“接口”是指具体的一种代码结构
,例如C#中用interface关键字定义的接口。而“面向接口编程”中的“接口” 可以说是一种从软件架构的角度、
从一个更抽象的层面上指那种用于隐藏具体底层类和实现多态性的结构部件。从这个意义上说,
如果定义一个抽象类,并且目的是为了实现多态,那么我认为把这个抽象类也称为“接口”是合理的。
但是用抽象类实现多态合理不合理?在下面第二条讨论。

  概括来说,我觉得两个“接口”的概念既相互区别又相互联系。
“面向接口编程”中的接口是一种思想层面的用于实现多态性、提高软件灵活性和可维护性的架构部件,
而具体语言中的“接口”是将这种思想中的部件具体实施到代码里的手段。

  2.关于抽象类与接口

  看到回复中这是讨论的比较激烈的一个问题。很抱歉我考虑不周没有在文章中讨论这个问题。
我个人对这个问题的理解如下:

  如果单从具体代码来看,对这两个概念很容易模糊,甚至觉得接口就是多余的,
因为单从具体功能来看,除多重继承外(C#,Java中),抽象类似乎完全能取代接口。
但是,难道接口的存在是为了实现多重继承?当然不是。我认为,抽象类和接口的区别在于使用动机。
使用抽象类是为了代码的复用,而使用接口的动机是为了实现多态性。所以,
如果你在为某个地方该使用接口还是抽象类而犹豫不决时,那么可以想想你的动机是什么。

  看到有朋友对IPerson这个接口的质疑,我个人的理解是,IPerson这个接口该不该定义,
关键看具体应用中是怎么个情况。如果我们的项目中有Women和Man,都继承Person,
而且Women和Man绝大多数方法都相同,只有一个方法DoSomethingInWC()不同(例子比较粗俗,各位见谅)
,那么当然定义一个AbstractPerson抽象类比较合理,因为它可以把其他所有方法都包含进去,
子类只定义 DoSomethingInWC(),大大减少了重复代码量。

  但是,如果我们程序中的Women和Man两个类基本没有共同代码,而且有一个PersonHandle类需要实例化他们,
并且不希望知道他们是男是女,而只需把他们当作人看待,并实现多态,那么定义成接口就有必要了。

  总而言之,接口与抽象类的区别主要在于使用的动机,而不在于其本身。
而一个东西该定义成抽象类还是接口,要根据具体环境的上下文决定。

  再者,我认为接口和抽象类的另一个区别在于,抽象类和它的子类之间应该是一般和特殊的关系,
而接口仅仅是它的子类应该实现的一组规则。(当然,有时也可能存在一般与特殊的关系,
但我们使用接口的目的不在这里)如,交通工具定义成抽象类,汽车、飞机、轮船定义成子类,
是可以接受的,因为汽车、飞机、轮船都是一种特殊的交通工具。再譬如Icomparable接口,
它只是说,实现这个接口的类必须要可以进行比较,这是一条规则。如果Car这个类实现了Icomparable,
只是说,我们的Car中有一个方法可以对两个Car的实例进行比较,可能是比哪辆车更贵,也可能比哪辆车更大,
这都无所谓,但我们不能说“汽车是一种特殊的可以比较”,这在文法上都不通。
 

 

--------------------------------------------------------------------------------

定义:现在我们要开发一个应用,模拟移动存储设备的读写,即计算机与U盘、MP3、移动硬盘等设备进行数据交换。

上下文(环境):已知要实现U盘、MP3播放器、移动硬盘三种移动存储设备,要求计算机能同这三种设备进行数据交换,
并且以后可能会有新的第三方的移动存储设备,所以计算机必须有扩展性,
能与目前未知而以后可能会出现的存储设备进行数据交换。各个存储设备间读、写的实现方法不同,
U盘和移动硬盘只有这两个方法,MP3Player还有一个PlayMusic方法。

名词定义:数据交换={读,写}

看到上面的问题,我想各位脑子中一定有了不少想法,这是个很好解决的问题,很多方案都能达到效果。
下面,我列举几个典型的方案。

解决方案列举


--------------------------------------------------------------------------------

方案一:分别定义FlashDisk、MP3Player、MobileHardDisk三个类,实现各自的Read和Write方法。
然后在Computer类中实例化上述三个类,为每个类分别写读、写方法。
例如,为FlashDisk写ReadFromFlashDisk、WriteToFlashDisk两个方法。总共六个方法。

方案二:定义抽象类MobileStorage,在里面写虚方法Read和Write,
三个存储设备继承此抽象类,并重写Read和Write方法。
Computer类中包含一个类型为MobileStorage的成员变量,并为其编写get/set器,
这样Computer中只需要两个方法:ReadData和WriteData,并通过多态性实现不同移动设备的读写。

方案三:与方案二基本相同,只是不定义抽象类,而是定义接口IMobileStorage,
移动存储器类实现此接口。Computer中通过依赖接口IMobileStorage实现多态性。

方案四:定义接口IReadable和IWritable,两个接口分别只包含Read和Write,
然后定义接口IMobileStorage接口继承自IReadable和IWritable,剩下的实现与方案三相同。

下面,我们来分析一下以上四种方案:

首先,方案一最直白,实现起来最简单,但是它有一个致命的弱点:可扩展性差。
或者说,不符合“开放-关闭原则”(注:意为对扩展开放,对修改关闭)。
当将来有了第三方扩展移动存储设备时,必须对Computer进行修改。这就如在一个真实的计算机上,
为每一种移动存储设备实现一个不同的插口、并分别有各自的驱动程序。当有了一种新的移动存储设备后,我们就要将计算机大卸八块,然后增加一个新的插口,在编写一套针对此新设备的驱动程序。这种设计显然不可取。

此方案的另一个缺点在于,冗余代码多。如果有100种移动存储,
那我们的Computer中岂不是要至少写200个方法,这是不能接受的!

我们再来看方案二和方案三,之所以将这两个方案放在一起讨论,
是因为他们基本是一个方案(从思想层面上来说),只不过实现手段不同,
一个是使用了抽象类,一个是使用了接口,而且最终达到的目的应该是一样的。

我们先来评价这种方案:首先它解决了代码冗余的问题,因为可以动态替换移动设备,
并且都实现了共同的接口,所以不管有多少种移动设备,只要一个Read方法和一个Write方法,
多态性就帮我们解决问题了。而对第一个问题,由于可以运行时动态替换,而不必将移动存储类硬编码在Computer中,所以有了新的第三方设备,完全可以替换进去运行。这就是所谓的“依赖接口,而不是依赖与具体类”,不信你看看,Computer类只有一个MobileStorage类型或IMobileStorage类型的成员变量,至于这个变量具体是什么类型,它并不知道,这取决于我们在运行时给这个变量的赋值。如此一来,Computer和移动存储器类的耦合度大大下降。

那么这里该选抽象类还是接口呢?还记得第一篇文章我对抽象类和接口选择的建议吗?看动机。
这里,我们的动机显然是实现多态性而不是为了代码复用,所以当然要用接口。

最后我们再来看一看方案四,它和方案三很类似,只是将“可读”和“可写”两个规则分别抽象成了接口,
然后让IMobileStorage再继承它们。这样做,显然进一步提高了灵活性,但是,这有没有设计过度的嫌疑呢?
我的观点是:这要看具体情况。如果我们的应用中可能会出现一些类,这些类只实现读方法或只实现写方法,
如只读光盘,那么这样做也是可以的。如果我们知道以后出现的东西都是能读又能写的,
那这两个接口就没有必要了。其实如果将只读设备的Write方法留空或抛出异常,也可以不要这两个接口。

总之一句话:理论是死的,人是活的,一切从现实需要来,防止设计不足,也要防止设计过度。

在这里,我们姑且认为以后的移动存储都是能读又能写的,所以我们选方案三。

实现


--------------------------------------------------------------------------------

下面,我们要将解决方案加以实现。我选择的语言是C#,但是在代码中不会用到C#特有的性质,
所以使用其他语言的朋友一样可以参考。

首先编写IMobileStorage接口:

Code:IMobileStorage

1namespace InterfaceExample
2{
3    public interface IMobileStorage
4    {
5        void Read();//从自身读数据
6        void Write();//将数据写入自身
7    }
8}

代码比较简单,只有两个方法,没什么好说的,接下来是三个移动存储设备的具体实现代码:

U盘

Code:FlashDisk

1namespace InterfaceExample
2{
3    public class FlashDisk : IMobileStorage
4    {
5        public void Read()
6        {
7            Console.WriteLine("Reading from FlashDisk……");
8            Console.WriteLine("Read finished!");
9        }
10
11        public void Write()
12        {
13            Console.WriteLine("Writing to FlashDisk……");
14            Console.WriteLine("Write finished!");
15        }
16    }
17}
MP3

Code:MP3Player

1namespace InterfaceExample
2{
3    public class MP3Player : IMobileStorage
4    {
5        public void Read()
6        {
7            Console.WriteLine("Reading from MP3Player……");
8            Console.WriteLine("Read finished!");
9        }
10
11        public void Write()
12        {
13            Console.WriteLine("Writing to MP3Player……");
14            Console.WriteLine("Write finished!");
15        }
16
17        public void PlayMusic()
18        {
19            Console.WriteLine("Music is playing……");
20        }
21    }
22}
移动硬盘

Code:MobileHardDisk

1namespace InterfaceExample
2{
3    public class MobileHardDisk : IMobileStorage
4    {
5        public void Read()
6        {
7            Console.WriteLine("Reading from MobileHardDisk……");
8            Console.WriteLine("Read finished!");
9        }
10
11        public void Write()
12        {
13            Console.WriteLine("Writing to MobileHardDisk……");
14            Console.WriteLine("Write finished!");
15        }
16    }
17}
可以看到,它们都实现了IMobileStorage接口,并重写了各自不同的Read和Write方法。
下面,我们来写Computer:

Code:Computer

1namespace InterfaceExample
2{
3    public class Computer
4    {
5        private IMobileStorage _usbDrive;
6
7        public IMobileStorage UsbDrive
8        {
9            get
10            {
11                return this._usbDrive;
12            }
13            set
14            {
15                this._usbDrive = value;
16            }
17        }
18
19        public Computer()
20        {
21        }
22
23        public Computer(IMobileStorage usbDrive)
24        {
25            this.UsbDrive = usbDrive;
26        }
27   
28        public void ReadData()
29        {
30            this._usbDrive.Read();
31        }
32
33        public void WriteData()
34        {
35            this._usbDrive.Write();
36        }
37    }
38}
其中的UsbDrive就是可替换的移动存储设备,之所以用这个名字,是为了让大家觉得直观,
就像我们平常使用电脑上的USB插口插拔设备一样。

OK!下面我们来测试我们的“电脑”和“移动存储设备”是否工作正常。
我是用的C#控制台程序,具体代码如下:

Code:测试代码

1namespace InterfaceExample
2{
3    class Program
4    {
5        static void Main(string[] args)
6        {
7            Computer computer = new Computer();
8            IMobileStorage mp3Player = new MP3Player();
9            IMobileStorage flashDisk = new FlashDisk();
10            IMobileStorage mobileHardDisk = new MobileHardDisk();
11
12            Console.WriteLine("I inserted my MP3 Player into my computer and copy some music to it:");
13            computer.UsbDrive = mp3Player;
14            computer.WriteData();
15            Console.WriteLine();
16
17            Console.WriteLine("Well,I also want to copy a great movie to my computer from a mobile hard disk:");
18            computer.UsbDrive = mobileHardDisk;
19            computer.ReadData();
20            Console.WriteLine();
21
22            Console.WriteLine("OK!I have to read some files from my flash disk and copy another file to it:");
23            computer.UsbDrive = flashDisk;
24            computer.ReadData();
25            computer.WriteData();
26            Console.ReadLine();
27        }
28    }
29}
现在编译、运行程序,如果没有问题,将看到如下运行结果:

 


图2.1 各种移动存储设备测试结果

好的,看来我们的系统工作良好。

后来……


--------------------------------------------------------------------------------


刚过了一个星期,就有人送来了新的移动存储设备NewMobileStorage,让我测试能不能用,我微微一笑,
心想这不是小菜一碟,让我们看看面向接口编程的威力吧!将测试程序修改成如下:

Code:测试代码

1namespace InterfaceExample
2{
3    class Program
4    {
5        static void Main(string[] args)
6        {
7            Computer computer = new Computer();
8            IMobileStorage newMobileStorage = new NewMobileStorage();
9
10            Console.WriteLine("Now,I am testing the new mobile storage:");
11            computer.UsbDrive = newMobileStorage;
12            computer.ReadData();
13            computer.WriteData();
14            Console.ReadLine();
15        }
16    }
17}
编译、运行、看结果:

哈哈,神奇吧,Computer一点都不用改动,就可以使新的设备正常运行。这就是所谓“对扩展开放,对修改关闭”。

 

图2.2 新设备扩展测试结果

又过了几天,有人通知我说又有一个叫SuperStorage的移动设备要接到我们的Computer上,
我心想来吧,管你是“超级存储”还是“特级存储”,我的“面向接口编程大法”把你们统统搞定。

但是,当设备真的送来,我傻眼了,开发这个新设备的团队没有拿到我们的IMobileStorage接口,
自然也没有遵照这个约定。这个设备的读、写方法不叫Read和Write,而是叫rd和wt,
这下完了……不符合接口啊,插不上。但是,不要着急,我们回到现实来找找解决的办法。
我们一起想想:如果你的Computer上只有USB接口,而有人拿来一个PS/2的鼠标要插上用,你该怎么办?
想起来了吧,是不是有一种叫“PS/2-USB”转换器的东西?也叫适配器,可以进行不同接口的转换。
对了!程序中也有转换器。

这里,我要引入一个设计模式,叫“Adapter”。它的作用就如现实中的适配器一样,
把接口不一致的两个插件接合起来。由于本篇不是讲设计模式的,而且Adapter设计模式很好理解,
所以我就不细讲了,先来看我设计的类图吧:

如图所示,虽然SuperStorage没有实现IMobileStorage,但我们定义了一个实现IMobileStorage的SuperStorageAdapter,
它聚合了一个SuperStorage,并将rd和wt适配为Read和Write,SuperStorageAdapter

 

图2.3 Adapter模式应用示意

具体代码如下:

Code:SuperStorageAdapter

1namespace InterfaceExample
2{
3    public class SuperStorageAdapter : IMobileStorage
4    {
5        private SuperStorage _superStorage;
6
7        public SuperStorage SuperStorage
8        {
9            get
10            {
11                return this._superStorage;
12            }
13            set
14            {
15                this._superStorage = value;
16            }
17        }
18   
19        public void Read()
20        {
21            this._superStorage.rd();
22        }
23
24        public void Write()
25        {
26            this._superStorage.wt();
27        }
28    }
29}
好,现在我们来测试适配过的新设备,测试代码如下:

Code:测试代码

1namespace InterfaceExample
2{
3    class Program
4    {
5        static void Main(string[] args)
6        {
7            Computer computer = new Computer();
8            SuperStorageAdapter superStorageAdapter = new SuperStorageAdapter();
9            SuperStorage superStorage = new SuperStorage();
10            superStorageAdapter.SuperStorage = superStorage;
11
12            Console.WriteLine("Now,I am testing the new super storage with adapter:");
13            computer.UsbDrive = superStorageAdapter;
14            computer.ReadData();
15            computer.WriteData();
16            Console.ReadLine();
17        }
18    }
19}
运行后会得到如下结果:

 

图2.4 利用Adapter模式运行新设备测试结果

OK!虽然遇到了一些困难,不过在设计模式的帮助下,我们还是在没有修改Computer任何代码的情况下实现了新设备的运行。

好了,理论在第一篇讲得足够多了,所以这里我就不多讲了。希望各位朋友结合第一篇的理论和这个例子,仔细思考面向接口的问题。当然,不要忘了结合现实。


 
   
自动化(Automation)基础概念:二次开发接口(API)与插件(Addin) 2009-07-06 00:53 在前文,我们已经解释了:
自动化(Automation)基础概念:COM组件(Component)与接口(Interface)
自动化(Automation)基础概念:变体(Variant)与Dispatch调用(IDispatch)
而同时,我们经常也可能经常听到以下这些词语:
自动化(Automation,COM Automation) OA(办公自动化,Office Automation)

 二次开发接口(应用程序开发接口,Application Programming Interface,API) 插件(Addin,Addon) 等等。
本文试图解释这些概念。
自动化(Automation)顾名思义是指“让机器在没有人工干预的情况下自动完成特定的任务”。

为了完成这一目标,自动化(Automation)技术的核心想法是,应用程序(Application)

需要把自己的核心功能以DOM模型的形式对外提供,使得别人能够通过这个DOM模型来使用该应用程序的功能。

这也就是我们通常说的应用程序编程接口——Application Programming Interface,简称API。

为了与Windows API这样的编程接口区分开来,我们引入一个专有名词,叫“二次开发接口”。

“二次开发”取意于“在现有应用程序基础上进行再开发”。其实如果你愿意把操作系统当作一个更大的应用程序的话,

二次开发接口和Windows API并没有什么很大的本质上的差异(尽管我们知道Windows API并不是以COM组件方式提供的)。
 
理解了自动化(Automation),OA(办公自动化,Office Automation)就比较好解释,无非是应用程序特指办公软件而已。
而OA是指办公(包括公文流转)系统的自动化。
在应用程序提供了编程接口(API)的前提下,典型情况下,我们有两种办法来使用这些API。

方法一是把应用程序当作一个Server,通过API对外提供服务。在此情形下,应用程序只是作为一个EXE COM Server的服务程序而已。

只要我们理解进程间的LPC或者RPC调用是怎么回事,那么一切就非常Easy。方法二是实现一个应用程序插件(Addin)。

这种方法更有意思一些。首先,这是一种进程内的调用,效率非常好。其次,这是一种双向的通讯,

应用程序通过它提供的插件机制感知到插件的存在,并且将插件加载上来;插件则是在获得活动权后,

通过应用程序的API完成特定的功能。最后,也是最重要的,插件与应用程序融为一体,实际上是扩展了应用程序的能力,

使得应用程序变得更为强大。
插件(Addins)的启动过程大体如下:
 
应用程序启动。通过注册表(或者存放于其他任何地方)获得插件列表。插件一般以 COM 组件形式提供,

故此只要有一个插件的 CLSID 或者 ProgID 的列表就可以了。另外,插件的功能可以千差万别,

但是他们需要统一实现一个接口,例如 _IDTExtensibility2 或者类似的东西。这个接口在下面的第二步就用到了。

遍历插件列表,创建并初始化各插件。关键是初始化。当然应用程序并不知道插件想做什么,

它只是取得 _IDTExtensibility2(或者类似接口),调用其中的初始化函数(如 OnConnection)。

插件获得了初始化机会。注意,在初始化的时候,应用程序把自己的DOM模型的根接口(我们通常称为Application)传入。

在 _IDTExtensibility2 中,根接口被定义为 IDispatch 类型,即 IDispatch* Application。

但是实际上可以更通用,如IUnknown* Application。有了这个 Application 指针,插件就可以为所欲为,

做它想做的事情,调用它想要调用的任何API。 从插件(Addins)展开来讲,可以讲非常多的内容。然而这不是本文的意图。

所以关于这方面的内容,我们只能留待以后有机会继续这个话题。不过我还是忍不住把话题起个开头:

由于插件(Addin)机制使得应用程序结构显得更为灵活,所以,越来越多的软件架构,

追求一种超轻量的内核(也就是我们说的应用程序,之所以称为内核,是因为它是组织一切的核心),

并把更多的功能通过插件(Addin)方式提供。超轻量的内核意味着需要解决一个额外的关键点:

就是插件(Addin)不只是扩展应用程序的功能,也同时扩展了应用程序的API,这些API与原有内核的API无缝地结合在一起,

从而使得整个系统可以滚雪球一样越滚越大。 


 

posted on 2011-11-15 16:58  兰保明  阅读(676)  评论(0编辑  收藏  举报