1、第十章 接口,基本要求 1.掌握接口的定义、实现和使用; 2.掌握通过接口实现多态性; 3.能正确区分区分接口方法和对象方法; 4.理解多继承的概念,掌握基于接口的多继承; 5.掌握集合列表、队列和堆栈的使用,能自定义集合。,第十章 接口,主要内容 10.1 接口的定义和使用 10.2 接口与多态 10.3 接口和多继承 10.4 接口与集合,10.1 接口的定义和使用,10.1.1 接口的定义通过前面的学习我们知道,抽象类不能被实例化,但它仍可以有自己的成员字段和非抽象方法。接口则是比抽象类更抽象的一种数据类型,它所描述的是功能契约,即能够提供什么样的服务,而不考虑与实现有关的任何因素。C#
2、中使用关键字interface来定义接口类型,其成员只能是一般方法、属性和索引函数,而不能有字段和构造函数。,10.1 接口的定义和使用,10.1.1 接口的定义例如,下面的代码就定义了一个接口IDrawable(描述取款),其中包含了一个接口方法Draw。public interface IDrawablebool Draw(decimal money);接口和抽象类都不能创建自己的实例,接口方法和抽象方法也一样没有执行代码;此外,接口方法不能是静态的,也不能使用任何访问限定符修饰。从某种意义上说,可以把接口看成是只包含抽象方法的抽象类,其中每个接口方法都表示一项“服务契约”。,10.1 接
3、口的定义和使用,10.1.1 接口的定义接口之间也可以继承。在下面的代码中,“可存取”接口IDepositDraw就是从IDrawable派生而来的,因此它也就继承了IDrawable接口的方法Draw。public interface IDepositDraw:IDrawablevoid Deposit(decimal money);说明:子接口可以包含与父接口相同的方法,但此时子接口的方法必须使用关键字new来隐藏父接口的方法,而不能使用override修饰符来重载父接口的方法。,10.1 接口的定义和使用,10.1.1 接口的定义接口之间的继承同样存在传递性。例如,接口IB从接口IA派生
4、,而接口IC又从接口IB派生,则接口IC就间接继承了接口IA,即接口IC包含接口IA定义的接口方法。不过接口不能从类派生。,10.1 接口的定义和使用,10.1.2 接口的实现如果一个类声明支持某个接口,它就必须履行该接口的契约,即支持该接口中定义的所有方法。这可以分为以下两种情况。 如果支持接口的类是非抽象类,那么必须支持接口中的所有方法,并为这些方法提供具体的实现; 如果支持接口的类是抽象类,那么必须支持接口中的所有方法,且这些方法要么提供实现,要么是抽象的。,10.1 接口的定义和使用,10.1.2 接口的实现类对接口的支持声明和类的继承声明类似,有时也称为从接口继承,这种继承关系同样存
5、在传递性:如果一个类声明支持某个接口,那么它的所有派生类也就自动支持该接口。例如,下面定义的BankAccount类就声明支持IDrawable接口,并提供了对接口方法的实现。public class BankAccount:IDrawableprivate decimal balance=1000;public BankAccount(decimal x)balance=x;,10.1 接口的定义和使用,10.1.2 接口的实现public bool Draw(decimal money) /*隐式实现接口方法*/if(balance=money)balance-=money;return
6、true;elsereturn false;,10.1 接口的定义和使用,10.1.2 接口的实现 1.隐式实现在上面的代码中,BankAccount是通过公有成员方法实现所支持的接口方法,这叫做对接口方法的隐式实现。此时既可以通过类的实例来进行方法调用,也可以隐式转换为接口实例再进行方法调用,例如:BankAccount account1=new BankAccount(1000);account1.Draw(500);IDrawable i1=account1; /转换成接口实例i1.Draw(300);,10.1 接口的定义和使用,10.1.2 接口的实现 进行隐式实现时,成员方法可以使
7、用abstract或virtual修饰(表示允许派生类重载),但不能使用sealed和override修饰。2.显示实现类对接口方法的实现还有另一种形式,即在方法名之前加上接口名,这叫做对接口方法的显示实现。以BankAccount类为例,其显示属性接口方法的代码如下:,10.1 接口的定义和使用,10.1.2 接口的实现 public class BankAccount:IDrawableprivate decimal balance=1000;public BankAccount(decimal x)balance=x;bool IDrawable.Draw(decimal money)
8、/*显示实现接口方法*/if(balance=money),10.1 接口的定义和使用,10.1.2 接口的实现 balance-=money;return true;elsereturn false;此时,方法不能使用任何修饰符,那么它实际上就是类的私有成员,因此不能通过类的对象来访问,而需要转换为接口实例才能访问,例如:,10.1 接口的定义和使用,10.1.2 接口的实现 BankAccount account1=new BankAccount(1000);account1.Draw(500); /错误的IDrawable i1=account1; /*转换成接口实例,正确*/i1.Dr
9、aw(300);显示实现明确指出了所实现的方法来自哪一个接口,因此方法前的接口名必须是定义方法的原始接口。例如,将BankAccount所支持的接口改为IDepositDraw,那么其显示实现的两个接口方法就应该是IDepositDraw.Deposit和IDrawable.Draw。,10.1 接口的定义和使用,10.1.2 接口的实现 public interface IDrawablebool Draw(decimal money);public interface IDepositDraw : IDrawablevoid Deposit(decimal money);,10.1 接口的
10、定义和使用,10.1.2 接口的实现 public class BankAccount : IDrawable, IDepositDrawprivate decimal balance;public BankAccount(decimal x) balance = x; bool IDrawable.Draw(decimal money) /*不能把IDrawable改成IDepositDraw*/,10.1 接口的定义和使用,10.1.2 接口的实现 if (balance = money) balance -= money;return true;else return false;voi
11、d IDepositDraw.Deposit(decimal money) balance += money; ,10.2 接口与多态,10.2.1 通过接口实现多态性 前面介绍了通过虚拟方法和重载方法实现多态性。类似地,我们可以对接口的实例直接调用其方法,程序会根据实现接口的实际对象来决定调用哪一个方法。例P10_1中,BankAccount、CreditCard和DebitCard都支持“可支付”接口IPayable,主程序Program的Recieve方法用于接收付款。只要对象声明支持IPayable接口,那么就可以通过Pay方法来要求其提供付款服务,至于付款对象的具体类型是什么、其内部
12、付款操作是怎么实现的,客户都不必关心。,10.2 接口与多态,10.2.2 区分接口方法和对象方法 派生类可以隐藏基类中的方法,也可以重载基类中的方法,但二者不能同时存在。然而利用接口来实现多态性,就可以达到“鱼和熊掌兼得”的效果,其关键就在于接口方法的隐式实现和显示实现可以并存。例P10_2中,BankAccount可以同时提供IDrawable.Draw方法和公有的Draw方法。,10.2 接口与多态,10.2.2 区分接口方法和对象方法 在这种情况下,显示实现的方法才是对接口方法的真正实现,而隐式实现的方法可以看作是对原接口方法的覆盖。此时通过接口实例调用的是显示实现的方法,而通过对象实
13、例调用的方法则是隐式实现的方法。例 P10_3。,10.3 接口和多继承,10.3.1 多继承概述 单一继承是指一个类最多只能从一个基类直接派生,这样的继承层次可以用一棵树来描述。如图10-1(a)所示,银行卡BankCard的派生类有一卡通GeneralCard和信用卡CreditCard,而VisaCard又由CreditCard派生。 单一继承在表达能力上有一定的限制。例如,并不是所有的银行卡都能在POS机上消费,那么消费功能的描述就需要在BankCard的派生类中重复多次。如果采用多继承技术,那么就可以将消费功能抽象在一个Payer类中,这样GenaralCard、CreditCard
14、就能同时从BankCard和Payer派生,如图10-1(b)所示。,10.3 接口和多继承,10.3.1 多继承概述 多继承的层次结构是一个有向图,这就会带来二义性问题。考虑Payer还可以有一个支持外币消费的派生类GloabalPayer,而VisaCard又同时从CreditCard和GlobalPayer派生,这时两个基类中的同名方法就会发生冲突,即不知道VisaCard中的base.pay是CreditCard中的pay方法还是GlobalPayer中的pay方法。此外,基类过多也会严重影响程序的性能,因为派生类的对象所占用的内存空间将是其所有基类对象的内存空间的总和,派生类对象在创
15、建时还会依次调用其所有基类的构造函数。不少专家认为,程序设计引言中多继承带来的问题远远大于其所带来的优越性。,10.3 接口和多继承,10.3.1 多继承概述 C#语言规定一个类只能有一个直接基类,但可以同时支持多个接口,这能有效地弥补单一继承在表达能力上的不足。而与类的多继承相比,C#基于接口的多继承是轻量级的:接口之间的继承只是“契约”继承,不需要提供实现;当一个类支持多个接口时,它也只是实现个接口的“契约”服务,而不需要处理多重构造函数和重载方法等复杂情况。例如,可以把消费和外币消费的功能描述分别放在接口Ipayable和其派生接口IglobalPayable中,并由CreditCard
16、和VisaCard个实现所需要的接口,如图10-1(c)所示。,10.3 接口和多继承,10.3.1 多继承概述 图10-1 银行卡的继承结构示意图,(a)类的单一继承,(b)类的多继承,(c)基于接口继承,10.3 接口和多继承,10.3.2 基于接口的多继承 和类之间的单一继承不同,一个接口可以有多个父接口,此时在继承声明中父接口之间通过逗号分隔(先后顺序不限)。例如,下面定义的接口IC就有两个父接口IA和IB,它也就继承了IA的F方法和IB的G方法。public interface IAvoid F();public interface IBvoid G();public interfa
17、ce IC:IA,IBvoid H();,10.3 接口和多继承,10.3.2 基于接口的多继承 一个类同样可以支持多个接口;如果同时存在类继承,那么继承声明中的基类就程序在所有接口之前。例如:public class CA:IA,IBvoid IA.F() void IB.G()public class CC:CA,ICvoid IC.H()例P10_4。,10.3 接口和多继承,10.3.3 解决二义性C#语言解决多继承二义性的方式很简单,即通过显示实现来明确方法来自哪一个接口。例如,在下面的代码中,接口IA和IB以及类CA都定义了方法F:public interface IAvoid F
18、();public interface IBvoid F();public class CApublic virtual void F()Console.WriteLine(“类CA的F方法“);,10.3 接口和多继承,10.3.3 解决二义性假定CA的派生类CB同时还支持接口IA和IB,那么CB对基类方法的继承以及对接口方法的实现可以分为以下几种情况。(1)如果CB没有定义方法F,那么它从CA继承的方法F就是对接口IA和IB的方法的隐式实现。(2)如果CB只重载或隐藏了CA的方法F,那么CB定义的成员方法F就是对接口方法的隐式实现。(3)否则,CB通过显示实现的方式来明确指定所实现的接口方
19、法,但这不会影响到它对基类方法的继承或重载。例P10_5。,10.4 接口与集合,10.4.1 集合型接口及其实现像列表、栈、队列等集合类型在计算机程序中有着广泛的应用。.NET类库的System.Collections命名空间下定义了一组与集合有关的接口,并通过在预定义集合类型中支持不同的接口来提供相关服务。表10-1简要地描述了.NET类库中与集合有关的接口类型,表10-2介绍了.NET类库中支持这些接口的主要集合类型。,10.4 接口与集合,10.4.1 集合型接口及其实现表10-1 .NET类库中与集合有关的接口类型,10.4 接口与集合,10.4.1 集合型接口及其实现表10-1 .
20、NET类库中与集合有关的接口类型(续),10.4 接口与集合,10.4.1 集合型接口及其实现表10-1 .NET类库中与集合有关的接口类型(续),10.4 接口与集合,10.4.1 集合型接口及其实现表10-2 .NET类库中的主要集合类型,10.4 接口与集合,10.4.1 集合型接口及其实现从接口方法的原型可以看出,这些集合操作都以object为元素类型,那么实际上就可以在集合中存储任意类型的对象。表10-2中列车的类型都支持IEnumerable和ICollection接口,因此可使用foreach语句来遍历集合元素,并通过Count属性来获取集合长度。此外,C#中的所有数组类型都默认
21、从Array类继承,而该类也支持IEnumerable和ICollection接口,只不过它对Count属性是显示接口实现,并通过Length属性来返回该值。,10.4 接口与集合,10.4.1 集合型接口及其实现例如,下面最后最后两行代码的输出是相同的。int x=new int5;ICollection icol=x;Console.WriteLine(x.Length);Console.WriteLine(icol.Count);,10.4 接口与集合,10.4.2 列表、队列和堆栈ArrayList类实现了IList接口的各个方法,并由此提供了动态数组的功能,例如,它不仅可通过Add方
22、法在集合末端加入元素,通过Remove方法删除指定元素,还可以通过Insert和RemoveAt方法在任何位置插入和删除元素,而集合的大小会自动进行调整。ArrayList自定义的成员方法BinarySearch和Sort还可用于集合元素的查找和排序。例 P10_6演示了ArrayList集合的简单用法。,10.4 接口与集合,10.4.2 列表、队列和堆栈注意,BinarySearch和Sort方法要求集合中的元素类型应该能够相互比较,否则它们起不到搜索和排序的效果。此外,Queue类的Enqueue和Dequeue方法分别用于集合元素的入队和出队,而Stack类的Push和Pop方法分别用
23、于集合元素的入栈和出栈操作。下面给出这两个类型的使用示例。Queue q1=new Queue();q1.Enqueue(10);q1.Enqueue(“China“);q1.Enqueue(DayOfWeek.Sunday);,10.4 接口与集合,10.4.2 列表、队列和堆栈while (q1.Count0)Console.WriteLine(q1.Dequeue();Stack s1=new Stack();s1.Push(10);s1.Push(“China“);s1.Push(DayOfWeek.Sunday);while (s1.Count0)Console.WriteLine(
24、s1.Pop();,10.4 接口与集合,10.4.2 列表、队列和堆栈除了默认的无参构造函数外,ArrayList、Queue和Stack都提供了以ICollection为参数类型的构造函数,它表示在创建集合对象的同时从指定的集合赋值元素,这样就能在这些集合类型以及数组之间方便地进行数据交换。例如:int x=5,6,8,9,10,-12;Queue q1=new Queue(x);foreach(int i in q1),10.4 接口与集合,10.4.2 列表、队列和堆栈Console.WriteLine(i);/*输出顺序和数组x中的元素顺序相同*/Stack s1=new Stack
25、(x);foreach(int i in s1)Console.WriteLine(i);/*输出顺序和数组x中的元素顺序相反*/ArrayList a1=new ArrayList(s1);foreach(int i in a1)Console.WriteLine(i);/*输出顺序和数组x中的元素顺序相反*/,10.4 接口与集合,10.4.3 自定义集合类型如果用户要在自定义类型中支持这些集合接口,就必须实现有关集合操作的接口方法,如通过Count属性返回集合长度,通过CopyTo方法将集合元素赋值到数组中等。例P10_7。提示,ICollection接口继承了IEnumerable接口,因此需要提供GetEnumerator方法来支持foreach遍历。此外,ICollection接口还要求提供IsSynchronized和SyncRoot属性,不过本例中只定义了它们的原型,而没有定义完整的实现。,