假設你設計一個和人交流的程序。
先建立一個接口
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無縫地結合在一起,
從而使得整個系統可以滾雪球一樣越滾越大。?
?