全面剖析C#接口編程之實現接口

接口(interface)用來定義一種程序的協定。實現接口的類或者結構要與接口的定義嚴格一致。在前面的文章中,我們已經對C#接口的概念,如何定義接口以及如何對接口進行訪問等問題進行了詳細的討論。在這些知識的基礎上,本文我們將來了解實現接口的方法。

顯式實現接口成員


爲了實現接口,類可以定義顯式接口成員執行體(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 就是顯式接口成員執行體。

說明:

· 不能在方法調用、屬性訪問以及索引指示器訪問中通過全權名訪問顯式接口成員執行體。事實上,顯式接口成員執行體只能通過接口的實例,僅僅引用接口的成員名稱來訪問。

· 顯式接口成員執行體不能使用任何訪問限制符,也不能加上abstract, virtual, override或static 修飾符。

· 顯式接口成員執行體和其他成員有着不同的訪問方式。因爲不能在方法調用、屬性訪問以及索引指示器訪問中通過全權名訪問,顯式接口成員執行體在某種意義上是私有的。但它們又可以通過接口的實例訪問,也具有一定的公有性質。

· 只有類在定義時,把接口名寫在了基類列表中,而且類中定義的全權名、類型和返回類型都與顯式接口成員執行體完全一致時,顯式接口成員執行體纔是有效的,例如:

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


使用顯式接口成員執行體通常有兩個目的:

· 因爲顯式接口成員執行體不能通過類的實例進行訪問,這就可以從公有接口中把接口的實現部分單獨分離開。如果一個類只在內部使用該接口,而類的使用者不會直接使用到該接口,這種顯式接口成員執行體就可以起到作用。

· 顯式接口成員執行體避免了接口成員之間因爲同名而發生混淆。如果一個類希望對名稱和返回類型相同的接口成員採用不同的實現方式,這就必須要使用到顯式接口成員執行體。如果沒有顯式接口成員執行體,那麼對於名稱和返回類型不同的接口成員,類也無法進行實現。

下面的定義是無效的,因爲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( )) ;


繼承接口實現


接口具有不變性,但這並不意味着接口不再發展。類似於類的繼承性,接口也可以繼承和發展。

注意:接口繼承和類繼承不同,首先,類繼承不僅是說明繼承,而且也是實現繼承;而接口繼承只是說明繼承。也就是說,派生類可以繼承基類的方法實現,而派生的接口只繼承了父接口的成員方法說明,而沒有繼承父接口的實現,其次,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的實現程序進行特殊化。

重新實現接口



我們已經介紹過,派生類可以對基類中已經定義的成員方法進行重載。類似的概念引入到類對接口的實現中來,叫做接口的重實現(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。

映射接口



類必須爲在基類表中列出的所有接口的成員提供具體的實現。在類中定位接口成員的實現稱之爲接口映射(interface mapping )。

映射,數學上表示一一對應的函數關係。接口映射的含義也是一樣,接口通過類來實現,那麼對於在接口中定義的每一個成員,都應該對應着類的一個成員來爲它提供具體的實現。

類的成員及其所映射的接口成員之間必須滿足下列條件:

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

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


注意:接口的成員包括它自己定義的成員,而且包括該接口所有父接口定義的成員。在接口映射時,不僅要對接口定義體中顯式定義的所有成員進行映射,而且要對隱式地從父接口那裏繼承來的所有接口成員進行映射。

在進行接口映射時,還要注意下面兩點:

· 在決定由類中的哪個成員來實現接口成員時,類中顯式說明的接口成員比其它成員優先實現。

· 使用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#接口編程之接口轉換》中全面介紹。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章