一、類和接口繼承
在Microsoft.Net Framwork中,有一個名為System.Object的類,它定義了4個公共實例方法:ToString, Equals, GetHashCode和GetType。該類是其他所有類的根或者說最終基類。換言之,所有類都繼承了Object的4個實例方法。這還意味著能操作Object類的實例的代碼實際能操作任何類的實例。
在CLR中,任何類都肯定從一個類也只能從一個類(而且只能從Objetc派生的類)派生的。這個類稱為基類。基類提供了一組方法簽名和這些方法的實現。你定義的新類可在將來由其它開發人員用作基類——所有方法簽名和方法實現都會由新的派生類繼承。
CLR允許開發人員定義接口,它實際只是對一組方法簽名進行了統一的命名。這些方法沒有提供任何實現。類通過指定接口與名稱來實現這個接口,它必須顯式提供接口方法的實現,否則CLR會認為此類型定義無效。 類繼承有一個重要特點,凡是能使用基類型實例的地方,都能使用派生類型的實例。類似的,凡是能使用具有接口類型實例的地方,都能使用實現了這個接口的一個類型的實例。二、定義接口
接口對一組方法簽名進行了統一命名。接口還能定義事件,無參屬性和有參屬性(C#中的索引器),因為這些東西本質都是方法。接口不能定義任何構造器方法,和任何實例字段。
雖然CLR允許接口定義靜態方法、靜態字段、常量和靜態構造器。但C#禁止接口定義任何一種這樣的靜態成員。 在C#中,使用interface關鍵字定義一個接口。要為接口指定一個名稱和一組實例方法簽名。比如FCL中的幾個接口定義:public interface IDisposable { void Dispose();} public interface IEnumerable { IEnumerator GetEnumerator();}對CLR而言,定義接口就像定義類型,也就是說,CLR會為接口類型對象定義一個內部數據結構,同時可用反射機制來查詢接口類型的功能。定義接口類型時,可指定你希望的任何訪問性修飾符。
根據約定,接口類型名稱要以大寫字母I開頭,目的是方便在源代碼中辨認接口類型。CLR支持泛型接口和接口中的泛型方法。三、繼承接口
本節將講介紹如何定義一個實現了接口的類型,然后再介紹如何創建這個類型的一個實例,并用這個對象調用接口的方法。最后介紹C#接口實現時,幕后發生的事情。
下面是在MSCorLib.dll中定義的System.IComparable<T>接口:public interface IComparable<in T> { Int32 CompareTo(T other);}以下代碼展示類如何定義一個實現類該接口的類型,還展示了對象兩個Point對象進行比較的代碼:
// Point 從 System.Object 派生并且實現了 IComparable<T>. public sealed class Point : IComparable<Point> { PRivate Int32 m_x, m_y; public Point(Int32 x, Int32 y) { m_x = x; m_y = y; } // 該方法實現了 IComparable<T>.CompareTo() public Int32 CompareTo(Point other) { return Math.Sign(Math.Sqrt(m_x * m_x + m_y * m_y) - Math.Sqrt(other.m_x * other.m_x + other.m_y * other.m_y)); } public override String ToString() { return String.Format("({0}, {1})", m_x, m_y); } } public static void Go() { Point[] points = new Point[] { new Point(3, 3), new Point(1, 2), }; //調用了由Point實現的IComparable<T>.CompareTo()方法 if (points[0].CompareTo(points[1]) > 0) { Point tempPoint = points[0]; points[0] = points[1]; points[1] = tempPoint; } Console.WriteLine("Points from closest to (0, 0) to farthest:"); foreach (Point p in points) Console.WriteLine(p); } C#編譯器要求將用于實現一個接口的方法標記為public。CLR要求將接口方法標記為virtual。如果在源代碼中沒有顯式地將方法標記為virtual,編譯器會將它們標記為virtual和sealed;這會阻止派生類重寫接口方法。如果顯式地將方法標記為virtual,編譯器就會將該方法標記為virtual(并保持它的非密封狀態)。這樣一來,派生類就能重寫它了。 如果一個接口方法是sealed的,派生類就不能重寫它。不過,派生類可以重寫繼承同一個接口,并可為該接口的方法提供它中的實現。在一個對象上調用一個接口的方法時,將調用該方法在該對象的類型中的實現。internal static class InterfaceReimplementation { public static void Go() { /************************* 第一個例子 *************************/ Base b = new Base(); // 結果顯示: "Base's Dispose" b.Dispose(); // 結果顯示: "Base's Dispose" ((IDisposable)b).Dispose(); /************************* 第二個例子 ************************/ Derived d = new Derived(); // 結果顯示: "Derived's Dispose" d.Dispose(); // 結果顯示: "Derived's Dispose" ((IDisposable)d).Dispose(); /************************* 第三個例子 *************************/ b = new Derived(); // 結果顯示: "Base's Dispose" b.Dispose(); // 結果顯示: "Derived's Dispose" ((IDisposable)b).Dispose(); } // 這個類型派生自 Object 并且實現了 IDisposable internal class Base : IDisposable { // 這個方法是隱式密封的,不能被重寫 public void Dispose() { Console.WriteLine("Base's Dispose"); } } // 這個類繼承了Base并且實現了IDisposable接口 internal class Derived : Base, IDisposable { // 這個方法不能重寫 Base's Dispose. // 'new' 關鍵字表明重新實現了IDisposable的Dispose new public void Dispose() { Console.WriteLine("Derived's Dispose"); // 注意: 下一行展示了如何讓調用基類的方法 // base.Dispose(); } } }四、關于調用接口方法的更多探討 FCL的System.String類型繼承了System.Object的方法簽名及其實現。此外,String類型還實現了幾個接口:IComparable,ICloneable,IEnumerable,IComparable<String>,IEnumerable<Char>等。這意味著String類型不需要實現(或重寫)其Object基類型提供的方法,但必須實現所有接口中聲明的方法。 CLR允許定義接口類型的字段,參數或局部變量。使用接口類型的一個變量,可以調用由那個接口定義的方法。例如:// s變量引用了String對象,使用s時,可以調用String, // Object,IComparable等定義的方法 private String s = "abc"; // comparable變量引用指向同一個String對象,使用comparable // 只能調用IComparable接口中的定義任何方法(包括Objetc中的方法) private IComparable comparable = s; // convertible變量引用指向同一個String對象,使用convertible // 只能調用IConvertible接口中的定義任何方法(包括Objetc中的方法) private IConvertible convertible = s; // enumerable 變量引用同一個String對象 // 在運行時,可將變量從一種接口類型轉型到另一種類型, // 只要該對象的類型實現了這兩個個接口 // 使用enumerable,只能調用IEnumerable聲明的任何方法(包括Objetc中的方法) private IEnumerable enumerable = (IEnumerable) convertible;和引用類型相似,值類型也可以實現零個或多個接口。不過,將值類型的實例轉型為接口類型時,值類型的實例必須裝箱。這是由于接口變量是一個引用,它必須指向堆上的一個對象,使CLR能檢查對象的類型對象指針,從而判斷對象的真實類型。然后,再調用已裝箱值類型的一個接口方法時,CLR會跟隨對象的類型對象指針,找到類型對象的方法表,從而調用正確的方法。五、 隱式和顯式接口方法實現 一個類型加載到CLR中時,會為該類型創建并初始化一個方法表。在這個方法表中,類型引入的每個新方法都有一條對象的記錄項。另外,還要為該類型繼承的所有虛方法添加記錄項。繼承的虛方法既有由繼承層次結構中的各個基類型定義的,也有由接口類型定義的。所以,對于下面這樣的一個簡單類型定義:
interter sealed class SimpleType : IDisposable { public void Dispose() { console.WriteLine("Dispose"); }}類型的方法表將包含以下方法對應的記錄項:
1)Object(隱式繼承的基類)定義的所有虛實例方法。 2)IDisposable(實現的接口)定義的所有接口方法。本例只有一個方法,即Dispose,因為IDisposable只定義了該方法。 3)SimpleType 引入的新方法Dispose。 為簡化編程,C#編譯器假定SimpleType引入的Dispose方法是對IDisposable的Dispose方法的實現。C#編譯器之所以做出這樣的假定,是因為Dispose方法的可訪問性是public,而且接口方法的簽名和新引入的方法完全一致,也就是說,這兩個方法具有相同的參數和返回類型。還有,如果新的Dispose方法被標記為virtual,C#編譯器仍會認為該方法匹配于接口方法。 C#編譯器將一個新方法和一個接口方法匹配起來之后,便會生成元數據,指明SimpleType類型的方法表中的兩個記錄項引用同一個實現。下面的代碼演示了如果調用類的公共Dispose方法以及如何調用IDisposable的Dispose方法在類中的實現:internal static class ExplicitInterfaceMethodImpl{ public static void Go() { SimpleType st = new SimpleType(); // 調用公共的 Dispose 方法實現 st.Dispose(); // 調用 IDisposable 的 Dispose 方法實現 IDisposable d = st; d.Dispose(); } public sealed class SimpleType : IDisposable { public void Dispose() { Console.WriteLine("Dispose"); } }}執行后,兩者是沒有任何卻別的。輸出結果都是Dispose
現在我們更改一下SimpleType,以便看出區別:public sealed class SimpleType : IDisposable{ public void Dispose() { Console.WriteLine("public Dispose"); } void IDisposable.Dispose() { Console.WriteLine("IDisposable Dispose"); }} 現在再次程序運行,會得到如下結果;public DisposeIDisposable Dispose
在C#中,將定義的那個接口的名稱作為方法名的前綴(例如IDisposable.Dispose),創建的就是顯式接口方法實現(EIMI)。注意,在C#中定義一個顯式接口方法時,不允許指定可訪問性。但是,編譯器生成的元數據時,其訪問性會被自動設為private,防止其他代碼在使用類的實例時直接調用接口方法。要調用接口方法,只能通過接口類型的一個變量來進行。
一個EIMI方法不能標記為virtual,所以它不能被重寫。這是因為EIMI方法并非真的是類型的對象模型的一部分,它是將一個接口(一組行為或方法)連接到一個類型上,同時避免公開方法的一種方式。六、 泛型接口 本節將介紹使用泛型接口的好處。 1.泛型接口提供了出色的編譯時類型安全性。有的接口(比如非泛型的IComparable接口),定義的方法使用了Object參數或Object返回類型,但這通常不是我們想要的。private static void SomeMethod1() { Int32 x = 1, y = 2; IComparable c = x; // CompareTo 期望接口一個 Object 類型; 傳遞 y (一個 Int32 類型) 允許 c.CompareTo(y); // Boxing occurs here // CompareTo期望接口一個 Object 類型; 傳遞 "2" (一個 String 類型) 允許 // 但運行是拋出 ArgumentException 異常 c.CompareTo("2"); }在理想情況下,接口方法應該使用強類型。這也正是FCL為什么還有包含一個泛型IComparable<in T>接口的原因。
private static void SomeMethod2() { Int32 x = 1, y = 2; IComparable<Int32> c = x; // CompareTo 期望接口一個 Int32 類型; 傳遞 y (一個 Int32 類型) 允許 c.CompareTo(y); // Boxing occurs here // CompareTo 期望接口一個 Int32 類型; 傳遞 "2" (一個 String 類型) 編譯不通過 // 指出 String 不能被隱式轉型為 Int32 // c.CompareTo("2"); } 2.處理值類型時,裝箱次數會少得多。 3.類可以實現新聞熱點
疑難解答