程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> 《.net編程先鋒C#》第五章 類(下)

《.net編程先鋒C#》第五章 類(下)

編輯:關於C語言
5.2.3 方法屏蔽
重定義方法的一個不同手段就是要屏蔽基類的方法。當從別人提供的類派生類時,這個功能特

別有價值。看清單 5.6,假設BaseClass由其他人所寫,而你從它派生出 DerivedClass 。

清單 5.6 Derived Class 實現一個沒有包含於 Base Class中的方法

1: using System;
2:
3: class BaseClass
4: {
5: }
6:
7: class DerivedClass:BaseClass
8: {
9: public void TestMethod()
10: {
11: Console.WriteLine("DerivedClass::TestMethod");
12: }
13: }
14:
15: class TestApp
16: {
17: public static void Main()
18: {
19: DerivedClass test = new DerivedClass();
20: test.TestMethod();
21: }
22: }

在這個例子中, DerivedClass 通過TestMethod()實現了一個額外的功能。但是,如果基類的

開發者認為把TestMethod()放在基類中是個好主意,並使用相同的名字實現它時,會出現什麼問題

呢?(見清單5.7)

清單 5.7 Base Class 實現和 Derived Class相同的方法

1: class BaseClass
2: {
3: public void TestMethod()
4: {
5: Console.WriteLine("BaseClass::TestMethod");
6: }
7: }
8:
9: class DerivedClass:BaseClass
10: {
11: public void TestMethod()
12: {
13: Console.WriteLine("DerivedClass::TestMethod");
14: }
15: }

在優秀的編程語言中,你現在會遇到一個真正的大麻煩。但是,C#會給你提出警告:
hiding2.cs(13,14): warning CS0114: 'DerivedClass.TestMethod()' hides inherited member

'BaseClass.TestMethod()'. To make the current method override that implementation, add

the override keyword. Otherwise add the new keyWord.
(hiding2.cs(13,14):警告 CS0114:'DerivedClass.TestMethod()' 屏蔽了所繼承的成員

'BaseClass.TestMethod()'。要想使當前方法改寫原來的實現,加上 override關鍵字。否則加上新

的關鍵字。)
具有了修飾符new,你就可以告訴編譯器,不必重寫派生類或改變使用到派生類的代碼,你的方法就

能屏蔽新加入的基類方法。清單5.8 顯示如何在例子中運用new修飾符。

清單 5.8 屏蔽基類方法

1: class BaseClass
2: {
3: public void TestMethod()
4: {
5: Console.WriteLine("BaseClass::TestMethod");
6: }
7: }
8:
9: class DerivedClass:BaseClass
10: {
11: new public void TestMethod()
12: {
13: Console.WriteLine("DerivedClass::TestMethod");
14: }
15: }

使用了附加的new修飾符,編譯器就知道你重定義了基類的方法,它應該屏蔽基類方法。但是,如果

你按以下方式編寫:
DerivedClass test = new DerivedClass();
((BaseClass)test).TestMethod();
基類方法的實現就被調用了。這種行為不同於改寫方法,後者保證大部分派生方法獲得調用。
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#類的另外一個功能是索引,它使象數組語法一樣訪問類中值成為可能。還有,如果當類中的某些事情發生時,你想客戶得到通知,要讓它們與事件關聯。
當垃圾收集器調用析構函數時,對象的生命就結束了。由於你不能准確地預測這種情況什麼時候會發生,所以應該創建一個方法以釋放這些寶貴的資源,當你停止使用它們時。
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved