第五章 類 (2)
5.3 類屬性 有兩種途徑揭示類的命名屬性——通過域成員或者通過屬性。前者是作為具有公共訪問性的成員變量而被實現的;后 者并不直接回應存儲位置,只是通過 存取標志(accessors)被訪問。 當你想讀出或寫入屬性的值時,存取標志限定了被實現的語句。用于讀出屬性的值的存取標志記為關鍵字get,而要修 改屬性的值的讀寫符標志記為set。 在你對該理論一知半解以前,請看一下清單5.9中的例子,屬性SquareFeet被標上了get和set的存取標志。 清單 5.9 實現屬性存取標志
1: using System; 2: 3: public class House 4: { 5: private int m_nSqFeet; 6: 7: public int SquareFeet 8: { 9: get { return m_nSqFeet; } 10: set { m_nSqFeet = value; } 11: } 12: } 13: 14: class TestApp 15: { 16: public static void Main() 17: { 18: House myHouse = new House(); 19: myHouse.SquareFeet = 250; 20: Console.WriteLine(myHouse.SquareFeet); 21: } 22: }
House類有一個命名為SquareFeet的屬性,它可以被讀和寫。實際的值存儲在一個可以從類內部訪問的變量中——如果 你想當作一個域成員重寫它,你所要做的就是忽略存取標志而把變量重新定義為: public int SquareFeet; 對于一個如此簡單的變量,這樣不錯。但是,如果你想要隱藏類內部存儲結構的細節時,就應該采用存取標志。在這種情 況下,set 存取標志給值參數中的屬性傳遞新值。(可以改名,見第10行。) 除了能夠隱藏實現細節外,你還可自由地限定各種操作: get和set:允許對屬性進行讀寫訪問。 get only:只允許讀屬性的值。 set only:只允許寫屬性的值。 除此之外,你可以獲得實現在set標志中有效代碼的機會。例如,由于種種原因(或根本沒有原因),你就能夠拒絕一個新 值。最好是沒有人告訴你它是一個動態屬性——當你第一次請求它后,它會保存下來,故要盡可能地推遲資源分配。
5.4 索引 你想過象訪問數組那樣使用索引訪問類嗎 ?使用C#的索引功能,對它的期待便可了結。
語法基本上象這樣: 屬性 修飾符 聲明 { 聲明內容}
具體的例子為 public string this[int nIndex] { get { ... } set { ... } }
索引返回或按給出的index設置字符串。它沒有屬性,但使用了public修飾符。聲明部分由類型string和this 組成用于表 示類的索引。 get和set的執行規則和屬性的規則相同。(你不能取消其中一個。) 只存在一個差別,那就是:你幾乎可以任意定義大括 弧中的參數。限制為,必須至少規定一個參數,允許ref 和out 修飾符。 this關鍵字確保一個解釋。索引沒有用戶定義的名字,this 表示默認接口的索引。如果類實現了多個接口,你可以增加更 多個由InterfaceName.this說明的索引。
為了演示一個索引的使用,我創建了一個小型的類,它能夠解析一個主機名為IP地址——或一個IP地址列表(以 http://www.microsoft.com為例 )。這個列表通過索引可以訪問,你可以看一下清單5.10 的具體實現。
清單 5.10 通過一個索引獲取一個IP地址
1: using System; 2: using System.Net; 3: 4: class ResolveDNS 5: { 6: IPAddress[] m_arrIPs; 7: 8: public void Resolve(string strHost) 9: { 10: IPHostEntry iphe = DNS.GetHostByName(strHost); 11: m_arrIPs = iphe.AddressList; 12: } 13: 14: public IPAddress this[int nIndex] 15: { 16: get 17: { 18: return m_arrIPs[nIndex]; 19: } 20: } 21: 22: public int Count 23: { 24: get { return m_arrIPs.Length; } 25: } 26: } 27: 28: class DNSResolverApp 29: { 30: public static void Main() 31: { 32: ResolveDNS myDNSResolver = new ResolveDNS(); 33: myDNSResolver.Resolve("http://www.microsoft.com"); 34: 35: int nCount = myDNSResolver.Count; 36: Console.WriteLine("Found {0} IP's for hostname", nCount); 37: for (int i=0; i < nCount; i++) 38: Console.WriteLine(myDNSResolver[i]); 39: } 40: }
為了解析主機名,我用到了DNS類,它是System .Net 名字空間的一部分。但是,由于這個名字空間并不包含在核心 庫中,所以必須在編譯命令行中引用該庫: csc /r:System.Net.dll /out:resolver.exe dnsresolve.cs 解析代碼是向前解析的。在該 Resolve方法中,代碼調用DNS類的靜態方法GetHostByName,它返回一個IPHostEntry 對象。結果,該對象包含有我要找的數組——AddressList數組。在退出Resolve 方法之前,在局部的對象實例成員 m_arrIPs中,存儲了一個AddressList array的拷貝(類型IPAddress 的對象存儲在其中)。 具有現在生成的數組 ,通過使用在類ResolveDNS中求得的索引,應用程序代碼就可以在第37至38行列舉出IP地址。 (在第6章 "控制語句",有更多有關語句的信息。) 因為沒有辦法更改IP地址,所以僅給索引使用了get存取標志。為了 簡單其見,我忽略了數組的邊界溢出檢查。
5.4 事件 當你寫一個類時,有時有必要讓類的客戶知道一些已經發生的事件。如果你是一個具有多年編程經驗的程序員,似乎有 很多的解決辦法,包括用于回調的函數指針和用于ActiveX控件的事件接收(event sinks)。現在你將要學到另外一種把客 戶代碼關聯到類通知的辦法——使用事件。 事件既可以被聲明為類域成員(成員變量),也可以被聲明為屬性。兩者的共性為,事件的類型必定是代表元,而函 數指針原形和C#的代表元具有相同的含義。 每一個事件都可以被0或更多的客戶占用,且客戶可以隨時關聯或取消事件。你可以以靜態或者以實例方法定義代表 元,而后者很受C++程序員的歡迎。 既然我已經提到了事件的所有功能及相應的代表元,請看清單5.11中的例子。它生動地體現了該理論。
清單5.11 在類中實現事件處理 1: using System; 2: 3: // 向前聲明 4: public delegate void EventHandler(string strText); 5: 6: class EventSource 7: { 8: public event EventHandler TextOut; 9: 10: public void TriggerEvent() 11: { 12: if (null != TextOut) TextOut("Event triggered"); 13: } 14: } 15: 16: class TestApp 17: { 18: public static void Main() 19: { 20: EventSource evsrc = new EventSource(); 21: 22: evsrc.TextOut += new EventHandler(CatchEvent); 23: evsrc.TriggerEvent(); 24: 25: evsrc.TextOut -= new EventHandler(CatchEvent); 26: evsrc.TriggerEvent(); 27: 28: TestApp theApp = new TestApp(); 29: evsrc.TextOut += new EventHandler(theApp.InstanceCatch); 30: evsrc.TriggerEvent(); 31: } 32: 33: public static void CatchEvent(string strText) 34: { 35: Console.WriteLine(strText); 36: } 37: 38: public void InstanceCatch(string strText) 39: { 40: Console.WriteLine("Instance " + strText); 41: } 42: }
第4行聲明了代表元(事件方法原形),它用來給第8行中的EventSource類聲明TextOut事件域成員。你可以觀察到代 表元作為一種新的類型聲明,當聲明事件時可以使用代表元。 該類僅有一個方法,它允許我們觸發事件。請注意,你必須進行事件域成員不為null的檢測,因為可能會出現沒有客 戶對事件感興趣這種情況。 TestApp類包含了Main 方法,也包含了另外兩個方法,它們都具備事件所必需的信號。其中一個方法是靜態的,而另 一個是實例方法。 EventSource 被實例化,而靜態方法CatchEvent被預關聯上了 TextOut事件: evsrc.TextOut += new EventHandler(CatchEvent); 從現在起,當事件被觸發時,該方法被調用。如果你對事件不再感興趣,簡單地取消關聯: evsrc.TextOut -= new EventHandler(CatchEvent); 注意,你不能隨意取消關聯的處理函數——在類代碼中僅創建了這些處理函數。為了證明事件處理函數也和實例方法 一起工作,余下的代碼建立了TestApp 的實例,并鉤住事件處理方法。 事件在哪方面對你特別有用?你將經常在ASP+中或使用到WFC (Windows Foundation Classes)時,涉及到事件和代表 元。
5.5 應用修飾符 在這一章的學習過程中,你已經見過了象public、virtual等修飾符。欲以一種易于理解的方法概括它們,我把它們劃 分為三節:
。類修飾符 。成員修飾符 。存取修飾符
5.5.1 類修飾符 到目前為止,我還沒有涉及到類修飾符,而只涉及到了應用于類的存取修飾符。但是,有兩個修飾符你可以用于類: abstract——關于抽象類的重要一點就是它不能被實例化。只有不是抽象的派生類才能被實例化。派生類必須實現抽 象基類的所有抽象成員。你不能給抽象類使用sealed 修飾符。 sealed——密封 類不能被繼承。使用該修飾符防止意外的繼承,在.NET框架中的類用到這個修飾符。 要見到兩個修飾符的運用,看看清單5.12 ,它創建了一個基于一個抽象類的密封類(肯定是一個十分極端的例子)。
清單 5.12 抽象類和密封類
1: using System; 2: 3: abstract class AbstractClass 4: { 5: abstract public void MyMethod(); 6: } 7: 8: sealed class DerivedClass:AbstractClass 9: { 10: public override void MyMethod() 11: { 12: Console.WriteLine("sealed class"); 13: } 14: } 15: 16: public class TestApp 17: { 18: public static void Main() 19: { 20: DerivedClass dc = new DerivedClass(); 21: dc.MyMethod(); 22: } 23: }
5.5.2 成員修飾符 與有用的成員修飾符的數量相比,類修飾符的數量很少。我已經提到了一些,這本書即將出現的例子描述了其它的成 員修飾符。 以下是有用的成員修飾符: abstract——說明一個方法或存取標志不能含有一個實現。它們都是隱式虛擬,且在繼承類中,你必須提供 override關鍵字。 const——這個修飾符應用于域成員或局部變量。在編譯時常量表達式被求值,所以,它不能包含變量的引用。 event ——定義一個域成員或屬性作為類型事件。用于捆綁客戶代碼到類的事件。 extern——告訴編譯器方法實際上由外部實現。第10章 “和非受管代碼互相操作” 將全面地涉及到外部代碼。 override——用于改寫任何基類中被定義為virtual的方法和存取標志。要改寫的名字和基類的方法必須一致。 readonly——一個使用 readonly修飾符的域成員只能在它的聲明或者在包含它的類的構造函數中被更改。
static——被聲明為static的成員屬于類,而不屬于類的實例。你可以用static 于域成員、方法、屬性、操作符甚至 構造函數。 virtual——說明方法或存取標志可以被繼承類改寫。
5.5.3 存取修飾符 存取修飾符定義了某些代碼對類成員(如方法和屬性)的存取等級。你必須給每個成員加上所希望的存取修飾符,否 則,默認的存取類型是隱含的。 你可以應用4個 存取修飾符之一: public——任何地方都可以訪問該成員,這是具有最少限制的存取修飾符。 protected——在類及所有的派生類中可以訪問該成員,不允許外部訪問。 private——僅僅在同一個類的內部才能訪問該成員。甚至派生類都不能訪問它。 internal——允許相同組件(應用程序或庫)的所有代碼訪問。在.NET組件級別,你可以把它視為public,而在外部 則為private。 為了演示存取修飾符的用法,我稍微修改了Triangle例子,使它包含了新增的域成員和一個新的派生類(見清單 5.13)。
清單 5.13 在類中使用存取修飾符
1: using System; 2: 3: internal class Triangle 4: { 5: protected int m_a, m_b, m_c; 6: public Triangle(int a, int b, int c) 7: { 8: m_a = a; 9: m_b = b; 10: m_c = c; 11: } 12: 13: public virtual double Area() 14: { 15: // Heronian formula 16: double s = (m_a + m_b + m_c) / 2.0; 17: double dArea = Math.Sqrt(s*(s-m_a)*(s-m_b)*(s-m_c)); 18: return dArea; 19: } 20: } 21: 22: internal class Prism:Triangle 23: { 24: private int m_h; 25: public Prism(int a, int b, int c, int h):base(a,b,c) 26: { 27: m_h = h; 28: } 29: 30: public override double Area() 31: { 32: double dArea = base.Area() * 2.0; 33: dArea += m_a*m_h + m_b*m_h + m_c*m_h; 34: return dArea; 35: } 36: } 37: 38: class PrismApp 39: { 40: public static void Main() 41: { 42: Prism prism = new Prism(2,5,6,1); 43: Console.WriteLine(prism.Area()); 44: } 45: }
Triangle 類和 Prism 類現在被標為 internal。這意味著它們只能在當前組件中被訪問。請記住“.NET組件”這 個術語指的是包裝( packaging,),而不是你可能在COM+中用到的組件。Triangle 類有三個 protected成員,它們在構 造函數中被初始化,并用于面積計算的方法中。由于這些成員是protected 成員,所以我可以在派生類Prism中訪問它們, 在那里執行不同的面積計算。Prism自己新增了一個成員m_h,它是私有的——甚至派生類也不能訪問它。 花些時間為每個類成員甚至每個類計劃一種保護層次,通常是個好主意。當需要引入修改時,全面的計劃最終會幫 助你,因為沒有程序員會愿意使用“沒有文檔”的類功能。 5.6 小結 這章顯示了類的各種要素,它是運行實例(對象)的模板。在一個對象的生命期,首先被執行的代碼是個構造函數。 構造函數用來初始化變量,這些變量后來在方法中用于計算結果。 方法允許你傳遞值、引用給變量,或者只傳送一個輸出值。方法可以被改寫以實現新的功能,或者你可以屏蔽基類成 員,如果它實現了一個具有和派生類成員相同名字的方法。 命名屬性可以被當作域成員(成員變量)或屬性存取標志實現。后者是get和set存取標志,忽略一個或另外一個,你 可以創建僅寫或僅讀屬性。存取標志非常適合于確認賦給屬性的值。 C#類的另外一個功能是索引,它使象數組語法一樣訪問類中值成為可能。還有,如果當類中的某些事情發生時,你想 客戶得到通知,要讓它們與事件關聯。 當垃圾收集器調用析構函數時,對象的生命就結束了。由于你不能準確地預測這種情況什么時候會發生,所以應該創 建一個方法以釋放這些寶貴的資源,當你停止使用它們時。
|