<三> 接口

 (1)接口概述

 接口(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 EdiBox:Control,IControl,IDataBound

{

   public void Paint();

   public void Bind(Binder b)

   {

     

   }

}

   在上面的代码中,Paint方法从IControl接口而来;Bind方法从IDataBound接口而来,都以public的身份在EditBox类中实现。

 C#中的接口是独立于类来定义的。这与C++模型是对立的,在C++中接口实际上就是抽象基类。

 接口和类都可以继承多个接口。

 类可以继承一个基类,接口根本不能继承类。这种模型避免了C++的多继承问题,C++中不同基类中的实现可能出现冲突。因此也不再需要诸如虚拟继承和显式作用域这类复杂机制。C#的简化接口模型有助于加快应用程序开发。

 一个接口定义一个只有抽象成员的引用类型。C#中一个接口实际所做的仅仅只存在着方法标志,但根本就没有执行代码。这就暗示了不能实例化一个接口,只能实例化一个派生自该接口的对象

 接口可以定义方法、属性和索引。所以对比一个类,接口的特殊性是:当定义一个类时,可以派生自多重接口,而您可以从仅有一个类派生。

 

(2)接口与组件

   接口描述了组件对外提供的服务。在组件和组件之间、组件和客户之间都通过接口进行交互。因此组件一旦发布,它只能通过预先定义的接口来提供合理的、一致的服务。这种接口定义之间的稳定性使用客户应用开发者能够构造出坚固的应用。一个组件可以实现多个组件接口,而一个特定的组件接口也可以被多个组件来实现。

   组件接口必须是能够自我描述的。这意味着组件接口应该不依赖于具体的实现,将实现和接口分离彻底消除了接口的使用者和接口的实现者之间的耦合关系,增加了信息的封装程度。同时这也要求组件接口必须使用一种与组件实现无关的语言。目前组件接口的描述标准是IDL语言。

   由于接口是组件之间的协议,因此组件的接口一旦被发布,组件生产者应该尽可能地保持接口不变,任何对接口语法或语义上的改变,都有可能造成组件与客户之间的联系遭到破坏。

   每个组件都是自主的,有其独特的功能,只通过接口与外界通信。当一个组件需要提供新的服务时,可以通过增加新的接口来实现。不会影响原接口已存在的客户,而新的客户,可以重新选择新的接口来获得服务。

 

(3)组件化程序设计

   组件化程序设计方法继承并发展了面向对象程序设计方法。它把对象技术应用于系统设计,对面向对象程序设计的实现过程做了进一步的抽象。我们可以把组件化程序设计方法用作构造系统的体系结构层次的方法,并且可以使用面向对象的方法很方便地实现组件。

   组件化程序设计强调真正的软件可重用性和高度的互操作性。它侧重于组件的产生和装配,这两方面一起构成了组件化程序设计的核心。组件的产生过程不仅仅是应用系统的需求,组件市场本身也推动了组件的发展,促进了软件厂商的交流与合作。组件的装配使得软件产品可以采用类似于搭积木方法快速地建立起来,不仅可以缩短软件产品的开发周期,同时也提高了系统的稳定性和可靠性。

   组件程序设计的方法有以下几个方面的特点:

编程语言和开发环境的独立性。

组件位置的透明性。

组件的进程透明性。

可扩充性。

可重用性。

具有强有力的基础设施。

系统一级的公共服务。

   C#语言由于其许多优点,十分适用于组件编程。但这并不是说C#是一门组件编程语言,也不是说C#提供了组件编程的工具。我们已经多次指出,组件应该具有与编程语言无关的特性。请读者记住这一点:组件模型是一种规范,不管采用何种程序语言设计组件,都必须遵守这一规范。比如组装计算机的例子,只要各个厂商为我们提供了配件规格、接口符合统一的标准,这些配件组合起来就能协同工作,组件编程也是一样。我们只说,利用C#语言进行组件编程将会给我们带来更大的方便。

 

1、定义接口

   从技术上讲,接口是一组包含了函数型方法的数据结构。通过这组数据结构,客户代码可以调用组件对象的功能。

   定义接口的一般形式为:

   [修饰符] interface 标识符 [:基列表]

   { 接口体 }

   [;]

   ◆ 修饰符(可选):允许使用的修饰符有new和其他4个访问修饰符public、protected、internal、private。在一个接口定义中同一修饰符不允许出现多次,new修饰符只能出现在嵌套接口定义中,表示覆盖了继承而来的同名成员。Public、protected、internal和private修饰符定义了对接口的访问权限(作用域),而接口成员的作用域由接口的作用域决定,因此对接口成员的声明不能使用任何访问修饰符,否则会出现语法错误。

   ◆ 标识符:接口名称。

   ◆ 基列表(可选):包含一个或多个显式基接口的列表,接口间由逗号分隔。(即 :基接口1,基接口2,…)

   ◆ 接口体:对接口成员的定义。

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

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

 

2、定义接口成员

   接口可以包含一个和多个成员,这些成员可以是方法、属性、索引器和事件,但不能是常量、域、操作符、构造函数或析构函数,而且不能包含任何静态成员。接口定义创建新的定义空间,并且接口定义直接包含的接口成员定义将新成员引入该定义空间。

   ◆ 接口的成员是由基接口继承的成员和接口本身定义的成员组成。

   ◆ 接口定义可以定义零个或多个成员。接口的成员必须是方法、属性、事件或索引器。接口不能包含常数、字段、运算符、实例构造函数、析构函数或类型,也不能包含任何种类的静态成员。

   ◆ 接口成员缺省访问方式是public。接口成员定义不能包含任何修饰符,比如成员定义前不能加abstract,public,protected,internal,private,virtual,override或static修饰符。

   ◆ 接口的成员之间不能相互同名。继承而来的成员不用再定义,但接口可以定义与继承而来的成员同名的成员,这时我们说接口成员覆盖了继承而来的成员,这不会导致错误,但会给出一个警告。关闭警告提示的方式是在成员定义前加上一个new关键字,隐藏基接口同名的成员。除此之外,若使用new关键字将会导致错误。

   ◆ 方法的名称必须与同一接口中定义的所有属性和事件的名称不同。此外,方法的签名必须与同一接口中定义的所有其他方法的签名不同。

   ◆ 属性或事件的名称必须与同一接口中定义的所有其他成员的名称不同。

   ◆ 一个索引器的签名必须区别于在同一接口中定义的其他所有索引器的签名。

   ◆ 接口声明的方法,方法中的属性、返回类型、标识符、和形式参数列表与一个类的方法所声明中的那些有相同的意义。一个接口方法声明不允许指定一个方法主体,而声明通常用一个分号结束。

   ◆ 接口属性声明的访问符与类属性声明的访问符相对应,除了访问符主体通常必须用分号。因此无论属性是读写、只读或只写,访问符都完全确定。

   ◆ 接口索引声明的属性、类型和形式参数列表与类的索引声明的那些有相同的意义。

下面通过一个例子说明如何定义一个接口和接口成员:

using System;

using System.Collections;

namespace 笔记

{

    //定义接口IPartA

    public interface IPartA

    {

        void SetDataA(string dataA);

    }

 

    //定义接口IPartB,继承IPartA

    public interface IPartB : IPartA

    {

       void SetDataB(string dataB);

    }

 

    //定义类SharedClass,继承接口IpartB

    public class SharedClass : IPartB

    {

        private string DataA;

        private string DataB;

 

        //实现接口IPartA的方法SetDataA

        public void SetDataA(string dataA)

       {

            DataA = dataA;

            Console.WriteLine("{0}", DataA);

        }

 

        //实现接口IPartB的方法SetDataB

        public void SetDataB(string dataB)

        {

            DataB = dataB;

            Console.WriteLine("{0}", DataB);

        }

    }

    class Test

    {

        public static void MMain()

        {

            SharedClass a = new SharedClass();

            a.SetDataA("interface IPartA");

            a.SetDataB("interface IPartB");

        }

    }

}

程序运行结果:

interface IPartA

interface IPartB

   程序中一共定义两个接口和一个类。接口IPartA定义方法SetDataA,接口IPartB定义方法SetDataB。接口之间也有继承关系,接口IPartB继承接口IPartA,也就继承了接口IPartA的SetDataA方法。接口只能定义成员,实现要由类或者结构来完成。SharedClass类派生于接口IPartB,因此要实现IPartB的SetDataB方法,也要实现IPartA的SetDataA方法。

   接口允许多重继承:

   Interface ID:IA,IB,IC

   {

    

   }

   类可以同时有一个基类和零个以上的接口,并要将基类写在前面:

   class MyClassB:MyCalssA,IA,IB
   {

    

    }

 

3、访问接口

对接口方法的调用和采用索引器访问的规则与类中的情况也是相同的。如果底层成员的命名与继承而来的高层成员一致,那么底层成员将覆盖同名的高层成员。但由于接口支持多重继承,在多继承中,如果两个父接口含有同名的成员,这就产生了二义性(这也是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.Cont=1会产生二义性,从而导致编译时错误,因此必须显式地给rs指派父接口类型,这种指派在运行时不会带来额外的开销。

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

using System;

using System.Collections;

namespace 笔记

{

    interface IBase

    {

        void FWay(int i);

    }

    interface Ileft : IBase

    {

        new void FWay(int i);

    }

    interface Iright : IBase

    {

        void G();

    }

    interface Iderived : Ileft, Iright { }

    class CText

    {

        void Text(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覆盖了(使用了new关键字)。虽然从IBase—>IRing—>Iderived这条继承路径上来看,Ileft.FWay方法是没有被覆盖的。只要记住这一点:一旦成员被覆盖以后,所有对其的访问都被覆盖以后的成员“拦截”了。

 

 

4、实现接口

(1)显式实现接口成员

  接口的实现指出接口成员所在的接口,则称为显式接口成员。为了实现接口,类可以定义显式接口成员实现体。显式接口成员实现体可以是一个方法、一个属性、一个事件或者一个索引器的定义,定义与该成员对应的完全限定名应保持一致。

例:显式接口调用

using System;

using System.Collections;

namespace 笔记

{

    //定义接口IPartA

    public interface IPartA

    {

        void SetDataA(string dataA);

    }

 

    //定义接口IPartB,继承IPartA

    public interface IPartB : IPartA

    {

        void SetDataB(string dataB);

    }

 

    //定义类SharedClass,继承接口IpartB

    public class SharedClass : IPartB

    {

        private string DataA;

        private string DataB;

 

        //实现接口IPartA的方法SetDataA

        //没有指定为public,并且指出了接口成员(SetDataA)所在的接口(IPartA)

        void IPartA.SetDataA(string dataA) (显式实现:指定成员所在接口)

        {

            DataA = dataA;

            Console.WriteLine("{0}", DataA);

        }

 

        //实现接口IPartB的方法SetDataB

        //没有指定为public, 并且指出了接口成员(SetDataB)所在的接口(IPartB)

        void IPartB.SetDataB(string dataB) (显式实现:指定成员所在接口)

        {

            DataB = dataB;

            Console.WriteLine("{0}", DataB);

        }

    }

 

    //显式接口成员只能通过接口来调用

    class Test

    {

        public static void MMain()

        {

            SharedClass a = new SharedClass();

            IPartB partb = a;

            partb.SetDataA("interface IPartA");

            partb.SetDataB("interface IPartB");

        }

    }

}

程序运行结果:

interface IPartA

interface IPartB

  方法本身并不是由类SharedClass提供。a.SetDataA(“interface IPartA”)或a.SetDataB(“interface IPartB”)调用都是错误的。显式接口成员没声明为public,这是因为这些方法都有着双重身份。在一个类中使用显式接口成员时,该方法被认为是私有方法,因此不能用类的实例调用它。但是,当将类的引用转型为接口引用时,接口中定义的方法就可以被调用,这时它又成为一个公有方法。

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

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

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

 

(2)非显式实现接口成员

using System;

using System.Collections;

namespace 笔记

{

    //定义接口IPartA

    public interface IPartA

    {

        void SetDataA(string dataA);

    }

 

    //定义接口IPartB,继承IPartA

    public interface IPartB : IPartA

    {

        void SetDataB(string dataB);

    }

 

    //定义类SharedClass,继承接口IpartB

    public class SharedClass : IPartB

    {

        private string DataA;

        private string DataB;

 

        //实现接口IPartA的方法SetDataA

        void SetDataA(string dataA)

        {

            DataA = dataA;

            Console.WriteLine("{0}", DataA);

        }

 

        //实现接口IPartB的方法SetDataB

        void IPartB.SetDataB(string dataB)

        {

            DataB = dataB;

            Console.WriteLine("{0}", DataB);

        }

    }

 

    class Test

    {

        public static void MMain()

        {

            SharedClass a = new SharedClass();

            a.SetDataA("interface IPartA");

            a.SetDataB("interface IPartB");

        }

    }

}

程序运行结果:

interface IPartA

interface IPartB

 

(3)继承接口实现

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

using System;

using System.Collections;

namespace 笔记

{

    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方法。

 

(4)重新实现接口

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

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

using System;

using System.Collections;

namespace 笔记

{

    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之上。

 

 

(5)映射接口

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

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

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

如果A和B都是成员方法,那么A和B的名称、类型、形参表(包括参数个数和每一个参数的类型)都应该是一致的。

如果A和B都是属性,那么A和B的名称、类型应当一致,而且A和B的访问函数也是类似的。但如果A不是显式接口成员实现体,A允许增加自己的访问函数。

如果A和B都是事件,那么A和B的名称、类型应当一致。

如果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:

using System;

using System.Collections;

namespace 笔记

{

    interface Interface1

    {

        void F();

    }

    class Class1

    {

        public void F() { }      //已经实现了接口

        public void G() { }

    }

    class Class2 : Class1, Interface1   //使用了类Class1提供的成员方法F来实现接口Interface1的成员方法F

    {

        new public void G() { }

    }

}

   在进行接口映射时,要注意以下几点:

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

◆ 使用private、protected和static修饰符的成员不能参与实现接口映射。

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

 

(6)利用接口回调

 实现回调的原理简介如下:

首先创建一个回调对象,然后再创建一个控制器对象,将回调对象需要被调用的方法告诉控件器对象。控制器对象负责检查某个场景是否出现或某个条件是否满足。当此场景出现或此条件满足时,自动调用回调对象的方法。

本示例展示了如何使用接口实现回调,此示例运行截图如下:

程序运行时,从键盘上上按任意一个键显示当前时间,整个程序可以“没完没了”地运行下去,除非您按了ESC键。

ICallBack接口定义了一个run()方法。

   public interface ICallBack

    {

       void Run();

    }

CallBackClass类实现此接口,并在其run()方法中向控制台输出当前时间。

   public class ICallBackClass : ICallBack

    {

       void Run(){…};

    }

Controller类中有一个私有的ICallBack类型的字段,用于存入回调对象的引用,此对象引用在构造函数中传入。

    class Controller

    {

        public ICallBack CallBackObject = null;

        public Controller(ICallBack obj)

        {

            this.CallBackObject = obj;

        }

        public void Begin()

        {

            Console.WriteLine("敲击任意键显示当前时间,ESC键退出...");

            while (Console.ReadKey(true).Key != ConsoleKey.Escape)

            {

                CallBackObject.Run(); //Run为回调的方法,外部传入

            }

        }

    }

Controller类的Begin()方法启动整个处理过程。

    class Program

    {

        static void Main(string[] args)

        {

            //创建控制器对象,将提供给它的回调对象传入

            Controller obj = new Controller(new CallBackClass());

            obj.Begin();

       }

    }

可以看到,当示例程序运行时,何时调用CallBackClass对象的run()方法是由用户决定的,用户每敲击一个键,控件器对象就调用一次CallBackClass对象的run()方法,在这个示例中,实现回调的关键在于ICallBack接口的引入。

作者: 聚拓互联 发表于 2011-05-30 20:08 原文链接

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架