接口在如下狀況下特別有用:sql
定義:接口就是一組抽象成員的命名集合。編程
抽象方法是純粹的協議,在其中沒有提供默認的實現。由接口定義的某個特定成員依賴於它所模擬的確切行爲。是的,接口表示某個類或結構能夠選擇去實現的行爲。一個類(或者一個結構)能夠支持任意數量的接口,所以本質上可就支持多種行爲。數組
.NET基礎類庫提供了幾百個預約義的接口類型,由各類類和結構實現。安全
對比接口類型和抽象基類網絡
接口類型和抽象基類很類似。若是類被標記爲抽象的,它能夠定義許多抽象成員來爲全部派生類型提供多態接口。然而,雖然類定義了一組抽象成員,它徹底能夠再定義許多構造函數、字段數據、非抽象成員(具備實現)等。而接口,只能包含抽象成員。編程語言
由抽象父類建立的多態接口有一個主要的限制,那就是隻有派生類型才支持由抽象父類定義的成員。然而,在大型軟件系統中,開發除了System.Object以外沒有公共父類的多個類層次結構很廣泛。因爲抽象基類中的抽象成員只應用到派生類型,咱們就不能以多個層次結構配置類型來支持相同的多態接口。ide
C#不支持類的多重繼承。接口類型就是來解決這個問題的。在定義接口以後,它就能夠被任何層次結構、任何命名空間或任何程序集(由任何.NET編程語言寫的)中的任何類或結構來實現。這樣的話,接口就有較高的多態性。函數
若是研究.NET Framework 4.5 SDK文檔的話,就會發現很是多看似無關的類型(System.Array、System.Data.SqlClient.SqlConnection、System.OperatingSystem、System.String等)都實現了這個接口。儘管這些類型不具備相同的父類(除了System.Object以外),咱們能夠經過IConeable接口類型把它們當成多態處理。測試
例如,若是咱們有一個含IConeable接口參數的方法CloneMe(),咱們就能夠把任何實現這個接口的對象傳給這個方法:spa
namespace ICloneableExample { class Program { static void Main( string[] args ) { Console.WriteLine("***** A First Look at Interfaces *****\n"); // 全部這些類都支持ICloneable接口 string myStr = "Hello"; OperatingSystem unixOS = new OperatingSystem(PlatformID.Unix, new Version()); System.Data.SqlClient.SqlConnection sqlCnn = new System.Data.SqlClient.SqlConnection(); // 所以,它們就能夠傳入接口ICloneable的方法 CloneMe(myStr); CloneMe(unixOS); CloneMe(sqlCnn); Console.ReadLine(); } private static void CloneMe( ICloneable c ) { // 克隆咱們得到的並輸出名字 object theClone = c.Clone(); Console.WriteLine("Your clone is a: {0}", theClone.GetType().Name); } } }
傳統抽象基類的另一個限制就是每個派生類型必須處理這一組抽象成員而且提供實現。爲了演示這個問題,假設咱們在Shape基類中新定義了一個叫GetNumberOfPoints()的抽象方法,它容許派生類型返回渲染圖形所需的頂點數:
abstract class Shape { //每個派生類型都必須支持這個方法 public abstract byte GetNumberOfPoints(); }
顯然,只有Hexagon類型才擁有頂點。然而,這樣更新後,全部派生類型(Circle、Hexagon以及ThreeDCircle)如今都必須提供這個方法完整的實現,即便這麼作沒有什麼意義。一樣,接口類型提供瞭解決方案。若是咱們定義了一個接口來表示"有頂點"這個行爲,咱們就能夠把它插到Hexagon類型中,Circle和ThreeDCircle則不受影響。
這裏是一個使用C#定義的自定義接口:
// 這個接口定義了"具備頂點"的行爲 public interface IPointy { // 隱式公共的和抽象的 byte GetNumberOfPoints(); }
記住,咱們定義接口成員時,不須要爲這個成員定義實現做用域。接口是純粹的協議,所以也不會定義實現(留給支持的類或結構)。所以,以下版本的IPointy會致使各類編譯器錯誤:
// 內有大量錯誤 public interface IPointy { // 錯誤!接口不能有字段 public int numbOfPoints; // 錯誤!接口不能有構造函數 public`IPonity() { numbOfPoints = 0; } // 錯誤!接口不能提供實現 public int GetNumberOfPoints() { return numbOfPoints; } }
無論怎麼樣,原始的IPointy接口定義了一個方法。然而,.NET接口還能夠定義許多屬性協議。例如,咱們可使用只讀屬性而不是其餘的訪問方法來建立IPointy接口:
public interface IPointy { // 在接口中的讀寫屬性差很少是 // retType PropName { get; set; } // 而接口中的只寫屬性是 // retType PropName { set; } byte Points { get; } }
接口類型還能夠包含事件以及索引器定義。
接口類型就其自己而言沒什麼用,由於它們只是抽象成員的集合。例如,咱們不能像類和結構同樣分配接口類型:
// 分配接口類型是不合法的 static void Main(string[] args) { IPointy p = new IPointy(); // 編譯器錯誤 }
除非被類或結構實現,不然接口沒有什麼用。在這裏,IPointy是一個表示"有頂點"這一行爲的接口。緣由很簡單,圖形層次結構中的一些類有頂點(如Hexagon),而其餘一些則沒有(好比Circle)。
若是類(或結構)選擇經過支持接口來擴展功能,就須要在其類型定義中使用逗號分隔的列表。要知道直接基類必須是冒號操做符後的第一個項。若是類類型從System.Object直接繼承,咱們徹底能夠只在列表中提供類支持的接口,由於若是沒有特別指明,C#編譯器會從System.Object擴展咱們的類型。因爲結構老是從System.ValueType繼承,只須要在結構定義後直接列出每個接口就好了。
實現接口是一個「要麼全要要麼全不要」的命題,也就是說支持類型沒法選擇實現哪些成員。
如今總結一下,下圖顯示的Visual Studio類結構圖使用流行的「棒棒糖」符號描述了與IPointy兼容的類。注意,Circle和ThreeDCircle沒有實現IPointy,由於這個行爲對這些特殊類沒有意義。Shape層次結構(包含接口):
如今已經有了一組支持IPointy接口的類,接下來的問題就是如何使用這些新功能。與給定接口功能最直接的交互方式就是直接在對象級別調用方法(所提供的接口成員不是顯式實現的)。
static void Main(string[] args) { // 調用IPointy定義的Points屬性 Hexagon hex = new Hexagon(); Console.WriteLine("Ponits:{0}", hex.Points); Console.ReadLine(); }
因爲讀者清楚六邊形(Hexagon)類型已經實現了該接口,有了Points屬性,因此在本例中這樣作沒有任何問題。但在其餘狀況下,讀者可能沒法在編譯時判斷指定類型支持哪一個接口。例如,假定讀者有一個包含50個Shape兼容類型的數組,其中僅有部分數組支持IPointy接口。很明顯,若是試圖在沒有實現IPointy接口的類型中調用Points屬性,將收到編譯錯誤。接下來的問題就是:如何才能動態判斷一個類型支持哪些接口呢?
在運行時判斷一個類型是否支持一個指定接口的一種方式是使用顯示強制轉換。若是這個類型不支持被請求的接口,將收到一個無效轉換異常(InvalidCastException)。使用結構化異常處理妥善處置這種可能的異常,例如:
static void Main(string[] args) { // 捕獲可能發生的InvalidCastException異常 Circle c = new Circle("Lisa"); IPointy itfPt = null; try { itfPt = (IPointy)c; Console.WriteLine(itfPt.Points); } catch (InvalidCastException e) { Console.WriteLine(e.Message); } Console.ReadLine(); }
使用try/catch邏輯並不是是最好的解決方法,在首次調用該接口成員以前判斷其支持哪一個接口更加理想。下面介紹兩種實現方式。
獲取接口引用:as關鍵字
判斷一個指定類型是否支持一個接口的第二種方式就是使用as關鍵字。若是該對象可被視爲一個指定的接口,你能夠在使用該關鍵字的語句中獲得指向該對象接口的引用;不然,將返回一個null的空引用。所以,首先要檢查null值:
static void Main(string[] args) { // 能將六角形hex2視爲實現了IPointy接口嗎 Hexagon hex2 = new Hexagon("Peter"); IPointy itfPt2 = hex2 as IPointy; if (itfPt2 != null) { Console.WriteLine("Points:{0}", itfPt2.Points); } else { Console.WriteLine("OOPS! Not pointy..."); } Console.ReadLine(); }
請注意,當使用as關鍵字的時候,無需使用try/catch邏輯。若是引用非空,說明調用的是一個正確的接口引用。
獲取接口引用:is關鍵字
還能夠經過使用is關鍵字來檢查是否實現一個接口。若是要考查的對象與指定接口不符,將返回false值。反之,若是該類型與指定接口相符,就能夠安全地調用這些成員,而沒必要使用try/catch邏輯。
假定更新了Shape類型的數組,使其中部分紅員實現了IPointy接口:
static void Main(string[] args) { // 生成Shape數組 Shape[] myShapes = { new Hexagon(), new Circle(), new Triangle("Joe"), new Circle("JoJo")}; for (int i = 0; i < myShapes.Length; i++) { // 回調Shape基類定義一個抽象的Draw()成員,由此全部Shape都知道如何繪製本身 myShapes[i].Draw(); // 哪些是有棱角的? if (myShapes[i] is IPointy) Console.WriteLine("-> Points: {0}", ((IPointy)myShapes[i]).Points); else Console.WriteLine("-> {0}\'s not pointy!", myShapes[i].PetName); Console.WriteLine(); } Console.ReadLine(); }
既然接口是有效的.NET類型,讀者能夠構造將接口做爲參數的方法,對於當前的示例,假定已經定義了另外一個名爲IDraw3D的接口:
// 模擬能以絕佳3D效果呈現一個類型的能力 public interface IDraw3D { void Draw3D(); }
接下來,假定3種圖形中的2種(ThreeDCircle與Hexagon)已經被設定爲支持這種新的行爲:
// Circle支持IDraw3D接口 class ThreeDCircle : Circle, IDraw3D { ... public void Draw3D() { Console.WriteLine("Drawing Circle in 3D!"); } } // Hexagon支持IPointy與IDraw3D接口 class Hexagon : Shape, IPointy, IDraw3D { ... public void Draw3D() { Console.WriteLine("Drawing Hexagon in 3D!"); } }
新的Visual Studio類圖:
若是讀者如今定義一個將IDraw3D接口做爲參數的方法,將能有效傳遞任何實現IDraw3D接口的對象(若是讀者視圖傳進一個不支持該接口的類型,將收到編譯錯誤):
// 繪製任何支持IDraw3D接口的類型 static void DrawIn3D(IDraw3D itf3d) { Console.WriteLine("-> Drawing IDraw3D compatible type"); itf3d.Draw3D(); }
能夠測試Shape數組中的項是否支持接口,若是支持,就將其傳入DrawIn3D()方法:
static void Main(string[] args) { // 生成Shape數組 Shape[] myShapes = { new Hexagon(), new Circle(), new Triangle("Joe"), new Circle("JoJo")}; for (int i = 0; i < myShapes.Length; i++) {
... // 支持繪製爲3D嗎? if (myShapes[i] is IDraw3D) DrawIn3D((IDraw3D)myShapes[i]); } Console.ReadLine(); }
例如,能夠寫一個接受Shape對象數組做爲參數、返回支持IPointy的第一項的引用的方法:
static void Main(string[] args) { // 構建Shape數組 Shape[] myShapes = { new Hexagon(), new Circle(), new Triangle("Joe"), new Circle("JoJo")}; // 獲取第一個pointy項 IPointy firstPointyItem = FindFirstPointyShape(myShapes); Console.WriteLine("The item has {0} points", firstPointyItem.Points); Console.ReadLine(); } static IPointy FindFirstPointyShape(Shape[] shapes) { foreach (Shape s in shapes) { if (s is IPointy) return s as IPointy; } return null; }
要理解的是,一樣的接口即便不在同一個類層次結構,也沒有除System.Object之外的公共父類,也能夠由多個類型實現,這能夠派生出去多很是強大的編程結構。例如,假設咱們要在當前項目中開發三個全新的類類型來對廚具(經過Knife和Fork類)和園藝設備(PitchFork,指乾草叉)建模:
接口能夠插入到類層次結構任何部分的類型中。
若是已經定義了PitchFork、Fork和Knife類型,那麼如今能夠定義一個支持IPointy接口的對象數組。既然這些成員都支持一樣的接口,所以能夠拋開類層次結構的所有差別性,經過數組進行迭代並將每一個對象視爲支持IPointy接口的對象:
static void Main(string[] args) { // 這個數組僅僅包含實現了IPointy接口的類型 IPointy[] myPointyObjects = {new Hexagon(), new Knife(), new Triangle(), new Fork(), new PitchFork()}; foreach (IPointy i in myPointyObjects) Console.WriteLine("Object has {0} points.", i.Points); Console.ReadLine(); }
下面強調一下這個示例的重要性,請記住:若是你有一個給定接口的數組,那麼這個數組能夠包含實現了該接口的任何類或者結構。
一個類或結構能夠實現許多接口。所以,咱們可能實現包含重複命名成員的接口,因此就須要處理命名衝突。如今設計3個自定義接口來表示實現類型呈現自身輸出的各類位置:
// 繪製到表單上 public interface IDrawToForm { void Draw(); } // 繪製到內存中 public interface IDrawToMemory { void Draw(); } // 呈現到打印機 public interface IDrawToPrinter { void Draw(); }
注意,每個接口都定義了Draw()方法,其名稱相同(碰巧都沒有參數)。若是咱們如今但願一個名爲Octagon的類類型支持這些接口中的每個,編譯器會容許以下的定義:
class Octagon : IDrawToForm, IDrawToMemory, IDrawToPrinter { public void Draw() { // 共享繪製邏輯 Console.WriteLine("Drawing to a printer..."); } }
儘管這段代碼能夠經過編譯,你可能也認爲咱們會有問題。簡單來講,若是提供一個Draw()方法實現,咱們就不能從Octagon對象根據某個接口採起一系列行爲。例如,下面的代碼會調用相同的Draw()方法,而無論咱們獲取到哪一個接口:
static void Main(string[] args) { Console.WriteLine("***** Fun with Interface Name Clashes *****\n"); // 全部這些調用都會調用相同的Draw()方法 Octagon oct = new Octagon(); IDrawToForm itfForm = (IDrawToForm)oct; itfForm.Draw(); IDrawToPrinter itfPrinter = (IDrawToPrinter)oct; itfPrinter.Draw(); IDrawToMemory itfMemory = (IDrawToMemory)oct; itfMemory.Draw(); Console.ReadLine(); }
顯然,把圖像呈現到窗體的代碼和把圖像呈現到網絡打印機或內存中某個區域的代碼不太同樣。若是要實現具備相同成員的接口,可使用顯示接口實現語法來解決這種命名衝突:
class Octagon : IDrawToForm, IDrawToMemory, IDrawToPrinter { // 對某個接口顯示綁定Draw() void IDrawToForm.Draw() { Console.WriteLine("Drawing to form..."); } void IDrawToMemory.Draw() { Console.WriteLine("Drawing to memory..."); } void IDrawToPrinter.Draw() { Console.WriteLine("Drawing to a printer..."); } }
咱們能夠看到,若是顯式實現接口成員的話,大體模式能夠歸結爲:returnType InterfaceName.MethodName(params){}
顯示實現的成員是自動私有的。
因爲顯式實現成員老是隱式私有的,這些成員在對象級別就不可用。咱們必須使用顯示轉換來訪問須要的功能。例如:
static void Main(string[] args) { Console.WriteLine("***** Fun with Interface Name Clashes *****\n"); Octagon oct = new Octagon(); // 如今必須使用轉換來訪問Draw()成員 IDrawToForm itfForm = (IDrawToForm)oct; itfForm.Draw(); // 若是之後不須要使用接口變量,能夠簡化成這個形式 ((IDrawToPrinter)oct).Draw(); // 也可使用"as"關鍵字 if (oct is IDrawToMemory) ((IDrawToMemory)oct).Draw(); Console.ReadLine(); }
雖然這個語法對解決命名衝突頗有用,可是若是但願從對象級別隱藏"高級"成員的話,咱們也可使用顯示接口實現。這樣,若是對象用戶使用點操做符的話,他就只能看到類型全部功能的子集。然而,那些須要更多高級行爲的人能夠經過顯示轉換提取須要的接口。
接口能夠組織成接口層次結構。和類層次結構類似,若是接口擴展了既有接口,它就繼承了父類定義的抽象成員。固然,和基於類的繼承不一樣的是,派生接口不會繼承真正的實現,而只是經過額外的抽象成員擴展了其自身的定義。
若是但願擴展既有接口功能又不變更既有代碼,接口層次結構就會頗有用。如今,從新設計以前的一組與呈現相關的接口,這樣IDrawable就是家族樹的根:
public interface IDrawable { void Draw(); }
因爲IDrawable定義了基本繪製行爲,咱們如今就能夠建立派生接口來擴展以修改後的格式呈現的能力,例如:
public interface IAdvancedDraw : IDrawable { void DrawInBoundingBox( int top, int left, int bottom, int right ); void DrawUpsideDown(); }
有了這樣的設計,若是一個類實現IAdvancedDraw,咱們如今就必須實如今繼承鏈上定義的每個成員(更準確地說是Draw()、DrawInBoundingBox()和DrawUpsideDown()方法):
public class BitmapImage : IAdvancedDraw { public void Draw() { Console.WriteLine("Drawing..."); } public void DrawInBoundingBox( int top, int left, int bottom, int right ) { Console.WriteLine("Drawing in a box..."); } public void DrawUpsideDown() { Console.WriteLine("Drawing upside down!"); } }
如今,使用BitmapImage時,能夠在對象級別上調用每個方法(由於它們都是公有的),也能夠經過顯示轉換提取每個支持接口的引用:
static void Main( string[] args ) { Console.WriteLine("***** Simple Interface Hierarchy *****"); // 從對象級別調用 BitmapImage myBitmap = new BitmapImage(); myBitmap.Draw(); myBitmap.DrawInBoundingBox(10, 10, 100, 150); myBitmap.DrawUpsideDown(); // 顯示獲取IAdvancedDraw IAdvancedDraw iAdvDraw = myBitmap as IAdvancedDraw; if (iAdvDraw != null) iAdvDraw.DrawUpsideDown(); Console.ReadLine(); }
和類類型不一樣,一個接口能夠擴展多個基接口。這就容許咱們設計很是強大、很是靈活的抽象。
// 接口能夠是多重繼承的 interface IDrawable { void Draw(); } interface IPrintable { void Print(); void Draw(); // <-- 注意,可能致使命名衝突 } // 多重接口繼承。沒有問題 interface IShape : IDrawable, IPrintable { int GetNumberOfSides(); }
接口層次結構:
如今,關鍵問題就是若是咱們有一個類支持IShape,須要實現多少方法呢?回答是:看狀況。若是但願提供Draw()的簡單實現,只須要提供3個成員,以下Rectangle類型所示:
class Rectangle : IShape { public int GetNumberOfSides() { return 4; } public void Draw() { Console.WriteLine("Drawing..."); } public void Print() { Console.WriteLine("Prining..."); } }
若是咱們更願意對每個Draw()方法提供特定實現(這裏應該比較有意義的),就可使用顯示接口實現解決命名衝突,以下面的Square類型所示:
class Square : IShape { // 使用顯式實現來處理成員命名衝突 void IPrintable.Draw() { // 繪製到打印機上 } void IDrawable.Draw() { // 繪製到屏幕上 } public void Print() { // 打印 } public int GetNumberOfSides() { return 4; } }