第3章 C#面向對象程序設計
第二章介紹了C#的語法和基礎知識。據此我們已經可以寫出一些控制台應用程序了。但是,要了解C#語言的強大功能,還需要使用面向對象編程(Object-Oriented Programming,OOP)技術。實際上,前面的例子已經在使用這些技術,但沒有重點講述。
本章先探討OOP的原理,包括OOP的基礎知識、與OOP相關的術語。接着學習如何在C#中定義類,包括基本的類定義語法、用於確定類可訪問性的關鍵字以及接口的定義。然后討論如何定義類成員,包括如何定義字段、數學和方法等成員。最后說明一些高級技術,包括集合、運算符重載、高級轉換、深度復制和定制異常。
3.1面向對象編程簡介
3.1.1 什么是面向對象編程
面向對象編程代表了一種全新的程序設計思路,與傳統的面向過程開發方法不同,面向對象的程序設計和問題求解更符合人們的思維習慣。
前面介紹的編程方法都是面向過程的程序設計方法,這種方法常常會導致所謂的單一應用程序,即所有的功能都包含在幾個代碼模塊中(常常是一個代碼模塊),適合解決比較小的簡單問題。而OOP技術則按照現實世界的特點來管理復雜的事物,把它們抽象為對象,具有自己的狀態和行為,通過對消息的反應來完成一定的任務。這種編程方法提供了非常強大的多樣性,大大增加了代碼的重用機會,增加了程序開發的速度;同時降低了維護負擔,將具備獨立性特制的程序代碼包裝起來,修改部分程序代碼時不至於會影響到程序的其他部分。
1. 對象
什么是對象?實際上,現實世界就是由各種對象組成的,如人、汽車、動物、植物等。復雜的對象可以由簡單的對象組成。對象都具有各自的屬性,如形狀、顏色、重量等;對外界都呈現出各自的行為,如人可以走路、說話、唱歌;汽車可以啟動、加速、減速、剎車、停止等。
在OOP中,對象就是變量和相關的方法的集合。其中變量表明對象的屬性,方法表明對象所具有的行為。一個對象的變量構成了這個對象的核心,包圍在它外面的方法使這個對象和其他對象分離開來。例如:我們可以把汽車抽象為一個對象,用變量來表示它當前的狀態,如速度、油量、型號、所處的位置等,它的行為則為上面提到的加速、剎車、換檔等。操作汽車時。不用去考慮汽車內部各個零件如何運作的細節,而只需根據汽車可能的行為使用相應的方法即可。實際上,面向對象的程序設計實現了對象的封裝,使我們不必關心對象的行為是如何實現的這樣一些細節。通過對對象的封裝,實現了模塊化和信息隱藏。有利於程序的可移植性和安全性,同時也利於對復雜對象的管理。
簡單地說,對象非常類似於本書前面討論的結構類型。略為復雜的對象可能不包含任何數據,而是只包含函數,表示一個過程。
2.類
在研究對象時主要考慮對象的屬性和行為,有些不同的對象會呈現相同或相似的屬性和行為,如轎車、卡車、面包車。通常將屬性及行為相同或相似對象歸為一類。類可以看成是對象的抽象,代表了此類對象所具有的共同屬性和行為。典型的類是“人類”,表明人的共同性質。比如我們可以定義一個汽車類來描述所有汽車的共性。通過類定義人們可以實現代碼的復用。我們不用去描述每一個對象(如某輛汽車),而是通過創建類(如汽車類)的一個實例來創建該類的一個對象,這樣大大鹼化了軟件的設計。
類是對一組具有相同特征的對象的抽象描述,所有這些對象都是這個類的實例。在C#中,類是一種數據類型,而對象是該類型的變量,變量名即是某個具體對象的標示名。
3.屬性和字段
通過屬性和字段可以訪問對象中包含的數據。對象數據可以區分不同的對象,因為同一個類的不同對象可能在屬性和字段中存儲了不同的值。包含在對象中的不間數據統稱為對象的狀態。
假定一個對象類表示一杯咖啡,叫做CupOfCoffee。在實例化這個類(即創建這個類的對象)時,必須提供對於類有意義的狀態。此時可以使用屬性和字段,讓代碼能通過該對象來設置要使用的咖啡品牌,咖啡中是否加牛奶或方糖,咖啡是否即溶等。給定的咖啡對象就有一個指定的狀態,例如“Columbian filter coffee with milk and two sugars”。
可以把信息存儲在字段和屬性中,作為string變量、int變量等。但是,屬性與字段是不同的,屬性不能直接訪問數據。一般情況下,在訪問狀態時最好提供屬性,而不是字段,因為這樣可以更好地控制整個過程,而使用它們的語法是相同的。
對屬性的讀寫訪問也可以由對象來明確定義。某些屬性是只讀的,只能查看它們的值,而不能改變侖們(至少不能直接改變)。還可以有只寫的屬性,其操作方式類似。
除了對屬性的讀寫訪問外,還可以為字段和屬性指定另—種訪問許可,這種可訪問性確定了什么代碼可以訪問這些成員,它們是可用於所有的代碼(公共),還是只能用於類中的代碼(私有),或者更復雜的模式。常見的情況是把字段設置為私有,通過公共屬性訪問它們。
例如,CupOfCoffee類,可以定義5個成員:Type、isInstant、Milk、Sugar、Description等。
4.方法
對象的所有行為都可以用方法來描述,在C#中,方法就是對象中的函數。
方法用於訪問對象的功能,與字段和屬性—樣:方法可以是公共的或私有的,按照需要限制外部代碼的訪問。它們常常使用對象狀態——訪問私有成員。例如,CupOfCoffee類定義了一個方法AddSugar()來增加方糖數屬性。
實際上,C#中的所有東西都是對象。控制台應用程序中的Main()函數就是類的一個方法。前面介紹的每個變量類型都是一個類。前面使用的每個命令都是一個屬性或方法。句點字符“.”把對象實例名和屬性或方法名分隔開來。
5.對象的生命周期
每個對象都一個明確定義的生命周期,即從使用類定義開始一直到刪除它為止。在對象的生命周期中,除了“正在使用”的正常狀態之外,還有兩個重要的階段:
● 構造階段——對象最初進行實例化的時期。這個初始化過程稱為構造階段,由構造函數完成。
● 析構階段——在刪除一個對象時,常常需要執行一些清理工作,例如釋放內存,由析構函數完成。
5.1構造函數
所有的對象都有一個默認的構造成數,該函數沒有參數,與類本身有相同的名稱。一個類定義可以包含幾個構造函數,它們有不同的簽名,代碼可以使用這些簽名實例化對象。帶有參數的構造函數通常用於給存儲在對象中的數據提供初始值。
在C#中,構造函數用new關鍵字來調用。例如,可以用下面的方式實例化一個CupOfCoffee對象:
CupOfCoffee myCup = new CupOfCoffee();
對象還可以用非默認的構造函數來創建。與默認的構造函數一樣,非默認的構造函數與類同名,但它們還帶有參數,例如:
CupOfCoffee myCup = new CupOfCoffee(“Blue Mountain”);
構造函數與字段、屬性和方法一樣,可以是公共或私有的。在類外部的代碼不能使用私有構造函數實例化對象,而必須使用公共構造函數。—些類沒有公共的構造函數,外部的代碼就不可能實例化它們。
5.2 析構函數
析構函數在用於清理對象。一般情況下,不需要提供解構方法的代碼,而是由默認的析構函數執行操作。但是,如果在刪除對象實例前,需要完成一些重要的操作,就應提供特定的析構函數。
6.靜態成員
屬性、方法和字段等成員是對象實例所特有的,即改變一個對象實例的這些成員不影響其他的實例中的這些成員。除此之外,還有一種靜態成員(也稱為共享成員),例如靜態方法、靜態屬性或靜態字段。靜態成員可以在類的實例之間共享,所以它們可以看作是類的全局對象。靜態屬性和靜態字段可以訪問獨立於任何對象實例的數據,靜態方法可以執行與對象類型相關、但不是特定實例的命令,在使用靜態成員時,甚至不需要實例化類型的對象。例如,前畫使用的Console.WriteLine()方法就是靜態的。
3.1.2 OOP技術
前面介紹了一些基礎知識,下面討論OOP中的一些技術,包括:抽象與接口、繼承、多態性、運算符重載等。
1. 抽象與接口
抽象化是為了要降低程序版本更新后,在維護方面的負擔,使得功能的提供者和功能的用戶分開,各自獨立,彼此不受影響。
為了達到抽象化的目的,需要在功能提供者與功能使用者之間提供一個共同的規范,功能提供者與功能使用者都要按照這個規范來提供、使用這些功能。這個共用的規范就是接口,接口定義了功能數量、函數名稱、函數參數、參數順序等。它是一個能聲明屬性、字段和方法的編程構造。它不為這些成員實現,只提供定義。接口定義了功能提供者與功能使用者之間的准則,因此只要接口不變,功能提供者就可以任意更改實現的程序代碼,而不影響到使用者。
一旦定義了接口,就可以在類中實現它。這樣,類就可以支持接口所指定的所有屬件和成員。注意,不能實例化接口,執行過程必須在實現接口的類中實現。
在前面的咖啡范例中,可以把較一般用途的屬性和方法例如AddSugar(),Milk,Sugar和
Instant組合到一個接口中,稱為IhotDrink(接口的名稱一般用大寫字母I開頭)。然后就可以在其他對象上使用該接口,例如CupOfTea類。
一個類可以支持多個接口,多個類也可以支持相同的接口。
2.繼承
繼承是OOP最重要的特性之—。任何類都可以從另—個類繼承,這就是說,這個類擁有它繼承的類的所有成員。在00P中,被繼承(也稱為派生)的類稱為父類(也稱為基類)。注意C#中的對象僅能派生於一個基類。
公共汽車、出租車、貨車等都是汽車,但它們是不同的汽車,除了具有汽車的共性外,它們還具有自己的特點,如不同的操作方法,不同的用途等。這時我們可以把它們作為汽車的子類來實現,它們繼承父類(汽車)的所有狀態和行為,同時增加自己的狀態和行為。通過父類和子類,我們實現了類的層次,可以從最一般的類開始,逐步特殊化,定義一系列的子類。同時,通過繼承也實現了代碼的復用,使程序的復雜性線性地增長,而不是呈幾何級數增長。
在繼承一個基類時,成員的可訪問性就成為一個重要的問題。派生類不能訪問基類的私有成員,但可以訪問其公共成員。不過,派生類和外部的代碼都可以訪問公共成員。這就是說,只使用這兩個可訪問性,不僅可以讓一個成員被基類和派生類訪問,而且也能夠被外部的代碼訪問。為了解決這個問題,C#提供了第三種可訪問性:protected,只有派生類才能訪問protected成員。
除了成員的保護級別外,我們還可以為成員定義其繼承行為。基類的成員可以足虛擬的,也就是說,成員可以由繼承它的類重寫。派生類可以提供成員的其他執行代碼。這種執行代碼不會刪除原來的代碼,仍可以在類中訪問原來的代碼,但外部代碼不能訪問它們。如果沒有提供其他執行方式,外部代碼就訪問基類中成員的執行代碼。虛擬成員不能是私有成員。
基類還可以定義為抽象類。抽象類不能直接實例化。要使用抽象類,必須繼承這個類,抽象類可以有抽象成員,這些成員在基類中沒有代碼實現,所以這些執行代碼必須在派生類中提供。
最后,類可以是密封的。密封的類不能用作基類,所以也沒有派生類。
在C#中,所有的對象都有—個共同的基類object,我們在第二章中曾提到過。
3.多態性
多態是面向對象程序設計的又一個特性。在面向過程的程序設計中,主要工作是編寫一個個的過程或函數,這些過程和函數不能重名。例如在一個應用中,需要對數值型數據進行排序,還需要對字符型數據進行排序,雖然使用的排序方法相同,但要定義兩個不同的過程(過程的名稱也不同)來實現。
在面向對象程序設計中,可以利用“重名”來提高程序的抽象度和簡潔性。首先我們來理解實際的現象,例如,“啟動”是所有交通工具都具有的操作,但是不同的具體交通工具,其“啟動”操作的具體實現是不同的,如汽車的啟動是“發動機點火——啟動引擎”、“啟動”輪船時要“起錨”、氣球飛艇的“啟動”是“充氣——解纜”。如果不允許這些功能使用相同的名字,就必須分別定義“汽車啟動”、“輪船啟動”、“氣球飛艇啟動”多個方法。這樣一來,用戶在使用時需要記憶很多名字,繼承的優勢就盪然無存了。為了解決這個問題,在面向對象的程序設計中引入了多態的機制。
多態是指一個程序中同名的不同方法共存的情況。主要通過子類對父類方法的覆蓋來實現多態。這樣一來,不同類的對象可以響應同名的方法來完成特定的功能,但其具體的實現方法卻可以不同。例如同樣的加法,把兩個時間加在一起和把兩個整數加在一起肯定完全不同。
通過方法覆蓋,子類可以重新實現父類的某些方法,使其具有自己的特征。例如對於車類的加速方法,其子類(如賽車)中可能增加了一些新的部件來改善提高加速性能,這時可以在賽車類中覆蓋父類的加速方法。覆蓋隱藏了父類的方法,使子類擁有自己的具體實現,更進一步表明了與父類相比,子類所具有的特殊性。
多態性使語言具有靈活、抽象、行為共享的優勢,很好地解決了應用程序函數同名問題。
注意並不是只有共享同一個父類的類才能利用多態性。只要子類和孫子類在繼承層次結構
中有一個相同的類,它們就可以用相同的方式利用多態性。
4.重載
方法重載是實現多態的另一個方法。通過方法重載,一個類中可以有多個具有相同名字的方法,由傳遞給它們的不同個數的參數來決定使用哪種方法。例如,對於一個作圖的類,它有一個draw()方法用來畫圖或輸出文字,我們可以傳遞給它一個字符串、一個矩形、一個圓形,甚至還可以再制定作圖的初始位置、圖形的顏色等。對於每一種實現,只需實現一個新的draw()方法即可,而不需要新起一個名字,這樣大大簡化了方法的實現和調用,程序員和用戶不需要記住很多的方法名,只需要傳入相應的參數即可。
因為類可以包含運算符如何運算的指令,所以可以把運算符用於從類實例化而來的對象。 我們為重載運算符編寫代碼,把它們用作類定義的一部分,而該運算符作用於這個類。也可以重載運算符,以相同的方式處理不同的類,其中一個(或兩個)類定義包含達到這一目的的代碼。
注意只能用這種方式重載現有的C#運算符,不能創建新的運算符。
5.消息和事件
對象之間必須要進行交互來實現復雜的行為。例如,要汽車加速,必須發給它一個消息,告訴它進行何種動作(這里是加速)以及實現這種動作所需要的參數(這里是需要達到的速度等)。一個消息包含三個方面的內容:消息的接收者、接收對象應采用的方法、方法所需要的參數。同時,接收消息的對象在執行相應的方法后,可能會給發送消息的對象返回一些信息。如上例中,汽車的儀表上會出現已達到的速度等。
在C#中,消息處理稱為事件。對象可以激活事件,作為它們處理的一部分。為此,需要給代碼添加事件處理程序,這是一種特殊類型的函數,在事件發生時調用。還需要配置這個處理程序,以監聽我們感興趣的事件。
使用事件可以創建事件驅動的應用程序,這類應用程序很多。例如,許多基於Windows的應用程序完全依賴於事件。每個按鈕單擊或滾動條拖動操作都是通過事件處理實現的,其中事件是通過鼠標或鍵盤觸發的。本章的后面將介紹事件是如何工作的。
3.2 定義類
本節將重點討論如何定義類本身。首先介紹基本的類定義語法、用於確定類可訪問性的關鍵字、指定繼承的方式以及接口的定義。
3.2.1 C#中的類定義
3.2.1.1 類的定義
C#使用class關鍵字來定義類。其基本結構如下:
Class MyClass
{
// class members
}
這段代碼定義了一個類MyClass。定義了一個類后,就可以對該類進行實例化。在默認情況下,類聲明為內部的,即只有當前代碼才能訪問,可以用intemal訪問修飾符關鍵字顯式指定,如下所示(但這是不必要的):
internal class MyClass
{
// class members
}
另外,還可以制定類是公共的,可以由其它任意代碼訪問。為此,需要使用關鍵字public:
public class MyClass
{
// class members
}
除了這兩個訪問修飾符關鍵字外,還可以指定類是抽象的(不能實例化,只能繼承,可以有抽象成員)或密封的(sesled,不能繼承)。為此,可以使用兩個互斥的關鍵字abstract或sealed。所以,抽象類必須用下述方式聲明:
public abstract class MyClass
{
// class members, may be abstract
}
密封類的聲明如下所示:
public sealed class MyClass
{
//class members
}
還可以在類定義中指定繼承。C#支持類的單一繼承,即只能有一個基類,語法如下:
class MyClass : MyBaseClass
{
// class members
}
在C#的類定義中,如果繼承了一個抽象類,就必須執行所繼承的所有抽象成員(除非派生類也是抽象的)。
編譯器不允許派生類的可訪問性比其基類更高。也就是說,內部類可以繼承於一個公共類,但公共類不能繼承於一個內部類。因此,下述代碼就是不合法的:
internal class MyBaseClass
{
// class members
}
public class MyClass : MyBaseClass
{
// class members
}
在C#中,類必須派生於另一個類。如果沒有指定基類,則被定義的類就繼承於基類System.Object。
除了以這種方式指定基類外,還可以指定支持的接口。如果指定了基類,它必須緊跟在冒號的后面,之后才是指定的接口。必須使用逗號分隔基類名(如果有基類)和接口名。
例如,給MyClass添加一接口,如下所示:
class MyClass : IMyInterface
{
// class memebrs
}
所有的接口成員都必須在支持該接口的類中實現,但如果不想使用給定的接口成員,可以提供一個“空”的執行方式(沒有函數代碼)。
下面的聲明是無效的,因為基類MyBaseClass不是繼承列表中的第一項:
class MyClass : IMyInterface, MyBaseClass
{
// class members
}
指定基類和接口的正確方式如下:
class : MyBaseClass, ImyInterface
{
// class members
}
可以指定多個接口,所以下面的代碼是有效的:
public class MyClass : MyBaseClass, ImyInterface, ImySecondInterface
{
// class members
}
表3.1是類定義個可以使用的訪問修飾符組合。
表3.1 訪問修飾符
修飾符 |
含義 |
none或internal |
類只能在當前程序中被訪問 |
public |
類可以在任何地方訪問 |
abstract或internal abstract |
類只能在當前程序中被訪問,不能實例化,只能繼承 |
public abstract |
類可以在任何地方訪問,不能實例化,只能繼承 |
sealed或internal sealed |
類只能在當前程序中被訪問,不能派生,只能實例化 |
public sealed |
類可以在任何地方訪問,不能派生,只能實例化 |
3.2.1.2接口的定義
接口聲明的方式與聲明類的方式相似,但使用的是關鍵字interface,例如:
interface ImyInterface
{
// interface members
}
訪問修飾符關鍵字public和internal的使用方式是相同的,所以要使接口的訪問是公共的,就必須使用public關鍵字:
public interface ImyInterface
{
// interface members
}
關鍵字abstract和sealed不能在接口中使用,因為這兩個修飾符在接口定義中是沒有意義的(接口不包含執行代碼,所以不能直接實例化,且必須是可以繼承的)。
接口的繼承也可以用與類繼承的類似方式來指定。主要的區別是可以使用多個基接口,例如:
public interface IMyInterface : IMyBaseInterface, ImyBaseInterface2
{
// interface members
}
下面看一個類定義的范例。
【例3-1】
using System;
public abstract class MyBaseClass
{
}
class MyClass:MyBaseClass
{
}
public interface IMyBaseInterface
{
}
interface IMyBaseInterface2
{
}
interface ImyInterface : IMyBaseInterface, IMyBaseInterface2
{
}
sealed class MyComplexClass:MyClass,IMyInterface
{
}
class Class1
{
static void Main(string[] args)
{
MyComplexClass myObj = new MyComplexClass();
Console.WriteLine(myObj.ToString());
}
}
這里的Clsss1不是主要類層次結構中的一部分,而是處理Main()方法的應用程序的入口點。MyBaseClass和IMyBaseInterface被定義為公共的,其他類和接口都是內部的。其中MyComplexClass繼承MyClass和IMyInterface,MyClass繼承MyBassClass,IMyInterface繼承IMyBaseInterface和IMyInterface2,而MyBaseClass和IMyBaseInterface、IMyBaseInterface2的共同的基類為object。Main()中的代碼調用MyComplexClass的一個實例myObj的ToString()方法。這是繼承System.ObJect的一種方法,功能是把對象的類名作為一個字符串返回,該類名用所有相關的命名空間來限定。
3.2.2 Object類
前面提到所有的.NET類都派生於System.Object。實際上,如果在定義類時沒有指定基類,編譯器就會自動假定這個類派生於object。其重要性在於,自己定義的所有類除了自己定義的方法和屬性外,還可以訪問為Object定義的許多公共或受保護的成員方法。在object中定義的方法如表3.2所示。
表3.2 object中的方法
方 法 |
訪問修飾符 |
作 用 |
string ToString() |
public virtual |
返回對象的字符串表示。在默認情況下,這是一個類類型的限定名,但它可以被重寫,以便給類類型提供合適的實現方式 |
int GetHashTable() |
public virtual |
在實現散列表時使用 |
bool Equals(object obj) |
public virtual |
把調用該方法的對象與另一個對象相比較,如果它們相等,就返回true。以默認的執行方式進行檢查,以查看對象的參數是否引用了同一對象。如果想以不同的方式來比較對象,可以重寫該方法。 |
bool Equals(object objA, object objB) |
public static |
這個方法比較傳遞給它的兩個對象是否相等。如果兩個對象都是空引用,這個方法會返回true。 |
bool ReferenceEquals(object objA, object objB) |
public static |
比較兩個引用是否指向同一個對象 |
Type GetType() |
public |
返回對象類型的詳細信息 |
object MemberwiseClone() |
protected |
通過創建一個新對象實例並復制成員,來復制該對象。成員復制不會得到這些成員的新實例。新對象的任何引用類型成員都將引用與源類相同的對象,這個方法是受保護的,所以只能在類或派生的類中使用。 |
這些方法是.NET Framework中對象類型必須支持的基本方法,但我們可以從不使用它們。下面將簡要幾個方法的作用。
GetType()方法:這個方法返回從System.Type派生的類的一個實例。在利用多態性時,GetType()是一個有用的方法,它允許根據對象的類型來執行不同的操作。聯合使用GetType()和typeof(),就可以進行比較,如下所示:
if (myObj.GetType() == typeof(MyComplexClass))
{
// myObj is an instance of the class MyComplexClass
}
ToString()方法:是獲取對象的字符串表示的一種便捷方式。當只需要快速獲取對象的內容,以用於調試時就可以使用這個方法。在數據的格式化方面,它提供的選擇非常少:例如,日期在原則上可以表示為許多不同的格式,但DateTime.ToString()沒有在這方面提供任何選擇。例如:
int i = -50;
string str = i.ToString(); // returns "–50"
下面是另一個例子:
enum Colors {Red, Orange, Yellow};
// later on in code...
Colors favoriteColor = Colors.Orange;
string str = favoriteColor.ToString(); // returns "Orange"
Object.ToString()聲明為虛類型,在這些例子中,該方法的實現代碼都是為C#預定義數據類型重寫過的代碼,以返回這些類型的正確字符串表示。Colors枚舉是一個預定義的數據類型,它實際上實現為一個派生於System.Enum的結構,而System.Enum有一個ToString()重寫方法,來處理用戶定義的所有枚舉。
如果不在自己定義的類中重寫ToString(),該類將只繼承System.Object執行方式——顯示類的名稱。如果希望ToString()返回一個字符串,其中包含類中對象的值信息,就需要重寫它。下面用一個例子Money來說明這一點。在該例子中,定義一個非常簡單的類Money,表示錢數。Money是decimal類的包裝器,提供了一個ToString()方法(這個方法必須聲明為override,因為它將重寫Object提供的ToString()方法)。該例子的完整代碼如下所示:
【例3-2】
using System;
class MainEntryPoint
{
static void Main(string[] args)
{
Money cash1 = new Money();
cash1.Amount = 40M;
Console.WriteLine("cash1.ToString() returns: " + cash1.ToString());
}
}
class Money
{
private decimal amount;
public decimal Amount
{
get
{
return amount;
}
set
{
amount = value;
}
}
public override string ToString()
{
return "$" + Amount.ToString();
}
}
在Main()方法中,先實例化一個Money對象,在這個實例化過程中調用了ToString(),選擇了我們自己的重寫方法。運行這段代碼,會得到如下結果:
StringRepresentations
cash1.ToString() returns: $40
3.2.3 構造函數和析構函數
在C#中定義類時,常常不需要定義相關的構造函數和析構函數,因為基類System.Object提供了一個默認的實現方式。但是,如果需要,也可以提供我們自己的構造函數和析構函數,以便初始化對象和清理對象。
1.構造函數
使用下述語法把簡單的構造函數添加到一個類中:
class MyClass
{
public MyClass()
{
// Constructor code
}
// rest of class definition
}
這個構造函數與包含它的類同名,且沒有參數,這是一個公共函數,所以用來實例化類的對象。
也可以使用私有的默認構造函數,即這個類的對象實例不能用這個構造函數來創建。例如:
class MyClass
{
private MyClass()
{
//Constructor code
}
// rest of class definition
}
構造函數也可以重載,即可以為構造函數提供任意多的重載,只要它們的簽名有明顯的區別,例如:
class MyClass
{
public MyClass()
{
//Default contructor code
}
public MyClass(int number)
{
//Non-default contructot code
}
//rest of class definition
}
如果提供了帶參數的構造函數,編譯器就不會自動提供默認的構造函數,下面的例子中,因為明確定義了一個帶一個參數的構造函數,所以編譯器會假定這是可以使用的唯一構造函數,不會隱式地提供其他構造函數:
public class MyNumber
{
public MyNumber(int number)
{
// Contructor code
}
// rest of class definition
}
2.構造函數的執行序列
在討論構造函數前,先看看在默認情況下,創建類的實例時會發生什么情況。
為了實例化派生的類,必須實例化它的基類。而要實例化這個基類,又必須實例化這個基類的基類,這樣一直到實例化System.Object為止。結果是無論使用什么構造函數實例化一個類,總是要先調用System.ObJect.Object()。
如果對一個類使用非默認的構造函數,默認的情況是在其基類上使用匹配十這個構造函數簽名的構造函數。如果沒有找到這樣的構造函數,就使用基類的默認構造函數。下面介紹一個例子,說明事件的發生順序。代碼如下:
public class MyBaseClass
{
public MyBaseClass()
{
}
public MyBaseClass(int i)
{
}
}
public class MyDerivedClass : MyBaseClass
{
public MyDerivedClass()
{
}
public MyDerivedClass(int i)
{
}
public MyDerivedClass(int i, int j)
{
}
}
如果以下面的方式實例化MyDerivedClass:
MyDrivedClass myObj = new MyDerivedClass();
則發生下面的一系列事件:
● 執行System.Object.Object()構造函數。
● 執行MyBaseClass. MyBaseClass()構造函數。
● 執行MyDrivedClass. MyDerivedClass()構造函數。
另外,如果使用下面的語句:
MyDrivedClass myObj = new MyDrivedClass(4);
則發生下面的一系列事件:
● 執行System.Object.Object()構造函數。
● 執行MyBaseClass. MyBaseClass(int i)構造函數。
● 執行MyDrivedClass. MyDerivedClass(int i)構造的數。
最后,如果使用下面的語句;
MyDeivedClass myObj = new MyDerivcdClass(4, 8);
則發生下面的一系列事件:
● 執行System. Object. Object()構造函數。
● 執行MyBaseClass. MyBaseClass()構造函數。
● 執行MyDerivedClass. MyDerivedClass(int i,tnt j)構造函數。
有時需要對發生的事件進行更多的控制。例如,在上面的實例化例子中,需要有下面的事件序列:
● 執行System.Object. Object()構造函數。
● 執行MyBaseClass. MyBaseClass(int i)構造函數。
● 執行MyDerivedClass. MyDerivedClass(int i, int j)構造函數。
使用這個序列可以編寫在MyBaseClass(int i)中使用int i參數的代碼,即MyDerivedClass(int i, int j))構造函數要做的工作比較少,只需要處理int j參數(假定int i參數在兩種情況下有相同的含義)。為此,只需指定在派生類的構造函數定義中所使用的基類的構造函數即可,如下所示:
public class MyDerivedClass : MyBaseClass
{
…
public MyDerivedClass(int i, int j) : base(i)
{
}
}
其中,base關鍵字指定.NET實例化過程,以使用基類中匹配指定簽名的構造函數。這里使用了一個int i參數,所以應使用MyBaseClass(int i)。這么做將不調用MyBaseClass(),而是執行本例前面列出的事件序列。
也可以使用這個關鍵字指定基類構造函數的字面值,例如使用MyDerivedClass的默認構造函數調用MyBaseClass非默認的構造函數:
public class MyDerivedClass : MyBaseClass
{
public MyDerivedClass() : base(5)
{
}
…
}
這段代碼將執行下述序列:
● 執行System. Object. Object()構造函數。
● 執行MyBaseClass. MyBaseClass(int i)構造函數。
● 執行MyDerivedClass. MyDerivedClass()構造函數。
除了base關鍵字外,這里還可以使用另一個關鍵字this。這個關鍵字指定在調用指定的構造函數前,.NET實例化過程對當前類使用非默認的構造函數。例如:
public class MyDerivedClass : MyBaseClass
{
public MyDerivedClass() : this(5, 6)
{
}
…
public MyDerivedClass(int i, int j) : base(i)
{
}
}
這段代碼將執行下述序列:
● 執行System. Object. Object()構造函數。
● 執行MyBaseClass. MyBaseClass(int i)構造函數。
● 執行MyDerivedClass. MyDerivedClass(int i, int j)構造函數。
● 執行MyDerivedClass. MyDerivedClass()構造的數。
惟一的限制是使用this或base關鍵字只能指定一個構造函數。
3.析構函數
析構函數使用略微不同的語法來聲明。在.NET中使用的析構函數(由System. Object類提供)叫作Finalize(),但這不是我們用於聲明析構函數的名稱。使用下面的代碼,而不是重寫Finalize():
class MyClass
{
~MyClass()
{
//destructor code
}
}
因此類的析構函數是用類名和前綴~來聲明的。當進行無用存儲單元收集時,就執行析構函數中的代碼,釋放資源。在調用這個析構函數后,還將隱式地調用基類的析構函數,包括System. Object根類中的Finalize()調用。
3.2.4 接口和抽象類
本章介紹了如何創建接口和抽象類。這兩種類型在許多方向都很類似,所以應看看它們的相似和不同之處,看看哪些情況應使用什么技術。
首先討論它們的類似之處。抽象類和接口都包含可以由派生類繼承的成員。接口和抽象類都不能直接實例化,但可以聲明它們的變量。如果這樣做,就可以使用多態性把繼承這兩種類型的對象指定給它們的變量。接着通過這些變量來使用這些類型的成員,但不能直接訪問派生對象的其他成員。
下面看看它們的區別。派生類只能繼承一個基類,即只能直接繼承一個抽象類(但可以用一個繼承鏈包含多個抽象類)。相反,類可以使用任意多個接口。但這不會產生太大的區別——這兩種情況得到的效果是類似的。只是采用接口的方式略有不同。
抽象類可以擁有抽象成員(沒有代碼體,旦必須在派生類中執行,否則派生類木身必須也是抽象的)和非抽象成員(它們擁有代碼體,也可以是虛擬的,這樣就可以在派生類中重寫)。另一方面,接口成員必須都在使用接口的類上執行——它們沒有代碼體。另外,接口成員被定義為公共的(因為它們傾向於在外部使用),但抽象類的成員也可以是私有的(只要它們不是抽象的)、受保護的、內部的或受保護的內部成員(其中受保護的內部成員只能在應用程序的代碼或派生類中訪問)。此外,接口不能包含字段、構造函數、析構函數、靜態成員或常量。
這說明這兩種類型用於完全不同的目的。抽象類主要用作對象系列的基類,共享某些主要特性,例如共同的目的和結構。接口則主要由類來使用,其個這些類在基礎水平上有所不同,但仍可以完成某些相同的任務。
例如,假定有一個對象系列表示火車,基類Train包含火車的核心定義,例如車輪的規格和引擎的類型(可以是蒸汽發動機、柴油發動機等)。但這個類是抽象的,因為並沒有“一般的”火車。為了創建一輛實際的火車,需要給該火車添加特性。為此,派生一些類,例如:Passenger Train,FreightTrain等。
汽車對象系列也可以用相同的方式來定義,使用Car抽象基類,其派生類有Compact,SUV和PickUp。Car和Train可以派生於一個相同的基類Vehicle。
現在,層次結構中的一些類共享相同的特性,這是因為它們的目的是相同的,而不是因為它們派生於相同的基類。例如,PassengerTrain,Compact,SUV和PickUp都可以運送乘客,所以它們都擁有IpassengerCarrier接口,FreightTrain和PickUp可以運送貨物,所以它們都擁有IHeavyLoadCarrier接口。
在進行更詳細的分工前,把對象系統以這種方式進行分解,可以清楚地看到哪種情形適合使用抽象類,哪種情形適合使用接口。只使用接口或只使用抽象繼承,就得不到這個范例的結果。
3.2.5 類和結構
在許多方面,可以把C#中的結構看作是縮小的類。它們基本上與類相同,但更適合於把一些數據組合起來的場合。它們與類的區別在於:
● 結構是值類型,不是引用類型。它們存儲在堆棧中或存儲為內聯(inline)(如果它們是另一個對象的一部分,就會保存在堆中),其生存期的限制與簡單的數據類型一樣。
● 結構不支持繼承。
● 結構的構造函數的工作方式有一些區別。尤其是編譯器總是提供一個無參數的默認構造函數,這是不允許替換的。
● 使用結構,可以指定字段如何在內存中布局。
下面將詳細說明類和結構之間的區別。
1.結構是值類型
雖然結構是值類型,但在語法上常常可以把它們當作類來處理。例如,在上面的Dimensions類的定義中,可以編寫下面的代碼:
struct Dimensions
{
public double Length;
public double Width;
}
Dimensions point = new Dimensions();
point.Length = 3;
point.Width = 6;
注意,因為結構是值類型,所以new運算符與類和其他引用類型的工作方式不同。new運算符並不分配堆中的內存,而是調用相應的構造函數,根據傳送給它的參數,初始化所有的字段。對於結構,可以編寫下述代碼:
Dimensions point;
point.Length = 3;
point.Width = 6;
如果Dimensions是一個類,就會產生一個編譯錯誤,因為point包含一個未初始化的引用——不指向任何地方的一個地址,所以不能給其字段設置值。但對於結構,變量聲明實際上是為整個結構分配堆棧中的空間,所以就可以賦值了。
結構遵循其他數據類型都遵循的規則:在使用前所有的元素都必須進行初始化。在結構上調用new運算符,或者給所有的字段分別賦值,結構就可以完全初始化了。當然,如果結構定義為類的成員字段,在初始化包含對象時,該結構會自動初始化為0。
結構是值類型,所以會影響性能,但根據使用結構的方式,這種影響可能是正面的,也可能是負面的。正面的影響是為結構分配內存時,速度非常快,因為它們將內聯或者保存在堆棧中。在結構超出了作用域被刪除時,速度也很快。另一方面,只要把結構作為參數來傳遞或者把一個結構賦給另一個結構(例如A=B,其中A和B是結構),結構的所有內容就被復制,而對於類,則只復制引用。這樣,就會有性能損失,根據結構的大小,性能損失也不同。注意,結構主要用於小的數據結構。但當把結構作為參數傳遞給方法時,就應把它作為ref參數傳遞,以避免性能損失——此時只傳遞了結構在內存中的地址,這樣傳遞速度就與在類中的傳遞速度一樣快了。另一方面,如果這樣做,就必須注意被調用的方法可以改變結構的值。
2.結構和繼承
不能從一個結構中繼承,惟一的例外是結構(和C#中的其他類型一樣)派生於類System.Object。因此,結構也可以訪問System.Object的方法。在結構中,甚至可以重寫System.Object中的方法—— 例如重寫ToString()方法。結構的繼承鏈是:每個結構派生於System.ValueType,System.ValueType派生於System.Object。ValueType並沒有給Object添加任何新成員,但提供了一些更適合結構的執行代碼。注意,不能為結構提供其他基類:每個結構都派生於ValueType。
3.結構的構造函數
為結構定義構造函數的方式與為類定義構造函數的方式相同,但不允許定義無參數的構造函數。例如:
struct Dimensions
{
public double Length;
public double Width;
Dimensions(double length, double width)
{
Length= length;
Width= width;
}
}
前面說過,默認構造函數把所有的字段都初始化為0,且總是隱式地給出,即使提供了其他帶參數的構造函數,也是如此。也不能提供字段的初始值,以此繞過默認構造函數。下面的代碼會產生編譯錯誤:
struct Dimensions
{
public double Length = 1; // error. Initial values not allowed
public double Width = 2; // error. Initial values not allowed
}
當然,如果Dimensions聲明為一個類,這段代碼就不會有編譯錯誤。
3.3 定義類成員
本節繼續討論在C#中如何定義類,主要介紹的是如何定義字段、屬性和方法等類成員。 首先介紹每種類型需要的代碼,然后將討論—些比較高級的成員技術:隱藏基類成員、調用重寫的基類成員。
3.3.1 成員定義
在類定義中,也提供了該類中所有成員的定義,也括字段、方法和屬性。所有成員都有自己的訪問級別,用下面的關鍵字之—來定義:
● public——成員可以由任何代碼訪問。
● private——成員只能由類中的代碼訪問(如果沒有使用任何關鍵字,就默認使用這個關鍵字)。
● internal——成員只能由定義它的工程(程序集)內部的代碼訪問。
● proteded——成員只能由類或派生類中的代碼訪問。
最后兩個關鍵字可以合並使用,所以也有protected internal成員。它們只能由工程(程序集)中派生類的代碼來訪問。
字段、方法和屬性都可以使用關鍵字static來聲明,這表示它們是用於類的靜態成員,而不是對象實例的成員。
1.定義字段
字段用標准的變量聲明格式和前面介紹的修飾符來聲明(可以進行初始化),例如:
class MyClass
{
public int MyInt;
}
字段也可以使用關鍵字readonly,表示這個字段只能在執行構造函數的過程中賦值,或由初始化賦值語句賦值。例如:
class MyClass
{
public readonly int MyInt = 17;
}
字段可以使用static關鍵字聲明為靜態,例如:
class MyClass
{
public static int MyInt;
}
靜態字段可以通過定義它們的類來訪問(在上面的例子中,是MyClass.MyInt),而不是通過這個類的對象實例來訪問。
另外,可以使用關鍵字const來創建一個常量。按照定義,const成員也是靜態的,所以不需要用static修飾。
2.定義方法
方法使用標准函數格式,以及可訪問性和可選的static修飾符來聲明。例如:
class MyClass
{
public string GetString()
{
return “Here is a string.”;
}
}
注意,如果使用了static關鍵字,這個方法就只能通過類來訪問,不能通過對象實例來訪問。
也可以在方法定義中使用下述關鍵字:
● virtual——方法可以重寫。
● abstract——方法必須重寫(只用於抽象類中)。
● override——方法重寫了一個基類方法(如果方法被重寫,就必須使用該關鍵字)。
● extern——方法定義放在其他地方。
下面的代碼是方法重寫的一個例子:
public class MyBaseClass
{
public virtual void DoSomething()
{
//Base implementation
}
}
public class MyDerivedClass : MyBaseClass
{
public override void DoSomething()
{
//Derived class implementation, override base implementation
}
}
如果使用了override,也可以使用sealed指定在派生類中不能對這個方法作進一步的修改,即這個方法不能由派生類重寫。例如:
public class MyDerivedClass : MyBaseClass
{
public override sealed void DoSomething()
{
//Derived class implementation, override base implementation
}
}
使用extern可以提供方法在工程外部使用的實現。
3.定義屬性
屬性定義的方式與字段定義的方式類似,但包含的內容比較多。這是因為它們在修改狀態前還至執行額外的操作。屬性擁有兩個類似函數的塊,一個塊用於獲取屬性的值,另一個塊用於設置屬性的值。
這兩個塊分別用get和set關鍵字來定義,可以用於控制對屬性的訪問級別。可以忽略其中的一個塊來創建只讀或只寫屬性(忽略get塊創建只寫屬性,忽略set塊創建只讀屬性)。當然,這僅適用於外部代碼,因為類中的代碼可以訪問這些塊能訪問的數據。屬性至少要包含一個塊,才是有效的(既不能讀取也不能修改的屬性沒有任何用處)。
屬性的基本結構包括標准訪問修改關鍵字(public,private等)、后跟類名、屬性名和get塊(或set塊,或者get塊和set塊,其中包含屬性處理代碼),例如:
public string SomeProperty
{
get
{
return "This is the property value";
}
set
{
// do whatever needs to be done to set the property
}
}
定義代碼中的第一行非常類似於定義域的代碼。區別是行末沒有分號,而是一個包含嵌套get和set塊的代碼塊。
get塊不帶參數,且必須返回屬性聲明的類型。簡單的屬性一般與一個私有字段相關聯,以控制對這個字段的訪問,此時get塊可以直接返回該字段的值,例如:
//Field used by property
private int myInt;
//Property
public int MyIntProp
{
get
{
return myInt;
}
set
{
//Property set code
}
}
注意類外部的代碼不能直接訪問這個myInt字段,因為其訪問級別是私有的。必須使用屬性來訪問該字段。
也不應為set代碼塊指定任何顯式參數,但編譯器假定它帶一個參數,其類型也與屬性相同,並表示為value。set函數以類似的方式把一個值賦給字段:
//Field used by property
private int myInt;
//Property
public int MyIntProp
{
get
{
return myInt;
}
set
{
myInt = value;
}
}
value等於類型與屬性相同的一個值,所以如果字段使用相同的類型,就不必進行數據類型轉換了。
這個簡單的屬性只是直接訪問myInt字段。在對操作進行更多的控制時,屬性的真正作用才能發揮出來。例如,下面的代碼包含一個屬性ForeName,它設置了一個字段foreName,該字段有一個長度限制。
private string foreName;
public string ForeName
{
get
{
return foreName;
}
set
{
if (value.Length > 20)
// code here to take error recovery action
// (eg. throw an exception)
else
foreName = value;
}
}
如果賦給屬性的字符串長度大於20,就修改foreName。使用了無效的值,該怎么辦?有4種選擇:
● 什么也個做。
● 給字段賦默認值。
● 繼續執行,就好像沒有發生錯誤—樣,但記錄下該事件以備將來分析。
● 拋出一個異常。
一般情況下,最后兩個選擇比較好,使用哪個選擇取決於如何使用類,以及給類的用戶授予多少控制權。拋出異常給用戶提供的控制權比較大,可以讓他們知道發生了什么情況,並作出合適的響應。關於異常詳見下一節。
記錄數據,例如記錄到文本文件學,對產品代碼會比較有效,因為產品代碼不應發生錯誤。它們允許開發人員檢查性能,如果需要,還可以調試現有的代碼。
屬性可以使用virtual,override和abstract關鍵字,就像方法—樣,但這幾個關鍵字不能全部用於字段。
3.3.2 類成員的其他議題
前面討論了成員定義的基本知識,下面討論一些比較高級的成員議題,包括:隱藏基類方法和調用重寫或隱藏的基類方法。
1.隱藏基類方法
當從基類繼承一個(非抽象的)成員時,也就繼承了其實現代碼。如果繼承的成員是虛擬的,就可以用override關鍵字重寫這段執行代碼。無論繼承的成員是否為虛擬,都可以隱藏這些執
行代碼。
使用下面的代碼就可以隱藏:
public class MyBaseClass
{
public void DoSomething()
{
//Base implementation
}
}
public class MyDerivedClass : MyBaseClass
{
public void DoSomething()
{
//Derived class implementation, hides base implementation
}
}
盡管這段代碼正常運行,但它會產生一個警告,說明隱藏了一個基類成員。如果是偶然地隱藏了一個需要使用的成員,此時就可以改正錯誤。如果確實要隱藏該成員,就可以使用new關鍵字顯式地說明,這是我們要隱藏的成員:
public class MyDerivedClass : MyBaseClass
{
new public void DoSomething()
{
//Derived class implementation, hides base implementation
}
}
此時應注意隱藏基類成員和重寫它們的區別。考慮下面的代碼:
public class MyBaseClass
{
public virtual void DoSomething()
{
Console.WriteLine(“Base imp”);
}
}
public class MyDerivedClass : MyBaseClass
{
public override void DoSomething()
{
Console.WriteLine(“Derived imp”);
}
}
其中重寫方法將替換基類中的執行代碼,這樣下面的代碼就將使用新版本,即使這是通過基類進行的,情況也是這樣:
MyDerivedClass myObj = new MyDerivedClass();
MyBaseClass myBaseObj;
myBaseObj = myObj;
myBaseObj.DoSomething();
結果如下:
Derivd imp
另外,還可以使用下面的代碼隱藏基類方法:
public class MyBaseClass
{
public virtual void DoSomething()
{
Console.WriteLine(“Base imp”);
}
}
public class MyDerivedClass : MyBaseClass
{
new public void DoSomething()
{
Console.WriteLine(“Derived imp”);
}
}
基類方法不必是虛擬的,但結果是一樣的,對於基類的虛擬方法和非虛擬方法來說,其結果如下:
Base imp
盡管隱藏了基類的執行代碼,但仍可以通過基類訪問它。
2.調用重寫或隱藏的基類方法
無論是重寫成員還是隱藏成員,都可以在類的內部訪問基類成員。這在許多情況下都是很有用的,例如:
● 要對派生類的用戶隱藏繼承的公共成員,但仍能在類中訪問其功能。
● 要給繼承的虛擬成員添加執行代碼,而不是簡單地用新的重寫的執行代碼替換它。
為此,可以使用base關鍵字,它表示包含在派生類中的基類的執行代碼(在控制構造函數時,其用法是類似的,見上節),例如:
class CustomerAccount
{
public virtual decimal CalculatePrice()
{
// implementation
return 0.0M;
}
}
class GoldAccount : CustomerAccount
{
public override decimal CalculatePrice()
{
return base.CalculatePrice() * 0.9M;
}
}
base使用對象實例,所以在靜態成員中使用它會產生錯誤。
除了使用base關鍵字外,還可以使用this關鍵字。與base—樣,thts也可以用在類成員的內部,且該關鍵字也引用對象實例。由this引用的對象實例是當前的對象實例(即不能在靜態成員中使用this關鍵字,因為靜態成員不是對象實例的一部分)。
this關鍵字最常用的功能是把一個當前對象實例的引用傳遞給一個方法,例如:
public void doSomething()
{
MyTargetClass myObj = new MyTargetClass();
myObj.DoSomethingWith(this);
}
其中,實例化的MyTargetClass的有一個方法DoSomethingWith(),該方法帶有一個參數,其類型與包含上述方法的類兼容。
3.3.3接口的實現
1.接口的定義
上一節中介紹了接口定義的方式與類相似,接口成員的定義與類成員的定義也相似,但有幾個重要的區別:
● 不允許使用訪問修飾符(public,private,protected或internal),所有的接口成員都是公共的
●接口成員不能包含代碼體。
● 接口不能定義字段成員。
● 接口成員不能用關鍵字static,virtual,abstract或sealed來定義。
但要隱藏繼承了基接口的成員,可以用關鍵字new來定義它們,例如:
interface IMyBaseInterface
{
void DoSomething();
}
interface ImyDerivedInterface : IMyBaseInterface
{
new void DoSomething();
}
其執行方式與隱藏繼承的類成員一樣。
在接口中定義的屬性可以確定訪問塊get和set中的哪一個能用於該屬性,例如:
interface IMyInterface
{
int MyInt
{
get;
set;
}
}
其中int屬性MyInt有get和set訪問程序塊。對於訪問級別有更嚴限制的屬性來說,可以省略它們中的任一個。但要注意,接口沒有指定屬性應如何存儲。接口不能指定字段,例如用於存儲屬性數據的字段。
2.在類中實現接口
執行接口的類必須包含該接口所有成員的執行代碼,且必須匹配指定的簽名(包括匹配指定的get和set塊),並且必須是公共的。可以使用關鍵字virtual或abstract來執行接口成員,但不能使用static或const,例如:
public interface IMyInterface
{
void DoSomething();
void DoSomethingElse();
}
public class MyClass : IMyInterface
{
public void DoSomething()
{
}
public void DoSomethingElse()
{
}
}
繼承一個實現給定接口的基類,就意味着派生類隱式地支持這個接口,例如:
public interface IMyInterface
{
void DoSomething();
void DoSomethingElse();
}
public class MyBaseClass : IMyInterface
{
public virtual void DoSomething()
{
}
public virtual void DoSomethingElse()
{
}
}
public class MyDerivedClass : MyBaseClass
{
public override void DoSomething()
{
}
}
如上所示,在基類中把執行代碼定義為虛擬,派生類就可以替換該執行代碼,而個是隱藏它們。如果要使用new關鍵字隱藏一個基類成員,而不是重寫宅,則方法IMyInterface.DoSomething()就總是引用基類版本,即使派生類通過這個接口來訪問也是這樣。
下面的例子用來說明如何定義和使用接口。這個例子建立在銀行賬戶的基礎上。假定編寫代碼,最終允許在銀行賬戶之間進行計算機轉賬業務。許多公司可以實現銀行賬戶,但它們都是彼此贊同表示銀行賬戶的所有類都實現接口IBankAccount。該接口包含一個用於存取款的方法和一個返回余額的屬性。這個接口還允許外部代碼識別由不同銀行賬戶執行的各種銀行賬戶類。
【例3-3】
public interface IBankAccount
{
void PayIn(decimal amount);
bool Withdraw(decimal amount);
decimal Balance
{
get;
}
}
public class SaverAccount : IBankAccount
{
private decimal balance;
public void PayIn(decimal amount)
{
balance += amount;
}
public bool Withdraw(decimal amount)
{
if (balance >= amount)
{
balance -= amount;
return true;
}
Console.WriteLine("Withdrawal attempt failed.");
return false;
}
public decimal Balance
{
get
{
return balance;
}
}
public override string ToString()
{
return String.Format("Venus Bank Saver: Balance = {0,6:C}", balance);
}
}
class MainEntryPoint
{
static void Main()
{
IBankAccount venusAccount = new SaverAccount();
venusAccount.PayIn(200);
venusAccount.Withdraw(100);
Console.WriteLine(venusAccount.ToString());
}
}
首先,需要定義接口的名稱為IbankAccount,然后編寫表示銀行賬戶的類SaverAccount,其中包含一個私有字段balance,當存款或取款時就調整這個字段。如果因為賬戶中的金額不足而取款失敗,就會顯示一個錯誤消息。SaverAccount派生於一個接口IbankAccount,表示它獲得了IBankAccount的所有成員,但接口並不實際實現其方法,所以SaverAccount必須提供這些方法的所有實現代碼。如果沒有提供實現代碼,編譯器就會產生錯誤。接口僅表示其成員的存在性,類負責確定這些成員是虛擬還是抽象的(但只有在類本身是抽象的,這些成員才能是抽象的)。在本例中,接口方法不必是虛擬的。有了自己的類后,就可以測試它們了。執行結果如下:
Venus Bank Saver: Balance = £100.00
Withdrawal attempt failed.
3.4類的更多內容
3.4.1 集合
第2章介紹了如何使用數組創建包含許多對象或值的變量類型。但數組有一定的限制。最大的限制是一旦創建好數組,它們的大小就是固定的,不能在現有數組的末尾添加新項目,除非創建一個新的數組。這常常意味着用於處理數組的語法比較復雜。
C#中的數組是作為System.Array類的實例來執行的,它們只是集合類中的一種。集合類一般用於處理對象列表,其功能比簡單數組要多,這些功能是通過執行System.Collections命名空間中的接口而實現的。
集合的功能可以通過接口來實現,該接口不僅沒有限制我們使用基本集合類,例如System.Array。相反我們還可以創建自己的定制集合類。這些集合可以專用於要枚舉的對象。這么做的一個優點是定制的集合類可以是強類型化的。也就是說,在從集合中提取項目時,不需要把它們轉換為正確的類型。
在System.Collections名稱空間中有許多接口都提供了基本的集合功能:
● IEnumerable提供了循環集合中項目的功能。
● ICollection(繼承於IEnumerable)可以獲取集合中項目的個數,並能把項目復制到一個簡單的數組類型中。
● IList(繼承於IEnumerable和ICollection)提供了集合的項目列表,並可以訪問這些項目,以及其他一些與項目列表相關的功能。
● IDictionary(繼承於IEnumerable和ICollection)類似於IList,但提供了可通過鍵碼值而不是索引訪問的項目列表。
System.Array類繼承了IList,ICollection和IEnumerable,但不支持IList的一些更高級的功能,它表示大小固定的一個項目列表。
1.定義集合
如何創建自己的、強類型化的集合?一種方式是手動執行需要的方法,但這比較花時間,在某些情況下也非常復雜。我們還可以從一個類派生自己的集合,例如System.Collections.CollectionBase類,這個抽象類提供了集合類的許多執行方式。
CollectionBase類有接口IEnumerable,ICollection和IList,但只提供了一些要求的執行代碼,特別是IList的Clear()和RemoveAt()方法,以及IcoIIection的Count屬性。如果要使用提供的功能,就需要自己執行其他代碼。
為了方便地完成任務,CollectionBase提供了兩個受保護的屬性,它們可以訪問存儲的對象本身。我們可以使用List和InnerList,其中List可以通過IList接口訪問項目,InnerList則是用於存儲項目的ArrayList對象。
2.索引符
索引符是一種特殊類型的屬性,可以把它添加到一個類中,以提供類似於數組的訪問。實際上,可以通過一個索引符提供更復雜的訪問,岡為我們可以定義和使用復雜的參數類型和方括號語法。它最常見的一個用法是對項目執行一個簡單的數字索引。
3.關鍵字值集合和IDictionary
除了Ilist接口外,集合還可以執行類似的IDictionary接口,允許項目通過一個關鍵字值(例
如字符串名)進行索引,而不是通過一個索引。
這也可以使用索引符來完成,但這次的索引符參數是與存儲的項日相關聯的一個關鍵字,而不是一個int索引,這樣集合的用戶友好性就更高了。
與索引的集合一樣,我們可以使用一個基類簡化IDictionary接口的實現,這個基類就是DictionaryBase,它也實現IEnumerable和ICollection接口,提供了對任何集合都相同的集合處理功能。
DictionaryBase與CollectionBase—樣,實現通過其支持的接口獲得的一些成員(但不是全部成員)。DictionaryBase也執行Clear()和Count,但不執行RemoveAt()。這是的為RemoveAt()是IList接口上的一個方法,不是IDictionary接口上的一個方法。但是,Dictionary有一個Remove()方法,這是一個應執行基於DictionaryBase的定制集合類的方法。
【例3-4】用集合類移動顯示產品信息。
using System;
using System.Collections;
using System.Text;
namespace Exp3_4
{
class Program
{
class Products
{
public string ProductName;
public double UnitPrice;
public int UnitsInStock;
public int UnitsOnOrder;
// 帶參構造器
public Products(string ProductName, double UnitPrice, int UnitsInStock, int UnitsOnOrder)
{
this.ProductName = ProductName;
this.UnitPrice = UnitPrice;
this.UnitsInStock = UnitsInStock;
this.UnitsOnOrder = UnitsOnOrder;
}
}
// 實現接口Ienumerator和IEnumerable類Iterator
public class ProductsIterator:IEnumerator,IEnumerable
{
// 初如化Products 類型的集合
private Products[] ProductsArray;
int Index;
public ProductsIterator()
{
// 使用帶參構造器賦值
ProductsArray = new Products[4];
ProductsArray[0] = new Products("Maxilaku", 20.00, 10, 60);
ProductsArray[1] = new Products("Ipoh Coffee", 46.00, 17, 10);
ProductsArray[2] = new Products("Chocolade", 12.75, 15, 70);
ProductsArray[3] = new Products("Pavlova", 17.45, 29, 0);
Index = -1;
}
// 實現IEnumerator的Reset()方法
public void Reset()
{
Index = -1;
}
// 實現IEnumerator的MoveNext()方法
public bool MoveNext()
{
return (++Index < ProductsArray.Length);
}
// 實現IEnumerator的Current屬性
public object Current
{
get
{
return ProductsArray[Index];
}
}
// 實現IEnumerable的GetEnumerator()方法
public IEnumerator GetEnumerator()
{
return (IEnumerator)this;
}
static void Main()
{
ProductsIterator ProductsIt = new ProductsIterator();
Products Product;
ProductsIt.Reset();
for (int i = 0; i < ProductsIt.ProductsArray.Length; i++)
{
ProductsIt.MoveNext();
Product = (Products)ProductsIt.Current;
Console.WriteLine("ProductName : " + Product.ProductName.ToString());
Console.WriteLine("UnitPrice : " + Product.UnitPrice.ToString());
Console.WriteLine("UnitsInStock : " + Product.UnitsInStock.ToString());
Console.WriteLine("UnitsOnOrder : " + Product.UnitsOnOrder.ToString());
}
Console.ReadLine();
}
}
}
}
3.4.2 運算符重載
可以通過我們設計的類使用標准的運算符,例如+,>等,這稱為重載,因為在使用特定的參數類型時,我們為這些運算符提供了自己的執行代碼,其方式與重載方法相同,方法的重載是為同名的方法提供不同的參數。
運算符重載非常有用,因為我們可以在運算符重載中執行需要的任何操作,在類實例上不能總是只調用方法或屬性,有時還需要做一些其他的工作,例如對數值進行相加、相乘或邏輯操作,如比較對象等。假定要定義一個類,表示一個數學矩陣,在數學中,矩陣可以相加和相乘,就像數字一樣。這並不像“把這兩個操作數相加”這么簡單。
1.運算符重載的基本語法
要重載運算符,可給類添加運算符類型成員(它們必須是static)。一些運算符有多種用途,(例如 - 運算符就有一元和二元兩種功能),因此我們還指定了要處理多少個操作數,以及這些操作數的類型。一般情況下,操作數的類型與定義運算符的類類型相同,但也可以定義處理混合類型的運算符,詳見后面的內容。
例如,考慮一個簡單的類AddClass1,如下所示:
public class AddClass1
{
public int val;
}
這僅是int值的一個包裝器,對於這個類,下面的代碼不能編譯:
AddClass1 op1 = new AddClass1();
op1.val = 5;
AddClass1 op2 = new AddClass1();
op2.val = 5;
AddClass1 op3 = op1 + op2;
其錯誤是+運算符不能應用於AddClass1類型的操作數,下面的代碼則可執行,但得不到希望的結果:
AddClass1 op1 = new AddClass1();
op1.val = 5;
AddClass1 op2 = new AddClass1();
op2.val = 5;
bool op3 = op1 == op2;
其中,使用==二元運算符來比較op1和op2,看看是否引用的是同一個對象,而不是驗證它們的值是否相等。在上述代碼中,即使op1.val和op2.val相等,op3也是false。要重載+運算符,可使用下述代碼:
public class AddClass1
{
public int val;
public static AddClass1 operator +(AddClass1 op1, AddClass1 op2)
{
AddClass1 returnVal = new AddClass1();
returnVal.val = op1.val + op2.val;
return returnVal;
}
}
可以看出,運算符重載看起來與標准靜態方法聲明類似,但它們使用關鍵字operatoe和運算符本身,而不是一個方法名。
現在可以成功地使用+運算符和這個類,如上面的例子所示:
AddClass1 op3=opl + op2;
重載所有的二元運算符都是一樣的,一元運算符看起來也是類似的,但只有一個參數:
public class AddClass1
{
public int val;
public static AddClass1 operator +(AddClass1 op1, AddClass1 op2)
{
AddClass1 returnVal = new AddClass1();
returnVal.val = op1.val + op2.val;
return returnVal;
}
public static AddClass1 operator –(AddClass1 op1)
{
AddClass1 returnVal = new AddClass1();
returnVal.val = -op1.val;
return returnVal;
}
}
這兩個運算符處理的操作數的類型與類相同,返回值也是該類型,但考慮下面的類定義:
public class AddClass2
{
public int val;
public static AddClass3 operator +(AddClass1 op1, AddClass2 op2)
{
AddClass3 returnVal = new AddClass3();
returnVal.val = op1.val + op2.val;
return returnVal;
}
}
public class AddClass2
{
public int val;
}
public class AddClass3
{
public int val;
}
下面的代碼就可以執行:
AddClass1 op1 = new AddClass1();
op1.val = 5;
AddClass2 op2 = new AddClass2();
op2.val = 5;
AddClass3 op3 = op1 + op2;
在合適時,可以用這種方式混合類型。但要注意,如果把相同的運算符添加到AddClass2中,上面的代碼就會失敗,因為它將不知道要使用哪個運算符。因此應注意不要把簽名相同的運算符添加到多個類中。
還要注意,如果混合了類型,操作數的順序必須與運算符重載的參數的順序相同。用了重載的運算符和順序錯誤的操作數,操作就會失敗。所以不能像下面這樣使用運算符:
AddClass3 op3=op2 + op1;
當然,除非提供了另一個重載運算符和倒序的參數:
public static AddClass3 operator +(AddClass2 op1, AddClass1 op2)
{
AddClass3 returnVal = new AddClass3();
returnVal.val = op1.val + op2.val;
return returnVal;
}
下述運算符可以重載:
● 一元運算符:+,-,!,~,++,--,true,false
● 二元運算符:+,-,*,/,%,&,|,^,<<,>>
● 比較運算符:==,!=,<,>,<=,>=
注意:如果重載true和false運算符,就可以在布爾表達式中使用類,例如if(opl){}。
不能重載賦值運算符,例如+=,但這些運算符使用它們的對應運算符的重載形式,例如+。也不能重載&&和||,但它們可以在計算中使用對應的運算符&和|。
一些運算符如<和>必須成對重載。這就是說,不能重載<,除非也重載了>。在許多情況下,可以在這些運算符中調用其他運算符,以減少需要的代碼(和可能發生的錯誤),例如:
public c1ass AddClass1
{
public int val;
public static bool operator >=(AddClass1 op1, AddClass1 op2)
{
return(op1.val >= op2.val);
}
public static bool operator <(AddClass1 op1, AddClass1 op2)
{
return !(op1>=op2);
}
//Also need implementations for <= and > operators
}
在比較復雜的運算符定義中,這可以減少代碼,且只要修改一個執行代碼,其他運算符也會被修改。
這同樣適用於==和!=,但對於這些運算符,常常需要重寫Object.Equals()和Object.GetHashCode(),因為這兩個函數也可以用於比較對象。重寫這些方法,可以確保無論類的用戶使用什么技術,都能得到相同的結果。這不太重要,但應增加進來,以保證其完整性。它需要下述非靜態重寫方法:
public class AddClass1
{
public int val;
public static bool operator ==(AddClass1 op1, AddClass1 op2)
{
return (op1.val == op2.val);
}
public static bool operator !=(AddClass1 op1, AddClass1 op2)
{
return !(op1 == op2);
}
public override bool Equals(object op1)
{
return val == ((AddClass1)op1).val;
}
public override int GetHashCode()
{
return val;
}
}
注意Equals()使用Object類型參數。我們需要使用這個簽名,否則就將重載這個方法,而不是重寫它。類的用戶仍可以訪問默認的執行代碼。這樣就必須使用數據類型轉換得到需要的結果。GetHashCode()可根據其狀態,獲取對象實例的—個惟一的int值。這里使用vaI就可以了,因為它也是個int值。
2.轉換運算符
除了重載如上所述的數學運算符之外,還可以定義類型之間的隱式和顯式轉換。如果要在不相關的類型之間轉換,這是必須的,例如,如果在類型之間沒有繼承關系,也沒有共享接口,這就是必須的。
下面定義convclassl和convclass2之間的隱式轉換,即編寫下述代碼:
ConvClass1 op1 = new ConvClass1();
ConvClass2 op2 = op1;
另外,還可以定義—個顯式轉換,在下面的代碼中調用:
ConvClass1 opl = new ConvClass1();
ConvClass2 op2 = (ConvClass2)opl;
例如,考慮下面的代碼;
public class ConvClass1
{
public int val;
public static implicit operator ConvClass2(ConvClass1 op1)
{
ConvClass2 returnVal = new ConvClass2();
returnVal.val = op1.val;
return returnVal;
}
}
public class ConvClass2
{
public double val;
public static explicit operator ConvClass1(ConvClass2 op1)
{
ConvClass1 returnVal = new ConvClass1();
checked{returnVal.val = (int)op1.Val;};
return returnVal;
}
}
其中,ConvClass1包含一個int值,ConvClass2包含—個double值。因為int值可以隱式轉換為double值,所以可以在ConvClassl和convClass2之間定義一個隱式轉換。但反過來就不行了,應把convClass1和ConvClass2之間的轉換定義為顯式。在代碼中,用關鍵字implicit和explicit來指定這些轉換,如上所示。
對於這些類,下面的代碼就很好;
ConvClass1 op1 = new ConvClass1();
op1.val = 3;
ConvClass2 op2 = op1;
但反方向的轉換需要下述顯式數據類型轉換:
ConvClass2 op1 = new ConvClass2();
op1.val = 3.15
ConvClass1 op2 = (ConvClass1)op1;
3.4.3 高級轉換
1.封箱和拆箱
上一節中討論了引用和值類型之間的區別,並通過比較結構(值類型)和類(引用類型)進行了說明。封箱(boxing)是把值類型轉換為System.Object類型,或者轉換為由值類型執行的接口類型。拆箱(unboxing)是相反的轉換過程。
例如,下面的結構類型:
struct MyStruct
{
public int Val;
}
可以把這種類型的結構放在object類型的變量中,以封箱它:
MyStruct valType1 = new MyStruct();
valType1.Val = 5;
object refType = valType1;
其中創建了一個類型為MyStruct的新變量(valTypel),並把一個值賦予這個結構的Val成員,然后把它封箱在object類型的變量(refType)中。
以這種方式封箱變量而創建的對象,包含值類型變量的一個副本的引用,而不過含源值類型變量的引用。如果要進行驗證,可以修改源結構的內容,把對象中包含的結構拆箱到新變量中,檢查其內容:
valType1.Val = 6;
MyStruct valType2 = (MyStruct)refType;
Console.WriteLine(“valType2.Val = {0}”,valType2.Val);
這段代碼將得到如下結果:
valType2.Val = 5
但在把一個引用類型賦予對象時,將執行不同的操作。通過把MyStruct轉化為—個類(不考慮這個類名不再合適的情況):
class MyStruct
{
public int Val;
}
如果不修改上面的客戶代碼,就會得到如下結果
valType.Val = 6
也可以把值類型封箱到一個接口中,只要它們執行這個接口即可。例如,假設MyStruct類實現IMyInterface接口,如下所示:
interface IMyInterface
{
}
struct MyStruct : IMyInterface
{
public int Val;
}
接着把結構封箱到一個IMyInterface類型中,如下所示:
MyStruct valType1 = new MyStruct();
IMyInterface refType = valType1;
然后使用一般的數據類型轉換語法拆箱它:
MyStruct ValType2 = (MyStruct)refType;
從這些范例中可以看出,封箱是在沒有用戶干涉的情況下進行的(即不需要編寫任何代碼),但拆箱一個值需要進行顯式轉換,即需要進行數據類型轉換(封箱是隱式的,所以不需要進行數據類型轉換)。
封箱非常有用,有兩個非常重要的原因。首先,它允許使用集合中的值類型(例如ArrayList),集合中項目的類型是object。其次,有一個內部機制允許在值類型上調用object,例如int和結構。最后要注意的是,在訪問值類型的內容前,必須進行拆箱。
2.is運算符
is運算符可以檢查未知的變量(該變量能用作對象參數,傳送給一個方法)是否可為約定的類型,如果可以進行轉換,該值就是true。在對對象調用方法前,可以使用該運算符查看執行該方法的對象的類型。is運算符不會檢查兩個類型是否相同,但可以檢查它們是否兼容。
is運算符的語法如下:
<operand> is <type>
這個表達式的結果如下:
● 如果<type>是一個類類型,而<operand>也是該類型,或者它繼承了該類型,或者它封箱到該類型中,則結果為true。
● 如果<type>是一個接口類型,而<operand>也是該類型,或者它是實現該接口的類型,則結果為true。
● 如果<type>是一個值類型,而<operand>也是該類型,或者它被拆箱到該類型中,則結果為true。
下面的例子用於說明該運算符的使用
【例3-5】
using System;
class Checker
{
public void Check(object param1)
{
if(param1 is ClassA)
Console.WriteLine("Variable can be converted to ClassA.");
else
Console.WriteLine("Variable can't be converted to ClassA.");
if(param1 is IMyInterface)
Console.WriteLine("Variable can be converted to IMyInterface.");
else
Console.WriteLine("Variable can't be converted to IMyInterface.");
if(param1 is MyStruct)
Console.WriteLine("Variable can be converted to MyStruct.");
else
Console.WriteLine("Variable can't be converted to MyStruct.");
}
}
interface IMyInterface
{
}
class ClassA:IMyInterface
{
}
class ClassB:IMyInterface
{
}
class ClassC
{
}
class ClassD:ClassA
{
}
struct MyStruct:IMyInterface
{
}
class Class1
{
static void Main(string[] args)
{
Checker check = new Checker();
ClassA try1 = new ClassA();
ClassB try2 = new ClassB();
ClassC try3 = new ClassC();
ClassD try4 = new ClassD();
MyStruct try5 = new MyStruct();
object try6 = try5;
Console.WriteLine("Analyzing ClassA type variable:");
check.Check(try1);
Console.WriteLine("\nAnalyzing ClassB type variable:");
check.Check(try2);
Console.WriteLine("\nAnalyzing ClassC type variable:");
check.Check(try3);
Console.WriteLine("\nAnalyzing ClassD type variable:");
check.Check(try4);
Console.WriteLine("\nAnalyzing Mystruct type variable:");
check.Check(try5);
Console.WriteLine("\nAnalyzing boxed MyStruct type variable:");
check.Check(try6);
}
}
運行結果如下:
Analyzing ClassA type variable:
Variable can be converted to ClassA.
Variable can be converted to IMyInterface.
Variable can't be converted to MyStruct.
Analyzing ClassB type variable:
Variable can't be converted to ClassA.
Variable can be converted to IMyInterface.
Variable can't be converted to MyStruct.
Analyzing ClassC type variable:
Variable can't be converted to ClassA.
Variable can't be converted to IMyInterface.
Variable can't be converted to MyStruct.
Analyzing ClassD type variable:
Variable can be converted to ClassA.
Variable can be converted to IMyInterface.
Variable can't be converted to MyStruct.
Analyzing Mystruct type variable:
Variable can't be converted to ClassA.
Variable can be converted to IMyInterface.
Variable can be converted to MyStruct.
Analyzing boxed MyStruct type variable:
Variable can't be converted to ClassA.
Variable can be converted to IMyInterface.
Variable can be converted to MyStruct.
這個例子說明了使用is運算符的各種可能的結果。其中定義了3個類、一個接口和一個結構,並把它們用作類的方法的參數,使用is運算符確定它們是否可以轉換為ClassA類型、接口類型和結構類型。只有ClassA和ClassD(繼承於ClassA)類型與ClassA兼容。如果類型沒有繼承一個類,就不會與該類兼容。ClassA、ClassB和MyStruct類都實現IMyInterface,所以它們都與IMyInterface類型兼容,ClassD繼承了ClassA,所以它們兩個也兼容。因此,只有ClassC是不兼容的。最后,只有MyStuct類型的變量本身和該類型的封箱變量與MyStruct兼容,因為不能把引用類型轉換位值類型。
3.as運算符
as運算將使用下面的語法,把一種類型轉換為指定的引用類型:
<operand> as <type>
這只適用於下列情況:
● <operand>的類型是<type>類型
● <operand>可以隱式轉換為<type>類型
● <operand>可以封箱到類型<typer>.中
如果不能從<operand>顯式轉換為<type>,則表達式的結果就是null。從基類到派生類之間的轉換可以顯式進行,但這常常是無效的。考慮上面例子中的ClassA和ClassD兩個類,其中ClassD派生於ClassA。下面的代碼使用as運算符把存儲在obj1中的ClassA實例轉換為ClassD類型:
ClassA obj1 = new ClassA();
ClassD obj2 = obj1 as ClassD();
這樣,就使obj2的結果為null。
但利用多態性可以把ClassD實例存儲在ClassA類型的變量中。下面的代碼就驗證了這一點,使用as運算符把包含ClassD類型實例的ClassA類型變量轉換為ClassD類型:
ClassD obj1 = new ClassD();
ClassA obj2 = obj1;
ClassD obj3 = obj2 as ClassD ;
這次的結果是obj3包含與obj1相同的對象引用,而不是null。
因此,as運算符非常有用,因為下面使用簡單數據類型轉換的代碼會拋出一個異常:
ClassA obj1 = new ClassA();
ClassD obj2 = (ClassD)obj1;
但上面的as表達式只會把null賦給obj2,不會拋出一個異常。
3.4.4 深度復制
上一節介紹了如何使用受保護的方法System.Object.MemberwiseClone()進行引用復制,使用一個方法GetCopy(),如下所示:
public class Cloner
{
public int Val;
public Cloner(int newVal)
{
Val = newVal;
}
public object GetCopy()
{
return MemberwiseClone();
}
}
假定有引用類型的字段,而不是值類型的字段,例如:
public class Content
{
public int Val;
}
public class Cloner
{
public Content MyContent = new Content();
public Cloner(int newVal)
{
MyContent.Val = newVal;
}
public object GetCopy()
{
return MemberwiseClone();
}
}
此時,通過GetCopy()得到的引用復制有一個字段,它引用的對象與源對象相同。下面的代碼使用這個類來說明這一點:
Cloner mySource = new Cloner(5);
Cloner myTafget = (Cloner) mySource.GetCopy();
Console.WriteLine(“myTarget.MyContent.Val = {0}”,myTarget.MyContent.Val) ;
mySource.MyContent.Val = 2;
Console.WriteLine(“myTarget.MyContent.Val = {0}”,myTarget.MyContent.Val) ;
第4行把一個值賦給mySource.MyContent.Val,源對象中公共字段MyContent的公共字段Val,也改變了myTarget.MyContent.Val的值。這是因為mySource.MyContent引用了與myTarget.MyContent相同的對象實例。上述代碼的結果如下:
myTarget.MyContent.Val = 5
myTarget.MyContent.Val = 2
修改上面的GetCopy()方法就可以進行深度復制,但最好使用.NET Framewok的標淮方式。為此,實現ICloneable接口,該接口有一個方法Clone(),這個方法不帶參數,返回一個對象類型,其簽名和上面使用的GetCopy()方法相同。
修改上面使用的類,可以使用下面的深度復制代碼:
public class Content
{
public int Val;
}
public class Cloner : ICloneable
{
public Content MyContent = new Content();
public Cloner(int newVal)
{
MyContent.Val = newVal;
}
public object Clone()
{
Cloner clonerCloner = new Cloner(MyContent.Val);
return clonerCloner;
}
}
其中使用包含在源Cloner對象(MyContent)中的Content對象的Val字段,創建一個新Cloner對象(MyContent)。這個字段是一個值類型,所以不需要深度復制。
使用與上面類似的代碼測試引用復制,但使用Clone()而不是GetCopy(),得到如下結果:
myTarget.MyContent.Val = 5
myTarget.MyContent.Val = 5
這次包含的對象是獨立的。
有時在比較復雜的對象系統中,調用Clone()應是一個遞歸過程。例如,如果Cloner類的MyContent字段也需要深度復制,就要使用下面的代碼:
public class Cloner : Icloneable
{
public Content MyContent = new Content();
…
public object Clone()
{
Cloner clonedCloner = new Cloner();
clonedCloner.MyContent = MyContent.Clone();
return clonedCloner;
}
}
這里調用了默認的構造函數,簡化了創建一個新Cloner()對象的語法。為了使這段代碼能正常工作,還需要在Content類上執行ICloneable接口。
3.4.5 定制異常
檢測錯誤並相應進行處理是正確設計軟件的基本原則。理想情況下,編寫代碼時,每一行代碼都按預想的運作,要用到的每種資源總是可以利用。但是,在現實世界中卻遠非如此順利。其他程序員可能會犯錯,網絡連接可能會中斷,數據庫服務器可能會停止運行,磁盤文件不一定有應用程序想要的內容。總之,編寫的代碼必須能夠檢測出類的這些錯誤並采取相應的對策。
報告錯誤的機制與錯誤本身一樣多種多樣。有些方法設計為返回一個布爾值,用它來指示方法的成功或者失敗。還有一些方法設計為將錯誤寫入某個日志文件或者數據庫中。豐富的錯誤報告模型意味着監控錯誤的代碼必須相對健壯。使用的每種方法可能以不同的方式報告錯誤,這也就是說,應用程序可能會因為各種方法檢測錯誤的方式不同而導致代碼雜亂。
.NET框架提供了一種用於報告錯誤的標准機制,稱之為結構化異常處理(SEH,Structured Exception Handling)。這種機制依靠異常指明失敗。異常是描述錯誤的類。.NET架使用異常來報告錯誤,並且在代碼中也可以使用異常。編寫代碼來監視任何代碼段生成的異常。不管它是來自CLR還是程序員自己的代碼,並且相應處理生成的異常。使用SEH,只需要在代碼中創建一個錯誤處理設計模式即可。
這種統一的錯誤處理方法對於啟用多語種.NET編程也是很重要的。當使用SEH設計所有代碼時,能夠安全並容易地混合以及匹配代碼(例如,C#,C++或者VB.NET)。作為對遵循SEH規則的回報。.NET框架確保通過各種語言正確地傳播和處理所有錯誤。
在C#代碼中檢測並處理異常非常簡單。在處理異常時需要標識三個代碼塊:使用異常處理的代碼塊;在處理第一個代碼塊時,如果找到某個異常,就執行代碼塊;在處理完異常之后執行選擇的代碼塊。
在C#中,異常的生成稱之為拋出(throwing)異常。被通知拋出了一個異常則稱之為捕獲(catching)異常。處理完異常之后執行的代碼塊是終結(finally)代碼塊。將介紹如何在C#中使用這些結構。還會介紹異常層次結構的成員。
與所有設計准則一樣,盲目地針對每種錯誤使用異常足不必要的。當錯誤對於代碼塊來說是本地的時候,使用錯誤返回代碼方法是比較合適的。在實現表單的有效性驗證時,經常會看到這種方法。這是可以接受的作法,因為通常有效性驗證錯誤對於收集輸入的表單來說是本地的。換句話說,當有效性驗證錯誤發生時,顯示一條錯誤消息並請求用戶正確地重新輸入所需的信息。因為錯誤和處理代碼都是本地的,所以處理資源泄漏很簡單。另一個示例是在讀取文件時處理一個文件結束條件。不用付出異常所需的開銷,就可以很好地處理這個條件。當錯誤發生時,完全在這個代碼塊中處理該錯誤條件。當調用發生錯誤所在代碼塊之外的代碼時,傾向於使用SEH處理錯誤。
1.指定異常處理
C#的關鍵字try指定讓某個代碼塊監視代碼執行時拋出的任何異常。使用try關鍵字很簡單。使用時,try關鍵字后面跟一對花括號,花括號中的語句用來監視代碼執行時拋出的異常。
try
{
//place satements here
}
在執行try代碼塊中的任何語句時,如果有異常拋出,就可以在代碼中捕獲該異常並相應進行處理。
2.捕獲異常
如果使用try關鍵字來指定希望被通知有關的異常拋出,就需要編寫捕獲異常的代碼並在代碼中處理報告的錯誤。
try代碼塊后面的C#關鍵字catch用於指定當捕獲到異常時應該執行哪段代碼。catch關鍵字的工作機理與try關鍵字類似。
2.1使用try關鍵字
最簡單形式的catch代碼塊捕獲前面try代碼塊中代碼拋出的任何異常。catch代碼塊的結構類似try代碼塊,以下面的代碼為例:
try
{
//place statements here
}
catch
{
//place statements here
}
如果從try代碼塊中拋出一個異常,就執行catch代碼塊中語句。如果try代碼塊中的語句沒有拋出異常,就不執行catch代碼塊中的代碼。
2.2捕獲特定類的異常
還可以編寫catch代碼塊來處理由try代碼塊中一條語句拋出的特定類的異常。在本章后面的“由.NET框架定義的異常”一節中,格介紹類異常的更多內容。catch代碼塊使用下列語法:
catch(要處理的異常所屬的類 異常的變量標識符)
{
當try代碼塊拋出指定類型的異常時,將執行的語句
}
以上面的代碼為例:
try
{
//place statements here
}
catch(Exception thrownException)
{
//palce statements here
}
在上述示例中,catch代碼塊捕獲try代碼塊拋出的Exception類型的異常。其中定義了一個Exception類型的變量ThrownException。在catch代碼塊中使用ThrownException變量來獲得有關拋出的異常的更多信息。
try代碼塊中的代碼可能會拋出各種類的異常,此時就要處理每個不同的類。C#允許指定多個catch代碼塊,每個代碼塊處理一種特定類的錯誤;
try
{
//place statements here
}
catch(Exception ThrownException)
{
//Catch Block 1
}
catch(Exception ThrownException2)
{
//Catch Block2
}
在上述示例中,檢測try代碼塊中的代碼以便拋出異常。 如果CLR發現try代碼塊中的代碼拋出了—個異常,那么就檢查該異帶所屬的類並執行相應的catch代碼塊。如果拋出的異常是類Exception2的一個對象.則不執行任何catch代碼塊。
還可以在catch代碼塊列表中添加一個普通的catch代碼塊,以下面的代碼為例:
try
{
//place statements here
}
catch(Exception ThrownException)
{
//Catch Block 1
}
catch
{
//Catch Block 2
}
在上述示例中,檢測try代碼塊中的代碼以便拋出異常。如果拋出的異常是類Exception的一個對象,則執行catch Block 1中的代碼。如果拋出的異常是其他類的對象,則執行普通catch代碼塊——catch Block 2中的代碼。
2.3出現異常之后進行消除
在catch代碼塊之后可能會有一代碼塊。在處理完異常之后以及沒有異常發生時都將執行這個代碼塊。如果要執行這類代碼,可以編寫一個finally代碼塊。C#的關鍵字finally指定在執行try代碼塊之后應該執行的代碼塊。finally代碼塊的結構與try代碼塊的結構相同:
finally
{
//place statements here
}
在finally代碼塊中釋放先前在方法中分配的資源,例如,假設編寫一個打開三個文件的方法。如果在try代碼塊中包含文件訪問代碼,那么將能夠捕獲與打開、讀、寫文件有關的異常。但是,在代碼的最后,即使拋出一個異常,也要關閉這三個文件。最好將關閉文件語句放入finally代碼塊,代碼如下所示:
try
{
//open files
//read files
}
catch
{
//catch exceptions
}
finally
{
//close files
}
如果沒任何catch塊,C#編譯器可以定義一個finally塊。用戶可以在try塊后編寫一個finally代碼塊。
3.由.NET框架定義的異常
.NET Framework定義了各種異常,在C#代碼或者調用的方法中找到某些錯誤時均可拋出這些異常。所有這些異常都是標准的.NET異常並且可以使用C#的catch代碼塊捕獲。每個.NET異常都定義在.NLT System名字空間中。下面描述了其中—些公共的異常。這些異常只代表.NET框架的基類庫中定義的一小部分異常。
3.1OutOfMemoryException異常
當用完內存時,CLR拋出OutOfMemoryException異常。如果試圖使用new運算符創建一個對象,而CLR沒有足夠的內存滿足這項請求,那么CLR就會拋出OutOfMenoryException異常,如下所示。
【例3-6】
using System;
Class MainClass
{
public static void Main()
{
int [] LargeArray;
try
{
LargeArray = new int[2000000000];
}
catch (OutOfMemeoryException)
{
Console.WriteLine("The CLR is out of memory.");
}
}
}
程序中的代碼試圖給一個擁有20億個整數的數組分配內存空間。因為—個整數要占用4字節內存空間,所以這么大的數組需要占用80億字節內存空間。而機器沒有這么大的內存空間可以使用,所以分配內存的操作將以失敗告終。try代碼塊中包含內存分配代碼,另外,還定義了一個catch代碼塊來處理CLR拋出的任何OutOfMemoryException異常。
3.2StackOverflowException異常
當用完堆棧空間時,CLR拋出StackOverflowException異常。CLR管理稱為堆棧(stack)的數據結構,堆棧用於跟蹤調用的方法以及這些方法的調用次序。CLR的堆棧空間有限,如果堆棧已滿,就會拋出StackOverflowException異常,如下所示。
【例3-7】
using System;
class MainClass
{
public static void Main()
{
try
{
Recursive();
}
catch(StackOverflowException)
{
Console.WriteLine("The CLR is out of stack space.");
}
}
public static void Recursive()
{
Recursive();
}
}
程序中的代碼實現了方法Recursive(),該方法在返回之前調用它本身。Main()方法調用Recursive()方法,並且最終會導致CLR消耗完堆棧空間,因為Recursive()方法從不真正地返回。Main()方法調用Recursive()方法,Recursive()方法反過來又調用Recursive()方法,不停地調用這種方法。最終,CLR消耗完堆棧空間並拋出StackOverflowException異常。
3.3NullReferenceException異常
在下面例子中,編譯器將捕獲試圖間接訪問一個空對象的異常。
【例3-8】
using System;
class MyClass
{
public int value;
}
class MainClass
{
public static void Main()
{
try
{
MyObject = new MyClass();
MyObject = null;
MyObject.value =123;
}
catch(NullReferenceExcption)
{
Console.WriteLine("Cannot reference a null object.");
}
}
}
程序中的代碼聲明了一個MyClass類型的對象變量,並將該變量設置為null(如果在語句中沒有使用new運算符,而只是聲明了一個MyClass類型的對象變量,那么在編譯時,編譯器將發出如下錯誤消息:“使用了未賦值的局部變量MyObject”)。然后,使用該對象的公共字段value,因為不能引用null對象,所以這種做法是非法的。cLR捕獲這類錯誤並拋出NullReferenceException異常。
3.4 TypeInitializationException異常
當某個類定義了一個靜態構造函數並且該構造因數拋出異常時,CLR就拋出TypeInitializationException異常。如果該構造函數中沒有catch代碼塊捕獲這類異常.那么CLR就拋出TypeInitializationException異常。
3.5 InvalidCastException異常
如果顯式類型轉換失敗,CLR就拋出InvalidCastException異常。在接口環境下容易產生這類情況。下面的例子說明了InvalidCastException異常。
【例3-9】
using System;
class MainClass
{
public static void Main()
{
try
{
MainClass MyObject = new MainClass();
IFormattable Formattable;
Formattable = (IFormattable)MyObject;
}
catch(InvalidCastException)
{
Console.WriteLine("MyObject does not implement the IFormattable interface");
}
}
}
程序中的代碼使用一個類型轉換運算符試圖獲得對.NKT的IFormattable接口的引用。因為MainClass類並沒有實現IFormattable接口,所以類型轉換操作會失敗,而且CLR 還會拋出InvalidCastException異常。
3.6 ArrayTypeMismatchException異常
當代碼將某個元素存儲到一個數組中時,如果元素類型與數組不匹配,CLR就會拋出ArrayTypeMismatchException異常。
3.7 IndexOutOfRangeException異常
當代碼使用元素索引號將元素存儲到數組時,如果元素索引號超出了數組的范圍.CLR就會拋出IndexOutOfRangeException異常。下面的程序說明了IndexOutOfRangeException異常。
【例3-10】
using System;
class MianClass
{
public static void Main()
{
try
{
int [] IntegerArray = new int[5];
IntegerArray[10] = 123;
}
catch(IndexOutOfRangeException)
{
Console.WriteLine("An invald element index access was attempted.");
}
}
}
程序中的代碼創建了一個擁有五個元素的數組,然后試圖給數組中的第十個元素指定值。因為索引號10已經超出了整型數組的范圍,所以CLR拋出IndexOutOfRangeException異常。
3.8 DivideByZeroException異常
當代碼執行數學運算時,如果導致用零作為除數,則CLR拋出DivideByZeroException異常。
3.9 OverflowException異常
當在數學運算中使用了C#的checked運算符時,如果導致溢出,則CLR拋出0verflowException異常。下面的程序說明了0verflowException異常。
【例3-11】
using System;
class MainClass
{
public static void Main()
{
try
{
checked
{
int Integer1;
int Integer2;
int Sum;
Integer1 = 2000000000;
Integer2 = 2000000000;
Sum = Integer1 + Integer2;
}
}
catch(OverflowException)
{
Console.WriteLine("A mathematical operation caused an overflow.");
}
}
}
程序中的代碼將兩個整數相加,每個整數的位為20億。結果即40億賦給第二個整型變量。問題是加法的結果大於C#整型值所允許的最大值,因此拋出數學運算溢出異常。
4.使用自定義的異常
可以自己定義異常,並在代碼中使用它們,就像它們是由.NET框架定義的異常一樣。這種設計一致性使得程序員能夠編寫catch代碼塊來處理從任何代碼段拋出的任何異常,不管代碼是.NET框架的,還是自定義類中的,或者是運行時執行的某個程序集中的。
4.1自定義異常
.NET框架聲明了一個類System.Exception,用它來作為.NET框架中的所有異常的基類。預先定義好的公共語言運行時類就從System.System.Exception類派生出來,而這個類是從System.Exception類派生出來的。按照這種規則,DivideByZeroException,NotFiniteNumberException和OverflowException異常從System.ArithmeticException類派生出來,而這個類又是從System. System.Exception類派生而來。定義的任何異常類必須從Systcm.ArithmeticException類派生而來,而這個類又派生自System.Exception基類。
System.Exception類包含四個只瀆屬性,在catch代碼塊中可以使用這些屬性來獲取有關拋出的異常的更多信息:
● Message屬性包含對異常原因的描述。
● InnerException屬性包含引起拋出當前異常的異常。該屬性的值可以為null,它表示沒 有可用的內異常。如果該屬性的值不為null,就會指向拋出的異常對象,而該對象導 致當前異常拋出。對於catch代碼塊來說,捕獲一種異常而拋出另一個異常也是可能 的。在這種情況下,InnerException屬性將包含對catch代碼塊捕獲的原異常對象的引 用。
● StackTrace屬性包含一個字符串,用它來顯示拋出異常時正在使用的方法調用的堆棧。
最后,堆棧服蹤將跟蹤所有返回CLR原始調用的路線至應用程序的Main()方法、TargetSite屬性包含拋出異常的方法。
其中某些屬性可以在System.Exception類的一個構造函數中指定:
public Exception(string message);
public Exception(string message, Exception innerException);
在構造函數中,自定義的異常可以調用基類的構造函數,以便設置屬性,以下面的代碼
為例:
using System;
class MyException:ApplicationException
{
public MyException(): base("This is my exception message.")
{
}
}
上述代碼定義了類MyException,該類從ApplicationException類派生出來。MyException類的構造函數使用base關鍵字調用了這個基類的構造函數。將基類的Message屬性設置為“Thisis my exception message”。
4.2拋出自定義的異常
可以用C#的throw關鍵字拋出自定義的異常。throw關鍵字后面必須跟一個表達式,該表達式的值為類System.Exception或者其派生類的一個對象。例如:
【例3-12】
using System;
class MyException: ApplicationException
{
public MyException():base("This is my exception message.")
{
}
}
class MainClass
{
public static void Main()
{
try
{
MainClass MyObject = new MainClass();
MyObject.ThrowException();
}
catch(MyException CaughtException)
{
Console.WriteLine(CaughException.Message);
}
}
public void ThrowException()
{
throw new MyException();
}
}
}
程序中的代碼聲明了一個新類MyException,該類派生自.NET框架定義的一個基類APPlicationException。MainClass類包括方法ThrowException,該方法拋出一個類型為MyException的new對象。該方法由Main()方法調用,調用代碼位於try代碼塊中。Main()方法還包含一個catch代碼塊,該代碼塊實現將異常的消息輸出到控制台上。因為是在構造MyException類對象時設置的消息,所以現在可以用這條消息並將其打印出來。運行清代碼后,將在控制台上打印出下列消息:
This is my exception message.
3.4.6 事件和委托
在典型的面向對象軟件的一般流程中,代碼段創建類的對象並在該對象上調用力法。在這種情況下,調用程序是主動代碼,因為它們是調用方法的代碼。而對象是被動的,因為只有當某種方法被調用時才會用上對象並執行某種動作。
然而,也可能存在相反的情況。對象可以執行一些任務並在執行過程中發生某些事情時通知調用程序。稱這類事情為事件(event),對象的事件發布稱為引發事件。
事件驅動處理對於.NET來說並不是什么技術,在事件驅動處理中,當有事件發生時,某些代碼段會通知其他對事件感興趣的代碼段。當用戶使用鼠標、敲擊鍵盤或者移動窗口時,Windows用戶接口層一直使用事件的形式通知Windows應用程序。當用戶采取影響ActiveX控件的動作時,ActiveX控件就會引發事件至ActiveX控件容器。
為了在C#代碼中激發、發布和預約事件更容易,C#語言提供了一些特殊的關鍵字。使用這些關鍵字允許C#類毫不費力地激發和處理事件。
1.定義委托
當設計C#類引發的事件時,需要決定其他代碼段如何接收事件。其他代碼段需要編寫一種方法來接收和處理發布的事件。例如,假設類實現了一個Web服務器,並想在任何時間從Internet發來頁面請求時激發一個事件。在類激發這個new request事件時,其他代碼段執行某種動作,並且代碼中應該包含一種方法,在激發事件時執行該方法。
類用戶實現接受和處理事件的方法由C#中的概念——委托(delegate)定義。委托是一種“函數模板”,它描述了用戶的事件處理程序必須有的結構。委托也是一個類,其中包含一個簽名以及對方法的引用。就像一個函數指針,但是它又能包含對靜態和實例方法的引用。對於實例方法來說,委托存儲了對函數人口點的引用以及對對象的引用。委托定義丁用戶事件處理程序內該返回的內容以及應該具備的參數表。
要在C#中定義一個委托,使用下列語法:
delegate 事件處理程序的返回類型 委托標識符(事件處理程序的參數表)
如果在激發事件的類中聲明委托,可以在委托前加上前綴public,protected,internal或者 private關鍵字,如下面的delegate定義示例所示:
public delegate void EvenNumberHandler(int Number);
在上述示例中,創建了一個稱為EvenNumberHandler的公共委托(public delegate),它不返回任何值。該委托只定義了一個要傳遞給它的參數,該參數為int類型。委托標識符(這里是EvenNumberHandler)可以是任何名稱,只要不與C#關鍵字的名稱重復即可。
2.定義事件
為了闡述清楚事件的概念,我們以一個示例開始講述。假設正驅車在路上並且儀表板上顯示燃料不足的燈亮了。在這個過程中。汽缸中的傳感器給計算機發出燃料快要耗盡的信號。然后,計算機又激發一個事件來點亮儀表板上的燈,這樣司機就知道要購買更多的油了。用最簡單的話來說,事件是計算機警告你發發生某種狀況的一種方式。
使用C#的關鍵字event來定義類激發的事件。在C#中事件聲明的最簡單形式包含下列內
容:
evet 事件類型 事件標識符 事件類型委托標識符匹配
以下面的Web服務器示例為例:
public delegate void NewRequestHandler(string URL);
public class WebSever
{
public event NewRequestHandler NewRequestEvent;
//...
}
上述示例聲明了一個稱為NewRequestHandler的委托。NewRequestHandler定義了一個委托,作為處理new request事件的方法的方法模板。任何處理new request事件的方法都必須遵循委托的調用規則:必須不返回任何數據,必須用一個字符串作為參數表。事件處理程序的實現可以擁有任意的方法名稱,只要返回值和參數表符合委托模板的要求即可。
WebServer類定義了一個事件NewRequestEvent。該事件的類型為NewRequestHandle。這意味着,只有與該委托的調用規則相匹配的事件處理程序才可以用於處理NewRequestEvent事件。
3.安裝事件
編寫完事件處理程序之后,必須用new運算符創建一個它的實例並將它安裝到激發事件的類中。創建一個事件處理程序new實例時,要用new運算符創建一個屬於這種委托類型的變量,並且作為參數傳遞事件處理程序方法的名稱。以Web服務器的示例為例,對事件處理程序new實例的創建如下代碼所示:
public void MyNewRequestHandler(string URL)
{
}
NewRequestHandler HandlerInstance;
HandlerInstance = new NewRequestHandler(MyNewRequestHandler);
在創建了事件處理程序的new實例之后,使用+=運算符將其添加到事件變量中:
NewRequestEvent += HandlerInstance;
上述語句連接了HandleInstance委托實例與NewRequestEvent事件,該委托支持MyNewRequestMethod方法。使用+=運算符,可以將任意多的委托實例與一個事件相連接。同理,可以使用-=運算符從事件連接中刪除委托實例:
NewRequestEvent -= HandlerInstance;
上述語句解除了HandlerInstance委托實例與NewRequestEvent事件的連接。
4.激發事件
可以使用事件標識符(比如事件的名稱)從類中激發事件.就像事件是一個方法一樣。作為方法調用事件將激發該事件c在Web瀏覽器示例中,使用如下語句激發new request事件:
NewRequestEvent(strURLOfNewRequest);
事件激發調用中使用的參數必須與事件委托的參數表匹配。定義NewRequestEvent事件的委托接受一個字符串參數;因此,當從Web瀏覽器類中激發該事件時,必須提供一個字符串。
下面的例子說明了委托和事件的概念。其中實現了一個類,該類從0計數到100,並在計數過程中找到偶數時激發一個事件。
【例3-13】
using System;
public delegate void EvenNumberHandler(int Number);
class Counter
{
public event EvenNumberHandler OnEvenNumber;
public Counter()
{
OnEvenNumber = null;
}
public void CountTo100()
{
int CurrentNumber;
for(CurrentNumber = 0; CurrentNumber <= 100;CurrentNumber++)
{
if(CurrentNumber % 2 == 0)
{
if(OnEvenNumber != null)
{
OnEventNumber(CurrentNumber);
}
}
}
}
}
classEvenNumberHandlerClass
{
public void EvenNumberFound(int EvenNumber)
{
Console.WriteLine(EvenNumber);
}
}
class ClassMain
{
public static void Main()
{
Counter MyCounter = new Counter();
EvenNumberHandlerClass MyEvenNumberHandlerClass = new
EvenNumberHandlerClass();
MyCounter.OnEvenNUmber += new
EvenNumberHandler(MyEvenNumberHandlerClass.EvenNumberFound);
}
}
程序實現了三個類
● Counter類執行計數功能。其中實現了一個公共方法CountTo100()和一個公共事件 OnEvenNumber。OnEvenNumber事件的委托類型為EvenNumberHandler。
● EvenNumherHandlerClass類包含一個公共方法EvenNumberFound。該方法為Counter類 的OnEvenNumber事件的事件處理程序。它將作為參數提供的整數打印到控制台上。
● MainClass類包含應用程序的Main()方法。Main()方法創建類Counter的一個對象並將該對象命名為MyCounter。還創建了類EvenNumberHandlerC1ass的一個new對象,井調用了對象MyEvenNumberHandlerC1ass。Majn()方法調用MyCounter對象的CountTo100()方法,但是不是在將委托實例安裝到Counter類中之前調用的。其中的代碼創建了一個new委托實例,用它來管理MyEvenNumber。HandlerClass對象的EvenNumberFound方法,並使用+=運算法將其添加到MyCounter對象的0nEvenNumber事件中。
CountTol00方法的實現使用一個局部變量從0計數到100。在每一次計數循環中,代碼都會檢測數字是否是偶數,方法是看數字被2除后是否有余數。如果數字確實是偶數,代碼就激發OnEvenNumber事件,將偶數作為參數提供,以便與事件委托的參數表匹配。
因為MyEvenNumherHandlerClass的EvenNumberFound方法是作為事件處理程序安裝的,而且該方法將提供的參數打印到控制台上,所以編譯並運行代碼后,0到100之間的所有偶數都會打印到控制台上。
6.標准化事件的設計
盡管C#可以接受它編譯的任何委托設計,但是.NET框架還是鼓勵開發人員采用標准的委托設計方式。最好委托設計使用兩個參數;例如,SystemEventhandler委托:
● 對引發事件的對象的引用
● 包含與事件有關的數據的對象
第二個參數包含了所有事件數據,應該是某個類的一個對象.這個類由.NET的System.EventArgs類派生出來。
對上面代碼進行修改,其中使用了這種標准的設計方式。
【例3-14】
using System;
public delegate void EvenNumberHandler(object Originator,
OnEvenNumberEventArgs EventNumberEventArgs);
class Counter
{
public event EvenNumberHandler OnEvenNumber;
public Counter()
{
OnEvenNumber = null;
}
public void CountTo100()
{
int CurrentNumber;
for(CurrentNumber = 0; CurrentNumber <= 100;CurrentNumber++)
{
if(CurrentNumber % 2 == 0)
{
if(OnEvenNumber != null)
{
OnEvenNumberEventArgs EventArguments;
EventArguments = new OnEvenNumberEvenArgs(CurrentNumber);
OnEventNumber(this,EventArguments);
}
}
}
}
}
public class OnEvenNumberEventArgs : EventArgs
{
private int EventNumber;
public OnEvenNumberEventArgs(int EvenNumber)
{
this.EvenNumber = EvenNumber;
}
public int Number
{
get
{
return EventNumber;
}
}
}
class EvenNumberHandlerClass
{
public void EvenNumberFound(object Originator,
OnEvenNumberEventArgs EvenNumberEventArgs)
{
Console.WriteLine(EvenNumberEventArgs.Number);
}
}
Class MainClass
{
public static void Main()
{
Counter MyCounter = new Counter();
EvenNumberHandlerClass MyEvenNumberHandlerClass = new
EvenNumberHandlerClass();
MyCounter.OnEvenNUmber += new
EvenNumberHandler(MyEvenNumberHandlerClass.EvenNumberFound);
MyCounter.CountTo100();
}
}
本章小結
本章學習了面向對象編程的基本概念,包括什么是對象、類、屬性和字段、方法、靜態成員等,還說明了對象的生命周期。討論了OOP中的一個技術,包括:抽象與接口、繼承、多態性、運算符重載等。
基本的類定義語法、用於確定類可訪問性的關鍵字、指定繼承的方式以及接口的定義也是在本章學習的。所有的.NET類都派生於System.Object。介紹了object中定義的方法。如何提供我們自己的構造函數和析構函數,以便初始化對象和清理對象。接口和抽象類的相似和不同之處,看看哪些情況應使用什么技術。講述了類和結構的區別。
本章學習了如何定義字段、屬性和方法等類成員,隱藏基類成員、調用重寫的基類成員,接口的實現。集合類一般用於處理對象列表,其功能比簡單數組要多,這些功能是通過執行System.Collections命名空間中的接口而實現的。可以通過我們設計的類使用標准的運算符,例如+,>等,這稱為重載。如何進行運算符重載。封箱(boxing)是把值類型轉換為System.Object類型,或者轉換為由值類型執行的接口類型。拆箱(unboxing)是相反的轉換過程。is和as運算符的使用方法,如何深度復制,如何定制異常,什么是事件和委托,如何使用。
下一章將學習如何用Windows組件設計可視化應用程序。
習題3
1.“必須手動調用對象的析構函數,否則就會浪費資源”,對嗎?
2.需要創建一個對象以調用其類的靜態方法嗎?
3.下面的代碼有什么錯誤?
public sealed class MyClass
{
// class members
}
public class myDerivedClass : MyClass
{
// class members
}
4.如何定義不能創建的類?
5.為什么不能創建的類仍舊有用?如何利用它們的功能?
6.編寫代碼,用虛擬方法GetString()定義一個基類MyClsss。這個方法應返回存儲在受保
護的字段myString中的字符串,該字段可以通過只寫公共屬性ContainedString來訪問。
7.從類MyClass派生一個類MyDerivdClass。重寫GetString()方法,使用該方法的基類執行代碼從基類中返回一個字符串,但在返回的字符串中添加文本“(output from derived class)”。
8. 編寫—個類MyCopyableClass,該類可以使用方法GetCopy()返回它本身的一個副本。這個方法應使用派生干System.0bject的MemberwiseClone()方法。給該類添加一個簡單的屬性,並且編寫使用該類檢查任務是否成功執行的客戶代碼。
9.創建一個集合類People,它是下述Person類的集合,該集合中的項目可以通過一個字符串索引符來訪問,該字符串索引符是人的姓名,與Person.Name屬性相同:
public class Person
{
private string name;
private int age;
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
public int Age
{
get
{
return age;
}
set
{
age = value;
}
}
}
10.擴展上一題中的Person類,重載>,<,>=和<=運算符,比較Person實例的Age屬性。
11.給People類添加GetOldest()方法,使用上面定義的重載運算符,返回其Age屬性的值為最大的Person對象數組(1個或多個對象,因為對於這個屬性而言,多個項目可以有相問的值)。
12.在People類上執行ICloneable接口,提供深度復制功能。