C# 語言是一門簡單,現代,優雅,面向對象,類型安全,平台獨立的一門新型組件編程語言。其語法風格源自C/C++家族,融合了Visual Basic的高效和C/C++強大,是微軟為奠定其下一互聯網霸主地位而打造的Microsoft.Net平台的主流語言。其一經推出便以其強大的操作能 力,優雅的語法風格,創新的語言特性,第一等的面向組件編程的支持而深受世界各地程序員的好評和喜愛。“它就是我多年來夢寐以求的計算機語言!”--很多 資深程序員拿到C#都是這樣的驚訝。從C#語言的名字(C Sharp)我們也可見微軟用其打造其下一代互聯網絡深度服務的勃勃雄心。C#語言目前已由微軟提交歐洲計算機制造商協會ECMA,經過標准化後的C#將 可由任何廠商在任何平台上實現其開發工具及其支持軟件,這為C#的發展提供了強大的驅動力,我們也可從這裡看到微軟前所未有的眼光和智慧。
組 件編程已經成為當今世界軟件業面向下一代程序開發的一致選擇,是90年代面向對象編程的深度發展。C#生逢其時,占盡天時地利,“第一等的面向組件編程的 支持”也決不是簡單說說那麼輕松。實際上,組件特性已經深深植入C#語言的各個層面,是為C#銳利(Sharp)之處。在下面的文章中筆者將從C#語言的 各個層面來展現C#語言中無處不見的組件特性,深度闡述C#面向組件編程。整個專題共分為十講:“第一講 ‘Hello,World!’程序”,“第二講 C#語言基礎介紹”,“第三講 Microsoft.Net平台基礎構造”,“第四講 類與對象”,“第五講 構造器與析構器”,“第六講 方法”,“第七講 域與屬性”,“第八講 索引器與操作符重載”,“第九講 數組與字符串”,“第十講 特征與映射”,“第十一講 COM互操作 非托管編程與異常處理”,“第十二講 用C#編織未來--C#編程模型概述”。
本頁內容
第一講 “Hello,World!”程序
第二講 C#語言基礎介紹
第三講 Microsoft.Net平台基礎構造
第四講 類與對象
第五講 構造器與析構器
第六講 方法
第七講 域與屬性
第八講 索引器與操作符重載
第一講 “Hello,World!”程序
“Hello World!”程序是程序員一直以來的一個浪漫約定,也是一個偉大的夢想--總有一天,出自人類之手的計算機會面對這個美麗的世界說一聲“Hello World!”。它是學習一門新語言的一個很好的起點,我們就從這裡開始,看下面例子:
//HelloWorld.cs by CornfIEld,2001
//csc HelloWorld.cs
using System;
class HelloWorld
{
public static void Main()
{
Console.WriteLine("Hello World !");
}
}
我 們可以打開Windows自帶的簡易的"記事本"程序來編寫這段代碼--筆者推薦剛開始采用這個極其簡單卻能把程序代碼暴露的相當清晰的編輯工具。我們將 它的文件名保存為HelloWorld.cs,其中".cs"是C#源代碼文件的擴展名。然後在配置好C#編譯器的命令行環境裡鍵入"csc HelloWorld.cs"編譯文件。可以看到編譯輸出文件HelloWorld.exe。我們鍵入HelloWorld執行這個文件可得到下面的輸 出:
Hello World !
下面我們來仔細分析上面的代碼和整個程序的編譯輸出及執行過程。先看文件開始的兩行代碼,這是C#語言的單行注釋語句。和C++語言類似,C#支持兩種注釋方法:以"//"開始的單行注釋和以"/*","*/"配對使用的多行注釋。注釋之間不能嵌套。
再 來看下面的"using System;"語句,這是C#語言的using命名空間指示符,這裡的"System"是Microsoft.NET系統提供的類庫。C#語言沒有自己 的語言類庫,它直接獲取Microsoft.NET系統類庫。Microsoft.Net類庫為我們的編程提供了非常強大的通用功能。該語句使得我們可以 用簡短的別名"Console"來代替類型"System.Console"。當然using指示符並不是必須的,我們可以用類型的全局名字來獲取類型。 實際上,using語句采用與否根本不會對C#編譯輸出的程序有任何影響,它僅僅是簡化了較長的命名空間的類型引用方式。
接著我們聲明 並實現了一個含有靜態Main()函數的HelloWorld類。C#所有的聲明和實現都要放在同一個文件裡,不像C++那樣可以將兩者分離。Main ()函數在C#裡非常特殊,它是編譯器規定的所有可執行程序的入口點。由於其特殊性,對Main()函數我們有以下幾條准則:
1.
Main()函數必須封裝在類或結構裡來提供可執行程序的入口點。C#采用了完全的面向對象的編程方式,C#中不可以有像C++那樣的全局函數。
2.
Main()函數必須為靜態函數(static)。這允許C#不必創建實例對象即可運行程序。
3.
Main()函數保護級別沒有特殊要求, public,protected,private等都可,但一般我們都指定其為public。
4.
Main()函數名的第一個字母要大寫,否則將不具有入口點的語義。C#是大小寫敏感的語言。
5.
Main ()函數的參數只有兩種參數形式:無參數和string 數組表示的命令行參數,即static void Main()或static void Main(string[]args) ,後者接受命令行參數。一個C#程序中只能有一個Main()函數入口點。其他形式的參數不具有入口點語義,C#不推薦通過其他參數形式重載Main() 函數,這會引起編譯警告。
6.
Main()函數返回值只能為void(無類型)或int(整數類型)。其他形式的返回值不具有入口點語義。
我 們再來看"HelloWorld.cs"程序中Main()函數的內部實現。前面提過,Console是在命名空間System下的一個類,它表示我們通 常打交道的控制台。而我們這裡是調用其靜態方法WriteLine()。如同C++一樣,靜態方法允許我們直接作用於類而非實例對象。WriteLine ()函數接受字符串類型的參數"Hello World !",並把它送入控制台顯示。如前所述,C#沒有自己的語言類庫,它直接獲取Microsoft.NET系統類庫。我們這裡正是通過獲取 Microsoft.Net系統類庫中的System.Console.WriteLine()來完成我們想要的控制台輸出操作。這樣我們便完成了 "Hello World!"程序。
但事情遠沒那麼簡單!在我們編譯輸出執行程序的同時,Microsoft.NET底層的諸多機制卻 在暗地裡湧動,要想體驗C#的銳利,我們沒有理由忽視其背靠的Microsoft.NET平台。實際上如果沒有Microsoft.Net平台,我們很難 再說C#有何銳利之處。我們先來看我們對"HelloWorld.cs"文件用csc.exe命令編譯後發生了什麼。是的,我們得到了 HelloWorld.exe文件。但那僅僅是事情的表象,實際上那個HelloWorld.exe根本不是一個可執行文件!那它是什麼?又為什麼能夠執 行?
好的,下面正是回答這些問題的地方。首先,編譯輸出的HelloWorld.exe是一個由中間語言(IL),元數據 (Metadata)和一個額外的被編譯器添加的目標平台的標准可執行文件頭(比如Win32平台就是加了一個標准Win32可執行文件頭)組成的PE (portable executable,可移植執行體)文件,而不是傳統的二進制可執行文件--雖然他們有著相同的擴展名。中間語言是一組獨立於CPU的指令集,它可以被 即時編譯器Jitter翻譯成目標平台的本地代碼。中間語言代碼使得所有Microsoft.NET平台的高級語言C#,VB.NET,VC.NET等得 以平台獨立,以及語言之間實現互操作。元數據是一個內嵌於PE文件的表的集合。元數據描述了代碼中的數據類型等一些通用語言運行時(Common Language Runtime)需要在代碼執行時知道的信息。元數據使得.Net應用程序代碼具備自描述特性,提供了類型安全保障,這在以前需要額外的類型庫或接口定義 語言(Interface Definition Language,簡稱IDL)。
這樣的解釋可能還是有點讓人困惑,那麼我們來實際 的解剖一下這個PE文件。我們采用的工具是.Net SDK Beta2自帶的ildasm.exe,它可以幫助我們提取PE文件中的有關數據。我們鍵入命令"ildasm /output:HelloWorld.il HelloWorld.exe",一般可以得到兩個輸出文件:helloworld.il和helloworld.res。其中後者是提取的資源文件,我 們暫且不管,我們來看helloworld.il文件。我們用"記事本"程序打開可以看到元數據和中間語言(IL)代碼,由於篇幅關系,我們只將其中的中 間語言代碼提取出來列於下面,有關元數據的表項我們暫且不談:
class private auto ansi beforefIEldinit HelloWorld
extends [mscorlib]System.Object
{
.method public hidebysig static void Main() cil managed
{
.entrypoint
// Code size 11 (0xb)
.maxstack 8
IL_0000: ldstr "Hello World !"
IL_0005: call void [mscorlib]System.Console::WriteLine(string)
IL_000a: ret
} // end of method HelloWorld::Main
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
// Code size 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
} // end of method HelloWorld::.ctor
} // end of class HelloWorld
我 們粗略的感受是它很類似於早先的匯編語言,但它具有了對象定義和操作的功能。我們可以看到它定義並實現了一個繼承自System.Object 的HelloWorld類及兩個函數:Main()和.ctor()。其中.ctor()是HelloWorld類的構造函數,可在 "HelloWorld.cs"源代碼中我們並沒有定義構造函數呀--是的,我們沒有定義構造函數,但C#的編譯器為我們添加了它。你還可以看到C#編譯 器也強制HelloWorld類繼承System.Object類,雖然這個我們也沒有指定。關於這些高級話題我們將在以後的講座中予以剖析。
那麼PE文件是怎麼執行的呢?下面是一個典型的C#/.Net應用程序的執行過程:
1.
用戶執行編譯器輸出的應用程序(PE文件),操作系統載入PE文件,以及其他的DLL(.Net動態連接庫)。
2.
操作系統裝載器根據前面PE文件中的可執行文件頭跳轉到程序的入口點。顯然,操作系統並不能執行中間語言,該入口點也被設計為跳轉到mscoree.dll(.Net平台的核心支持DLL)的_ CorExeMain()函數入口。
3.
CorExeMain()函數開始執行PE文件中的中間語言代碼。這裡的執行的意思是通用語言運行時按照調用的對象方法為單位,用即時編譯器將中間語言編譯成本地機二進制代碼,執行並根據需要存於機器緩存。
4.
程序的執行過程中,垃圾收集器負責內存的分配,釋放等管理功能。
5.
程序執行完畢,操作系統卸載應用程序。
清 楚的知曉編譯輸出的PE文件的執行過程是深度掌握C#語言編程的關鍵,這種過程的本身就诠釋著C#語言的高級內核機制以及其背後 Microsoft.NET平台種種詭秘的性質。一個"Hello World !"程序的概括力已經足夠,在我們對C#語言有了一個很好的起點之後,下面的專題會和大家一起領略C#基礎語言,窺探Microsoft.Net平台構 造,步步體驗C#銳利編程的極樂世界,Let's go!
返回頁首
第二講 C#語言基礎介紹
在體驗C#的銳利之前,關乎語言基本知識的掌握是必不可少的一環。由於C#基本語言很多源自C/C++,在這裡對那些和C/C++類似的地方僅作簡單介紹,我們將體驗專注於那些區別於傳統C/C++的關鍵的語言基礎知識。
數據類型
C# 語言的數據類型主要分為兩類:值類型和引用類型。另外一種數據類型"指針"是為unsafe上下文編程專門設定的,其中unsafe上下文指對代碼進行 unsafe標示以滿足利用指針對內存直接進行操作要求的C#非托管代碼,這些代碼將失去Microsoft.Net平台的垃圾收集等CLR性質,我們放 在"COM互操作 非托管編程與異常處理"專題裡闡述。值類型的變量本身包含他們的數據,而引用類型的變量包含的是指向包含數據的內存塊的引用或者叫句柄。從下面這幅圖中可 以清晰地看出兩者的差別:
引用類型帶來的可能的問題便是當多個變量引用同樣的內存塊時,對任何一個引用變量的修改都會導致該對象的值的改變。null值表示引用類型沒有對任何實際地址進行引用。
值 類型可分為結構類型和枚舉類型。結構類型包括簡單類型和用戶自定義結構類型。枚舉類型和用戶自定義結構類型我們將在"第九講 結構,枚舉,數組與字符串"專題裡詳細闡述。簡單類型又可分為布爾類型和數值類型。C#語言中布爾類型嚴格與數值類型區分,只有true和false兩種 取值,不存在像C/C++裡那樣和其他類型之間的轉換。數值類型包括整值,浮點和decimal三種類型。整值類型有sbyte,byte,short, ushort,int,uint,long,ulong,char共九種。除了char類型外,其他8種兩兩一組分別為有符號和無符號兩種。浮點值有 float和double兩種。decimal主要用於金融,貨幣等對精度要求比較高的計算環境。下表是對這些簡單類型的一個詳細的描述:
簡單類型 描 述 示 例
sbyte
8-bit 有符號整數
sbyte val = 12;
short
16-bit 有符號整數
short val = 12;
int
32-bit有符號整數
int val = 12;
long
64-bit有符號整數
long val1 = 12; long val2 = 34L;
byte
8-bit無符號整數
byte val1 = 12; byte val2 = 34U;
ushort
16-bit 無符號整數
ushort val1 = 12; ushort val2 = 34U;
uint
32-bit 無符號整數
uint val1 = 12; uint val2 = 34U;
ulong
64-bit 無符號整數
ulong val1 = 12; ulong val2 = 34U; ulong val3 = 56L; ulong val4 = 78UL;
float
32-bit單精度浮點數
float val = 1.23F;
double
64-bit雙精度浮點數
double val1 = 1.23; double val2 = 4.56D;
bool
布爾類型
bool val1 = true; bool val2 = false;
char
字符類型 ,Unicode 編碼
char val = 'h';
decimal
28個有效數字的128-bit十進制類型
decimal val = 1.23M;
引 用類型共分四種類型:類,接口,數組,委派。類除了我們可以定義自己的類型外,又包括兩個比較特殊的類型object和string。object是C# 中所有類型(包括所有的值類型和引用類型)的繼承的根類。string類型是一個密封類型(不能被繼承),其實例表示Unicode字符串,它和數組類型 我們將放在"第九講 結構,枚舉,數組與字符串"中詳述。接口類型定義一個方法的合同,我們將在"第七講 接口 繼承與多態"中講述。委派類型是一個指向靜態或實例方法的簽名,類似於C/C++中的函數指針,將在"第八講 委派與事件"中講述。實際上我們將從後面的專題中看到這些類型都是類的某種形式的包裝。
每種數據類型都有對應的缺省值。數值類型的缺省 值為0或0.0,其中char的缺省為'\x0000'。布爾類型的缺省值為false。枚舉類型的缺省值為0。結構類型的缺省值是將它所有的值類型的域 設置為對應值類型的缺省值,將其引用類型的域設置為null。所有引用類型的缺省值為null。
不同類型的數據之間可以轉換,C#的類 型轉換有隱含轉換,明晰轉換,標准轉換,自定義轉換共四種方式。隱含轉換與明晰轉換和C++裡一樣,數據從"小類型"到"大類型"的轉換時為隱含轉換,從 "大類型"到"小類型"的轉換為明晰轉換,明晰轉換需要如"(Type)data"一般的括號轉換操作符。標准轉換和自定義轉換是針對系統內建轉換和用戶 定義的轉換而言的,兩者都是對類或結構這樣的自定義類型而言的。
變量與常量
變量表示存儲位置,變量必須有確定的數據類型。C#的類型安全的含義之一就是確保變量的存儲位置容納著合適的類型。可以將C#中的變量分為靜態變量,實例變量,傳值參數,引用參數,輸出參數,數組參數和本地變量共七種。本地變量則是在方法體內的臨時變量。
靜 態變量和實例變量主要是針對類或結構內的數據成員(又叫域)而言的。靜態變量在它寄存的類或結構類型被裝載後得到存儲空間,如果沒有對它進行初始化賦值, 靜態變量的初始值將是它的類型所持有的缺省值。實例變量在它的類實例被創建後獲得存儲空間,如果沒有經過初始化賦值,它的初始值與靜態變量的定義相同。兩 者更詳細的說明我們放在"第六講 域 方法 屬性與索引器"專題裡。
傳值參數,引用參數,輸出參數,數組參數主要針對方法的參數類型而 言的。簡單的講傳值參數是對變量的值的一種傳遞,方法內對變量的改變在方法體外不起作用。對於傳值參數本身是引用型的變量稍有不同,方法內對該引用(句 柄)變量指向的數據成員即實際內存塊的改變將在方法體外仍然保留改變,但對於引用(句柄)本身的改變不起作用。引用參數是對變量的句柄的一種傳遞,方法內 對該變量的任何改變都將在方法體外保留。輸出參數是C#專門為有多個返回值的方法而量身定做的,它類似於引用變量,但可以在進入方法體之前不進行初始化, 而其他的參數在進入方法體內C#都要求明確的初始化。數組參數是為傳遞大量的數組元素而專門設計的,它從本質上講是一種引用型變量的傳值參數。它們更詳細 的闡述我們也放在"第六講 域 方法 屬性與索引器"專題裡。
本地變量嚴格的講是在C#的塊語句,for語句,switch語句,using語句內聲明的變量,它的生命周期嚴格地被限制在這些語句塊內部。
常量在編譯時便確定它的值,在整個程序中也不許修改。常量聲明的同時必須賦值。由於它的編譯時確定值的特性,引用類型可能的值只能為string和null(除string外,引用類型的構建器必須在運行時才能確定引用類型的值)。
操作符與表達式
C# 保留了C++所有的操作符,其中指針操作符(*和->)與引用操作符(&)需要有unsafe的上下文。C#擯棄了范圍辨析操作符 (::),一律改為單點操作符(.)。我們不再闡述那些保留的C++的操作符,這裡主要介紹C#引入的具有特殊意義的幾個操作符:as,is,new, typeof,sizeof,stackalloc。
as操作符用於執行兼容類型之間的轉換,當轉換失敗時,as 操作符結果為null。is 操作符用於檢查對象的運行時類型是否與給定類型兼容,當表達式非null且可以轉化為指定類型時,is操作符結果為true,否則為false。as和 is操作符是基於同樣的類型鑒別和轉換而設計的,兩者有相似的應用場合。實際上expression as type相當於expression is type ? (type)expression : (type)null。
作為操作符的new用於在堆上創建對象和調用構造函數, 值得注意的是值類型對象(例如結構)是在堆棧上創建的,而引用類型對象(例如類)是在堆上創建的。new也用於修飾符,用於隱藏基類成員的繼承成員。為隱 藏繼承的成員,使用相同名稱在派生類中聲明該成員並用 new 修飾符修改它。typeof 運算符用於獲得某一類型的 System.Type 對象,我們將在"第十講 特征與映射"裡結合Microsoft.Net的類型系統對它作詳細的闡述。sizeof 運算符用於獲得值類型(不適用於引用類型)的大小(以字節為單位)。stackalloc用於在堆棧上分配內存塊, 僅在局部變量的初始值設定項中有效,類似於C/C++語言的_alloca。sizeof和statckalloc都由於涉及內存的直接操作而需要 unsafe上下文。
C#裡的某些操作符可以像C++裡那樣被重載。操作符重載使得自定義類型(類或結構)可以用簡單的操作符來方便的表達某些常用的操作。
為完成一個計算結果的一系列操作符和操作數的組合稱為表達式。和C++一樣,C#的表達式可以分為賦值表達式和布爾表達式兩種,C#沒有引入新的表達式形式,我們對此不再贅述。
命名空間與語句
C# 采用命名空間(namespace)來組織程序。命名空間可以嵌套。using指示符可以用來簡化命名空間類型的引用。using指示符有兩種用法。 "using System;"語句可以使我們用簡短的類型名"Console"來代替類型"System.Console"。"using Output = System.Console;"語句可以使我們用別名"Output"來代替類型"System.Console"。命名空間的引入大大簡化了C#程序 的組織方式。
C#語句可以分為標號語句,聲明語句,塊語句,空語句,表達式語句,選擇語句,反復語句,跳轉語句,try語句,checked/unchecked語句,lock語句,using語句。
標 號語句主要為goto跳轉設計,C#不允許跨方法的跳轉,但允許小規模的方法內的跳轉。聲明語句可以同時進行初始化賦值,對象的實例化聲明需要new關鍵 字。塊語句采用"{"和"}"定義語句塊,主要是界定局部變量的作用范圍。空語句在C#中用分號";"表示,沒有執行語義。表達式語句通過表達式構成語 句。
選擇語句有if語句和switch語句兩種,與C++別無二致。反復語句除了while,do,for三種循環結構外引入了foreach語句用於遍歷集合中所有的元素,但這需要特定的接口支持,我們在後面的章節裡對之作詳細闡述。
跳轉語句有break,continue,goto,return,throw五種語句,前四種與C++裡的語義相同,throw語句與後面的try語句我們將在"第十一講 COM互操作 非托管編程與異常處理"闡述。
checked/unchecked語句主要用於數值運算中溢出檢查的上下文。lock語句主要用於線程信號量的鎖控制。using語句主要用於片斷資源管理。這些我們在後續章節裡都會有具體的涉及。
返回頁首
第三講 Microsoft.Net平台基礎構造
拋 開Microsoft.NET平台去談C#是沒有意義的,C#之“Sharp”也正在其後端強大的平台。僅僅拘泥於語法層面是體驗不了C#的銳利之處的, C#程序很多詭秘之處必須依靠Microsoft.NET平台才能深度的掌握和運用。簡單的講,Microsoft.Net平台是一個建立在開放互聯網絡 協議和標准之上,采用新的工具和服務來滿足人們的計算和通信需求的革命性的新型XML Web智能計算服務平台。它允許應用程序在因特網上方便快捷地互相通信,而不必關心使用何種操作系統和編程語言。
從技術層面具體來說, Microsoft.NET平台主要包括兩個內核,即通用語言運行時(Common Language Runtime,簡稱CLR)和Microsoft.NET框架類庫,它們為Microsoft.NET平台的實現提供了底層技術支持。通用語言運行時是 建立在操作系統最底層的服務,為Microsoft.NET平台的執行引擎。Microsoft.NET框架包括一套可被用於任何編程語言的類庫,其目的 是使得程序員更容易地建立基於網絡的應用和服務。在此之上是許多應用程序模板,這些模板為開發網絡應用和服務提供高級的組件和服務。 Microsoft.Net平台之浩瀚絕非這裡的幾千字能夠廓清,我們下面將著重體驗那些對我們用C#開發應用程序至關重要的平台基礎構造。
通用語言運行時(CLR)
通 用語言運行時是整個Microsoft.NET框架賴以建構的基礎,它為Microsoft.NET應用程序提供了一個托管的代碼執行環境。它實際上是駐 留在內存裡的一段代理代碼,負責應用程序在整個執行期間的代碼管理工作,比較典型的有:內存管理,線程管理,安全管理,遠程管理,即時編譯,代碼強制安全 類型檢查等。這些都可稱得上Microsoft.Net框架的生命線。
實際上我們可以看出來,CLR代理了一部分傳統操作系統的管理功 能。在CLR下的代碼稱之為托管代碼,否則稱為非托管代碼。我們也可將CLR看作一個技術規范,無論程序使用什麼語言編寫,只要能編譯成微軟中間語言 (MSIL),就可以在它的支持下運行,這使得應用程序得以獨立於語言。目前支持CLR的編程語言多達二三十種。微軟中間語言是我們在 Microsoft.NET平台下編譯器輸出的PE文件的語言。它是Microsoft.NET平台最完整的語言集,非常類似於PC機上的匯編語言。即時 編譯器在運行時將中間語言編譯成本地二進制代碼。它為Microsoft.NET平台提供了多語言的底層技術支持。另外根據需要, Microsoft.Net即時編譯器提供了特殊情況下的經濟型即時編譯和安裝時編譯技術。
CLR的設計目的便是直接在應用程序運行環 境中為基於組件的編程提供第一等的支持。正如在Windows中添加了對窗口、控件、圖形和菜單的直接支持,為基於消息的編程添加了底層結構,為支持設備 無關性添加了抽象內容一樣,CLR直接支持組件(包括屬性和事件)、對象、繼承性、多態性和接口。對屬性和事件的直接支持使得基於組件的編程變得更簡單, 而不需要特殊的接口和適配設計模式。在組件運行時,CLR負責管理內存分配、啟動和中止線程和進程、強化安全系數,同時還調整任何該組件涉及到的其他組件 的附屬配置。序列化支持允許以多種格式操作存儲在磁盤上的組件,包括基於業界標准XML的SOAP。CLR提供了處理錯誤條件的有力、協調的方式。每個模 塊都具有內置的完整的元數據,這意味著諸如動態創建和方法調用之類的功能更容易,也更安全。映射甚至允許我們靈活地創建和執行代碼。我們可以控制應用程序 使用的組件的版本,這使應用程序更加可靠。組件代碼是與處理器無關的和易於驗證的中間語言 ( IL),而不是某一種特定的機器語言,這意味著組件不但可以在多種計算機上運行,而且可以確保組件不會覆蓋它們不使用的內存,也不會潛在地導致系統崩潰。 CLR根據托管組件的來源(例如來自因特網,企業局域網,本地機)等因素對他們判定以適當的信任度,這樣CLR會根據他們的信任度來限定他們執行如讀取文 件,修改注冊表等某些敏感操作的權限。借助通用類型系統(Common Type System,簡稱CTS)對代碼類型進行嚴格的安全檢查避免了不同組件之間可能存在的類型不匹配的問題。CLR下的編程全部是圍繞組件進行的。
值得指出的是CLR通常寄宿在其他高性能的服務器應用程序中,比如:因特網信息服務器(IIS),Microsoft SQL Server。這使得我們可以充分利用通用語言運行時諸多的安全,高效的優點來部署自己的商業邏輯。
內存管理
CLR 對程序員影響最大的就是它的內存管理功能,以至於我們很有必要單獨把它列出來闡述。它為應用程序提供了高性能的垃圾收集環境。垃圾收集器自動追蹤應用程序 操作的對象,程序員再也用不著和復雜的內存管理打交道。這在某些喜歡張口閉口底層編程的所謂的高手來說,自動內存管理從來都是他們嘲笑的對象。的確,為通 用軟件環境設計的自動化內存管理器永遠都抵不上自己為特定程序量身訂制的手工制作。但現代軟件業早已不再是幾百行代碼的作坊作業,動辄成千上萬行的代碼, 大量的商業邏輯凸現的已不再是算法的靈巧,而是可管理性,可維護性的工程代碼。.NET/C#不是為那樣的作坊高手准備的,C語言才是他們的尤物。在 Microsoft.NET托管環境下,CLR負責處理對象的內存布局,管理對象的引用,釋放系統不再使用的內存(自動垃圾收集)。這從根本上解決了長期 以來困擾軟件的內存洩漏和無效內存引用問題,大大減輕了程序員的開發負擔,提高了程序的健壯性。實際上我們在托管環境下根本找不到關於內存操作或釋放的語 言指令。值得指出的是Microsoft.Net應用程序可以使用托管數據,也可以使用非托管數據,但CLR並不能判斷托管數據與非托管數據。
垃 圾收集器負責管理.NET應用程序內存的分配和釋放。當用new操作符創建新的對象時,垃圾收集器在托管堆(Managed Heap)中為對象分配內存資源。只要托管堆內的內存空間可用,垃圾收集器就為每一個新創建的對象分配內存。當應用程序不再持有某個對象的引用,垃圾收集 器將會探測到並釋放該對象。值得注意的是垃圾收集器並不是在對象引用無效時就立即開始釋放工作,而是根據一定算法來決定什麼時候進行收集和對什麼對象進行 收集。任何一個機器的內存資源總是有限的,當托管堆內的內存空間不夠用時,垃圾收集器啟動收集線程來釋放系統內存。垃圾收集器根據對象的存活時間,對象歷 經的收集次數等來決定對哪些對象的內存進行釋放。宏觀的看,我們並不知道垃圾收集的確切行為,但Microsoft.Net類庫為我們提供了控制垃圾收集 行為的部分功能,在某些特殊情況下,我們有必要進行一些受限的操作。
垃圾收集器並不意味著程序員從此可以一勞永逸,如果正在操作一個包裝了如文件,網絡連接,Windows句柄,位圖等底層操作系統資源的對象,我們還是需要明確地釋放這些非托管資源的。這在“第五講 構造器與析構器”裡有詳細的闡述。
Microsoft.Net框架類庫
Microsoft.NET 框架類庫是一組廣泛的,面向對象的可重用類的集合,為應用程序提供各種高級的組件和服務。它將程序員從繁重的編程細節中解放出來專注於程序的商業邏輯,為 應用程序提供各種開發支持--不管是傳統的命令行程序還是Windows圖形界面程序,擬或是面向下一代因特網分布式計算平台的ASP.Net或XML Web服務。下面是對這些組件和服務的一個概括。
• 系統框架服務
服務框架包括一套開發人員希望在標准語言庫中存在的基類庫,例如:集合、輸入/輸出,字符串及數據類。另外,基類庫提供訪問操作系統服務如圖畫、網絡、線程、全球化和加密的類。服務框架也包括數據訪問類庫,及開發工具,如調試和剖析服務,能夠使用的類。
• ADO.Net組件
ADO.NET為基於網絡的可擴展的應用程序和服務提供數據訪問服務。ADO.Net不僅支持傳統的基於連接指針風格的數據訪問,同時也為更適合於把數據返回到客戶端應用程序的無連接的數據模板提供高性能的訪問支持。
• XML數據組件
所有的數據都可被看作XML,開發人員可以通過XML為任何數據使用轉換,傳輸和確認服務。系統框架對XML數據提供第一等的操作支持。系統也支持ADO.Net數據與XML數據之間的通用轉換。
• Windows表單組件
Windows表單組件為開發人員提供了強大的Windows應用程序模型和豐富的Windows用戶接口,包括傳統的ActiveX控件和Windows XP的新界面,如透明的、分層的、浮動窗口。對設計時的強大支持也是Windows表單組件令人興奮的地方。
• ASP.Net應用服務
ASP.NET 的核心是高性能的用於處理基於低級結構的HTTP請求的運行語言。編譯運行方式大大提高了它的性能。ASP.NET使用基於構件的Microsoft .Net框架配制模板,因此它獲得了如XCOPY配制、構件並行配制、基於XML配制等優點。它支持應用程序的實時更新,提供高速緩沖服務改善性能。
• ASP.Net Web表單
ASP.NET Web表單把基於VB的表單的高生產性的優點帶到了網絡應用程序的開發中來。ASP.NET Web表單支持傳統的將HTML內容與角本代碼混合的ASP語法,但是它提出了一種將應用程序代碼和用戶接口內容分離的更加結構化的方法。ASP.NET 提供了一套映射傳統的Html用戶接口部件(包括列表框,文本框和按鈕)的ASP.Net Web表單控件和一套更加復雜強大的網絡應用控件(如日歷和廣告轉板)。
• XML Web服務
ASP.NET應用服務 體系架構為用ASP.NET建立XML Web服務提供了一個高級的可編程模板。雖然建立XML Web服務並不限定使用特定的服務平台,但是它提供許多的優點將簡化開發過程。使用這個編程模型,開發人員甚至不需要理解HTTP、SOAP或其它任何網 絡服務規范。 ASP.Net XML Web服務為在Internet上綁定應用程序提供了一個利用現存體系架構和應用程序的簡單的、靈活的、基於產業標准的模型。
返回頁首
第四講 類與對象
組件編程不是對傳統面向對象的拋棄,相反組件編程正是面向對象編程的深化和發展。類作為面向對象的靈魂在C#語言裡有著相當廣泛深入的應用,很多非常“Sharp”的組件特性甚至都是直接由類包裝而成。對類的深度掌握自然是我們“Sharp XP”重要的一環。
類
C# 的類是一種對包括數據成員,函數成員和嵌套類型進行封裝的數據結構。其中數據成員可以是常量,域。函數成員可以是方法,屬性,索引器,事件,操作符,實例 構建器,靜態構建器,析構器。我們將在“第五講 構造器與析構器”和“第六講 域 方法 屬性與索引器”對這些成員及其特性作詳細的剖析。除了某些導入的外部方法,類及其成員在C#中的聲明和實現通常要放在一起。
C#用多種修飾符來表達類的不同性質。根據其保護級C#的類有五種不同的限制修飾符:
1.
public可以被任意存取;
2.
protected只可以被本類和其繼承子類存取;
3.
internal只可以被本組合體(Assembly)內所有的類存取,組合體是C#語言中類被組合後的邏輯單位和物理單位,其編譯後的文件擴展名往往是“.DLL”或“.EXE”。
4.
protected internal唯一的一種組合限制修飾符,它只可以被本組合體內所有的類和這些類的繼承子類所存取。
5.
private只可以被本類所存取。
如果不是嵌套的類,命名空間或編譯單元內的類只有public和internal兩種修飾。
new修飾符只能用於嵌套的類,表示對繼承父類同名類型的隱藏。
abstract用來修飾抽象類,表示該類只能作為父類被用於繼承,而不能進行對象實例化。抽象類可以包含抽象的成員,但這並非必須。abstract不能和new同時用。下面是抽象類用法的偽碼:
abstract class A
{
public abstract void F();
}
abstract class B: A
{
public void G() {}
}
class C: B
{
public override void F()
{
//方法F的實現
}
}
抽象類A內含一個抽象方法F(),它不能被實例化。類B繼承自類A,其內包含了一個實例方法G(),但並沒有實現抽象方法F(),所以仍然必須聲明為抽象類。類C繼承自類B,實現類抽象方法F(),於是可以進行對象實例化。
sealed用來修飾類為密封類,阻止該類被繼承。同時對一個類作abstract和sealed的修飾是沒有意義的,也是被禁止的。
對象與this關鍵字
類 與對象的區分對我們把握OO編程至關重要。我們說類是對其成員的一種封裝,但類的封裝設計僅僅是我們編程的第一步,對類進行對象實例化,並在其數據成員上 實施操作才是我們完成現實任務的根本。實例化對象采用MyClass myObject=new MyClass()語法,這裡的new語義將調用相應的構建器。C#所有的對象都將創建在托管堆上。實例化後的類型我們稱之為對象,其核心特征便是擁有了 一份自己特有的數據成員拷貝。這些為特有的對象所持有的數據成員我們稱之為實例成員。相反那些不為特有的對象所持有的數據成員我們稱之為靜態成員,在類中 用static修飾符聲明。僅對靜態數據成員實施操作的稱為靜態函數成員。C#中靜態數據成員和函數成員只能通過類名引用獲取,看下面的代碼:
using System;
class A
{
public int count;
public void F()
{
Console.WriteLine(this.count);
}
public static string name;
public static void G()
{
Console.WriteLine(name);
}
}
class Test
{
public static void Main()
{
A a1=new A();
A a2=new A();
a1.F();
a1.count=1;
a2.F();
a2.count=2;
A.name="CCW";
A.G();
}
}
我們聲明了兩個A對象a1,a2。對於實例成員count和F(),我們只能通過a1,a2引用。對於靜態成員name和G()我們只能通過類型A來引用,而不可以這樣a1.name,或a1.G()。
在 上面的程序中,我們看到在實例方法F()中我們才用this來引用變量count。這裡的this是什麼意思呢?this 關鍵字引用當前對象實例的成員。在實例方法體內我們也可以省略this,直接引用count,實際上兩者的語義相同。理所當然的,靜態成員函數沒有 this 指針。this 關鍵字一般用於從構造函數、實例方法和實例訪問器中訪問成員。
在構造函數中this用於限定被相同的名稱隱藏的成員,例如:
class Employee
{
public Employee(string name, string alias)
{
this.name = name;
this.alias = alias;
}
}
將對象作為參數傳遞到其他方法時也要用this表達,例如:
CalcTax(this);
聲明索引器時this更是不可或缺,例如:
public int this [int param]
{
get
{
return array[param];
}
set
{
array[param] = value;
}
}
System.Object類
C# 中所有的類都直接或間接繼承自System.Object類,這使得C#中的類得以單根繼承。如果我們沒有明確指定繼承類,編譯器缺省認為該類繼承自 System.Object類。System.Object類也可用小寫的object關鍵字表示,兩者完全等同。自然C#中所有的類都繼承了 System.Object類的公共接口,剖析它們對我們理解並掌握C#中類的行為非常重要。下面是僅用接口形式表示的System.Object類:
namespace System
{
public class Object
{
public static bool Equals(object objA,object objB){}
public static bool ReferenceEquals(object objA,object objB){}
public Object(){}
public virtual bool Equals(object obj){}
public virtual int GetHashCode(){}
public Type GetType(){}
public virtual string ToString(){}
protected virtual void Finalize(){}
protected object MemberwiseClone(){}
}
我 們先看object的兩個靜態方法Equals(object objA,object objB),ReferenceEquals(object objA,object objB)和一個實例方法Equals(object obj)。在我們闡述這兩個方法之前我們首先要清楚面向對象編程兩個重要的相等概念:值相等和引用相等。值相等的意思是它們的數據成員按內存位分別相等。 引用相等則是指它們指向同一個內存地址,或者說它們的對象句柄相等。引用相等必然推出值相等。對於值類型關系等號“= =”判斷兩者是否值相等(結構類型和枚舉類型沒有定義關系等號“= =”,我們必須自己定義)。對於引用類型關系等號“= =”判斷兩者是否引用相等。值類型在C#裡通常沒有引用相等的表示,只有在非托管編程中采用取地址符“&”來間接判斷二者的地址是否相等。
靜 態方法Equals(object objA,object objB)首先檢查兩個對象objA和objB是否都為null,如果是則返回true,否則進行objA.Equals(objB)調用並返回其值。問 題歸結到實例方法Equals(object obj)。該方法缺省的實現其實就是{return this= =obj;}也就是判斷兩個對象是否引用相等。但我們注意到該方法是一個虛方法,C#推薦我們重寫此方法來判斷兩個對象是否值相等。實際上 Microsoft.Net框架類庫內提供的許多類型都重寫了該方法,如:System.String(string),System.Int32 (int)等,但也有些類型並沒有重寫該方法如:System.Array等,我們在使用時一定要注意。對於引用類型,如果沒有重寫實例方法Equals (object obj),我們對它的調用相當於this= =obj,即引用相等判斷。所有的值類型(隱含繼承自System.ValueType類)都重寫了實例方法Equals(object obj)來判斷是否值相等。
注意對於對象x,x.Equals(null)返回false,這裡x顯然不能為null(否則不能完成 Equals()調用,系統拋出空引用錯誤)。從這裡我們也可看出設計靜態方法Equals(object objA,object objB)的原因了--如果兩個對象objA和objB都可能為null,我們便只能用object. Equals(object objA,object objB)來判斷它們是否值相等了--當然如果我們沒有改寫實例方法Equals(object obj),我們得到的仍是引用相等的結果。我們可以實現接口IComparable(有關接口我們將在“第七講 接口 繼承與多態”裡闡述)來強制改寫實例方法Equals(object obj)。
對於值類型,實例方法Equals(object obj)應該和關系等號“= =”的返回值一致,也就是說如果我們重寫了實例方法Equals(object obj),我們也應該重載或定義關系等號“= =”操作符,反之亦然。雖然值類型(繼承自System.ValueType類)都重寫了實例方法Equals(object obj),但C#推薦我們重寫自己的值類型的實例方法Equals(object obj),因為系統的System.ValueType類重寫的很低效。對於引用類型我們應該重寫實例方法Equals(object obj)來表達值相等,一般不應該重載關系等號“= =”操作符,因為它的缺省語義是判斷引用相等。
靜態方法ReferenceEquals(object objA,object objB)判斷兩個對象是否引用相等。如果兩個對象為引用類型,那麼它的語義和沒有重載的關系等號“= =”操作符相同。如果兩個對象為值類型,那麼它的返回值一定是false。
實 例方法GetHashCode()為相應的類型提供哈希(hash)碼值,應用於哈希算法或哈希表中。需要注意的是如果我們重寫了某類型的實例方法 Equals(object obj),我們也應該重寫實例方法GetHashCode()--這理所應當,兩個對象的值相等,它們的哈希碼也應該相等。下面的代碼是對前面幾個方法的 一個很好的示例:
using System;
struct A
{
public int count;
}
class B
{
public int number;
}
class C
{
public int integer=0;
public override bool Equals(object obj)
{
C c=obj as C;
if (c!=null)
return this.integer==c.integer;
else
return false;
}
public override int GetHashCode()
{
return 2^integer;
}
}
class Test
{
public static void Main()
{
A a1,a2;
a1.count=10;
a2=a1;
//Console.Write(a1==a2);沒有定義“= =”操作符
Console.Write(a1.Equals(a2));//True
Console.WriteLine(object.ReferenceEquals(a1,a2));//False
B b1=new B();
B b2=new B();
b1.number=10;
b2.number=10;
Console.Write(b1==b2);//False
Console.Write(b1.Equals(b2));//False
Console.WriteLine(object.ReferenceEquals(b1,b2));//False
b2=b1;
Console.Write(b1==b2);//True
Console.Write(b1.Equals(b2));//True
Console.WriteLine(object.ReferenceEquals(b1,b2));//True
C c1=new C();
C c2=new C();
c1.integer=10;
c2.integer=10;
Console.Write(c1==c2);//False
Console.Write(c1.Equals(c2));//True
Console.WriteLine(object.ReferenceEquals(c1,c2));//False
c2=c1;
Console.Write(c1==c2);//True
Console.Write(c1.Equals(c2));//True
Console.WriteLine(object.ReferenceEquals(c1,c2));//True
}
}
如我們所期望,編譯程序並運行我們會得到以下輸出:
True False
False False False
True True True
False True False
True True True
實例方法GetType()與typeof的語義相同,它們都通過查詢對象的元數據來確定對象的運行時類型,我們在“第十講 特征與映射”對此作詳細的闡述。
實例方法ToString()返回對象的字符串表達形式。如果我們沒有重寫該方法,系統一般將類型名作為字符串返回。
受保護的Finalize()方法在C#中有特殊的語義,我們將在“第五講 構造器與析構器”裡詳細闡述。
受 保護的MemberwiseClone()方法返回目前對象的一個“影子拷貝”,該方法不能被子類重寫。“影子拷貝”僅僅是對象的一份按位拷貝,其含義是 對對象內的值類型變量進行賦值拷貝,對其內的引用類型變量進行句柄拷貝,也就是拷貝後的引用變量將持有對同一塊內存的引用。相對於“影子拷貝”的是深度拷 貝,它對引用類型的變量進行的是值復制,而非句柄復制。例如X是一個含有對象A,B引用的對象,而對象A又含有對象M的引用。Y是X的一個“影子拷貝”。 那麼Y將擁有同樣的A,B的引用。但對於X的一個“深度拷貝”Z來說,它將擁有對象C和D的引用,以及一個間接的對象N的引用,其中C是A的一份拷貝,D 是B的一份拷貝,N是M的一份拷貝。深度拷貝在C#裡通過實現ICloneable接口(提供Clone()方法)來完成。
對對象和System.Object的把握為類的學習作了一個很好的鋪墊,但這僅僅是我們銳利之行的一小步,關乎對象成員初始化,內存引用的釋放,繼承與多態,異常處理等等諸多“Sharp”特技堪為浩瀚,讓我們繼續期待下面的專題!
返回頁首
第五講 構造器與析構器
構造器
構 造器負責類中成員變量(域)的初始化。C#的類有兩種構造器:實例構造器和靜態構造器。實例構造器負責初始化類中的實例變量,它只有在用戶用new關鍵字 為對象分配內存時才被調用。而且作為引用類型的類,其實例化後的對象必然是分配在托管堆(Managed Heap)上。這裡的托管的意思是指該內存受.Net的CLR運行時管理。和C++不同的是,C#中的對象不可以分配在棧中,用戶只聲明對象是不會產生構 造器調用的。
實例構造器分為缺省構造器和非缺省構造器。缺省構造器是在一個類沒有聲明任何構造器的情況下,編譯器強制為該類添加的一個 無參數的構造器,該構造器僅僅調用父類的無參數構造器。缺省構造器實際上是C#編譯器為保證每一個類都有至少一個構造器而采取的附加規則。注意這裡的三個 要點:
1.
子類沒有聲明任何構造器;
2.
編譯器為子類加的缺省構造器一定為無參數的構造器;
3.
父類一定要存在一個無參數的構造器。
看下面例子的輸出:
using System;
public class MyClass1
{
public MyClass1()
{
Console.WriteLine(“MyClass1
Parameterless Contructor!”);
}
public MyClass1(string param1)
{
Console.WriteLine(“MyClass1
Constructor Parameters : ”+param1);
}
}
public class MyClass2:MyClass1
{
}
public class Test
{
public static void Main()
{
MyClass2 myobject1=new MyClass2();
}
}
編譯程序並運行可以得到下面的輸出:
MyClass1 Parameterless Contructor!
讀者可以去掉MyClass1的無參構造器public MyClass1()看看編譯結果。
構 造器在繼承時需要特別的注意,為了保證父類成員變量的正確初始化,子類的任何構造器默認的都必須調用父類的某一構造器,具體調用哪個構造器要看構造器的初 始化參數列表。如果沒有初始化參數列表,那麼子類的該構造器就調用父類的無參數構造器;如果有初始化參數列表,那麼子類的該構造器就調用父類對應的參數構 造器。看下面例子的輸出:
using System;
public class MyClass1
{
public MyClass1()
{
Console.WriteLine("MyClass1 Parameterless Contructor!");
}
public MyClass1(string param1)
{
Console.WriteLine("MyClass1
Constructor Parameters : "+param1);
}
}
public class MyClass2:MyClass1
{
public MyClass2(string param1):base(param1)
{
Console.WriteLine("MyClass2
Constructor Parameters : "+param1);
}
}
public class Test
{
public static void Main()
{
MyClass2 myobject1=new MyClass2("Hello");
}
}
編譯程序並運行可以得到下面的輸出:
MyClass1 Constructor Parameters : Hello
MyClass2 Constructor Parameters : Hello
C#支持變量的聲明初始化。類內的成員變量聲明初始化被編譯器轉換成賦值語句強加在類的每一個構造器的內部。那麼初始化語句與調用父類構造器的語句的順序是什麼呢?看下面例子的輸出:
using System;
public class MyClass1
{
public MyClass1()
{
Print();
}
public virtual void Print() {}
}
public class MyClass2: MyClass1
{
int x = 1;
int y;
public MyClass2()
{
y = -1;
Print();
}
public override void Print()
{
Console.WriteLine("x = {0}, y = {1}", x, y);
}
}
public class Test
{
static void Main()
{
MyClass2 MyObject1 = new MyClass2();
}
}
編譯程序並運行可以得到下面的輸出:
x = 1, y = 0
x = 1, y = -1
容易看到初始化語句在父類構造器調用之前,最後執行的才是本構造器內的語句。也就是說變量初始化的優先權是最高的。
我 們看到類的構造器的聲明中有public修飾符,那麼當然也可以有protected/private/ internal修飾符。根據修飾符規則,我們如果將一個類的構造器修飾為private,那麼我們在繼承該類的時候,我們將不能對這個private的 構造器進行調用,我們是否就不能對它進行繼承了嗎?正是這樣。實際上這樣的類在我們的類內的成員變量都是靜態(static)時,而又不想讓類的用戶對它 進行實例化,這時必須屏蔽編譯器為我們暗中添加的構造器(編譯器添加的構造器都為public),就很有必要作一個private的實例構造器了。 protected/internal也有類似的用法。
類的構造器沒有返回值,這一點是不言自明的。
靜態構造器初始化類中的靜態變量。靜態構造器不象實例構造器那樣在繼承中被隱含調用,也不可以被用戶直接調用。掌握靜態構造器的要點是掌握它的執行時間。靜態構造器的執行並不確定(編譯器沒有明確定義)。但有四個准則需要掌握:
1.
在一個程序的執行過程中,靜態構造器最多只執行一次。
2.
靜態構造器在類的靜態成員初始化之後執行。或者講編譯器會將靜態成員初始化語句轉換成賦值語句放在靜態構造器執行的最開始。
3.
靜態構造器在任何類的靜態成員被引用之前執行。
4.
靜態構造器在任何類的實例變量被分配之前執行。
看下面例子的輸出:
using System;
class MyClass1
{
static MyClass1()
{
Console.WriteLine("MyClass1 Static Contructor");
}
public static void Method1()
{
Console.WriteLine("MyClass1.Method1");
}
}
class MyClass2
{
static MyClass2()
{
Console.WriteLine("MyClass2 Static Contructor");
}
public static void Method1()
{
Console.WriteLine("MyClass2.Method1");
}
}
class Test
{
static void Main()
{
MyClass1.Method1();
MyClass2.Method1();
}
}
編譯程序並運行可以得到下面的輸出:
MyClass1 Static Contructor
MyClass1.Method1
MyClass2 Static Contructor
MyClass2.Method1
當然也可能輸出:
MyClass1 Static Contructor
MyClass2 Static Contructor
MyClass1.Method1
MyClass2.Method1
值得指出的是實例構造器內可以引用實例變量,也可引用靜態變量。而靜態構造器內能引用靜態變量。這在類與對象的語義下是很容易理解的。
實際上如果我們能夠深刻地把握類的構造器的唯一目的就是保證類內的成員變量能夠得到正確的初始化,我們對各種C#中形形色色的構造器便有會心的理解--它沒有理由不這樣!
析構器
由 於.Net平台的自動垃圾收集機制,C#語言中類的析構器不再如傳統C++那麼必要,析構器不再承擔對象成員的內存釋放--自動垃圾收集機制保證內存的回 收。實際上C#中已根本沒有delete操作!析構器只負責回收處理那些非系統的資源,比較典型的如:打開的文件,獲取的窗口句柄,數據庫連接,網絡連接 等等需要用戶自己動手釋放的非內存資源。我們看下面例子的輸出:
using System;
class MyClass1
{
~MyClass1()
{
Console.WriteLine("MyClass1's destructor");
}
}
class MyClass2: MyClass1
{
~MyClass2()
{
Console.WriteLine("MyClass2's destructor");
}
}
public class Test
{
public static void Main()
{
MyClass2 MyObject = new MyClass2();
MyObject = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
編譯程序並運行可以得到下面的輸出:
MyClass2's destructor
MyClass1's destructor
其 中程序中最後兩句是保證類的析構器得到調用。GC.Collect()是強迫通用語言運行時進行啟動垃圾收集線程進行回收工作。而 GC.WaitForPendingFinalizers()是掛起目前的線程等待整個終止化(Finalizaion)操作的完成。終止化 (Finalizaion)操作保證類的析構器被執行,這在下面會詳細說明。
析構器不會被繼承,也就是說類內必須明確的聲明析構器,該 類才存在析構器。用戶實現析構器時,編譯器自動添加調用父類的析構器,這在下面的Finalize方法中會詳細說明。析構器由於垃圾收集機制會被在合適的 的時候自動調用,用戶不能自己調用析構器。只有實例析構器,而沒有靜態析構器。
那麼析構器是怎麼被自動調用的?這在 .Net垃圾回收機制由一種稱作終止化(Finalizaion)的操作來支持。.Net系統缺省的終止化操作不做任何操作,如果用戶需要釋放非受管資 源,用戶只要在析構器內實現這樣的操作即可--這也是C#推薦的做法。我們看下面這段代碼:
using System;
class MyClass1
{
~MyClass1()
{
Console.WritleLine("MyClass1 Destructor");
}
}
而實際上,從生成的中間代碼來看我們可以發現,這些代碼被轉化成了下面的代碼:
using System;
class MyClass1
{
protected override void Finalize()
{
try
{
Console.WritleLine("My Class1 Destructor");
}
finally
{
base.Finalize();
}
}
}
實 際上C#編譯器不允許用戶自己重載或調用Finalize方法--編譯器徹底屏蔽了父類的Finalize方法(由於C#的單根繼承性質, System.Object類是所有類的祖先類,自然每個類都有Finalize方法),好像這樣的方法根本不存在似的。我們看下面的代碼實際上是錯的:
using System;
class MyClass
{
override protected void Finalize() {}// 錯誤
public void MyMethod()
{
this.Finalize();// 錯誤
}
}
但下面的代碼卻是正確的:
using System;
class MyClass
{
public void Finalize()
{
Console.WriteLine("My Class Destructor");
}
}
public class Test
{
public static void Main()
{
MyClass MyObject=new MyClass();
MyObject.Finalize();
}
}
實際上這裡的Finalize方法已經徹底脫離了“終止化操作”的語義,而成為C#語言的一個一般方法了。值得注意的是這也屏蔽了父類System.Object的Finalize方法,所以要格外小心!
終 止化操作在.Net運行時裡有很多限制,往往不被推薦實現。當對一個對象實現了終止器(Finalizer)後,運行時便會將這個對象的引用加入一個稱作 終止化對象引用集的隊列,作為要求終止化的標志。當垃圾收集開始時,若一個對象不再被引用但它被加入了終止化對象引用集的隊列,那麼運行時並不立即對此對 象進行垃圾收集工作,而是將此對象標志為要求終止化操作對象。待垃圾收集完成後,終止化線程便會被運行時喚醒執行終止化操作。顯然這之後要從終止化對象引 用集的鏈表中將之刪去。而只有到下一次的垃圾收集時,這個對象才開始真正的垃圾收集,該對象的內存資源才被真正回收。容易看出來,終止化操作使垃圾收集進 行了兩次,這會給系統帶來不小的額外開銷。終止化是通過啟用線程機制來實現的,這有一個線程安全的問題。.Net運行時不能保證終止化執行的順序,也就是 說如果對象A有一個指向對象B的引用,兩個對象都有終止化操作,但對象A在終止化操作時並不一定有有效的對象A引用。.Net運行時不允許用戶在程序運行 中直接調用Finalize()方法。如果用戶迫切需要這樣的操作,可以實現IDisposable接口來提供公共的Dispose()方法。需要說明的 是提供了Dispose()方法後,依然需要提供Finalize方法的操作,即實現假托的析構函數。因為Dispose()方法並不能保證被調用。所 以.Net運行時不推薦對對象進行終止化操作即提供析構函數,只是在有非受管資源如數據庫的連接,文件的打開等需要嚴格釋放時,才需要這樣做。
大 多數時候,垃圾收集應該交由.Net運行時來控制,但有些時候,可能需要人為地控制一下垃圾回收操作。例如在操作了一次大規模的對象集合後,我們確信不再 在這些對象上進行任何的操作了,那我們可以強制垃圾回收立即執行,這通過調用System.GC.Collect() 方法即可實現,但頻繁的收集會顯著地降低系統的性能。還有一種情況,已經將一個對象放到了終止化對象引用集的鏈上了,但如果我們在程序中某些地方已經做了 終止化的操作,即明確調用了Dispose()方法,在那之後便可以通過調用System.GC.SupressFinalize()來將對象的引用從終 止化對象引用集鏈上摘掉,以忽略終止化操作。終止化操作的系統負擔是很重的。
在深入了解了.Net運行時的自動垃圾收集功能後,我們便會領會C#中的析構器為什麼繞了這麼大的彎來實現我們的編程需求,才能把內存資源和非內存資源的回收做的游刃有余--這也正是析構的本原!
返回頁首
第六講 方法
方 法又稱成員函數(Member Function),集中體現了類或對象的行為。方法同樣分為靜態方法和實例方法。靜態方法只可以操作靜態域,而實例方法既可以操作實例域,也可以操作靜 態域--雖然這不被推薦,但在某些特殊的情況下會顯得很有用。方法也有如域一樣的5種存取修飾符--public,protected, internal,protected internal,private,它們的意義如前所述。
方法參數
方法的參數是個值得特 別注意的地方。方法的參數傳遞有四種類型:傳值(by value),傳址(by reference),輸出參數(by output),數組參數(by array)。傳值參數無需額外的修飾符,傳址參數需要修飾符ref,輸出參數需要修飾符out,數組參數需要修飾符params。傳值參數在方法調用過 程中如果改變了參數的值,那麼傳入方法的參數在方法調用完成以後並不因此而改變,而是保留原來傳入時的值。傳址參數恰恰相反,如果方法調用過程改變了參數 的值,那麼傳入方法的參數在調用完成以後也隨之改變。實際上從名稱上我們可以清楚地看出兩者的含義--傳值參數傳遞的是調用參數的一份拷貝,而傳址參數傳 遞的是調用參數的內存地址,該參數在方法內外指向的是同一個存儲位置。看下面的例子及其輸出:
using System;
class Test
{
static void Swap(ref int x, ref int y)
{
int temp = x;
x = y;
y = temp;
}
static void Swap(int x,int y)
{
int temp = x;
x = y;
y = temp;
}
static void Main()
{
int i = 1, j = 2;
Swap(ref i, ref j);
Console.WriteLine("i = {0}, j = {1}", i, j);
Swap(i,j);
Console.WriteLine("i = {0}, j = {1}", i, j);
}
}
程序經編譯後執行輸出:
i = 2, j = 1
i = 2, j = 1
我們可以清楚地看到兩個交換函數Swap()由於參數的差別--傳值與傳址,而得到不同的調用結果。注意傳址參數的方法調用無論在聲明時還是調用時都要加上ref修飾符。
籠統地說傳值不會改變參數的值在有些情況下是錯誤的,我們看下面一個例子:
using System;
class Element
{
public int Number=10;
}
class Test
{
static void Change(Element s)
{
s.Number=100;
}
static void Main()
{
Element e=new Element();
Console.WriteLine(e.Number);
Change(e);
Console.WriteLine(e.Number);
}
}
程序經編譯後執行輸出:
10
100
我們看到即使傳值方式仍然改變了類型為Element類的對象t。但嚴格意義上講,我們是改變了對象t的域,而非對象t本身。我們再看下面的例子:
using System;
class Element
{
public int Number=10;
}
class Test
{
static void Change(Element s)
{
Element r=new Element();
r.Number=100;
s=r;
}
static void Main()
{
Element e=new Element();
Console.WriteLine(e.Number);
Change(e);
Console.WriteLine(e.Number);
}
}
程序經編譯後執行輸出:
10
10
傳 值方式根本沒有改變類型為Element類的對象t!實際上,如果我們能夠理解類這一C#中的引用類型(reference type)的特性,我們便能看出上面兩個例子差別!在傳值過程中,引用類型本身不會改變(t不會改變),但引用類型內含的域卻會改變(t.Number改 變了)!C#語言的引用類型有:object類型(包括系統內建的class類型和用戶自建的class類型--繼承自object類型),string 類型,interface類型,array類型,delegate類型。它們在傳值調用中都有上面兩個例子展示的特性。
在傳值和傳址情 況下,C#強制要求參數在傳入之前由用戶明確初始化,否則編譯器報錯!但我們如果有一個並不依賴於參數初值的函數,我們只是需要函數返回時得到它的值是該 怎麼辦呢?往往在我們的函數返回值不至一個時我們特別需要這種技巧。答案是用out修飾的輸出參數。但需要記住輸出參數與通常的函數返回值有一定的區別: 函數返回值往往存在堆棧裡,在返回時彈出;而輸出參數需要用戶預先制定存儲位置,也就是用戶需要提前聲明變量--當然也可以初始化。看下面的例子:
using System;
class Test
{
static void ResoluteName(string fullname,out string firstname,out string lastname)
{
string[] strArray=fullname.Split(new char[]{' '});
firstname=strArray[0];
lastname=strArray[1];
}
public static void Main()
{
string MyName="CornfIEld Lee";
string MyFirstName,MyLastName;
ResoluteName(MyName,out MyFirstName,out MyLastName);
Console.WriteLine("My first name: {0}, My last name: {1}",
MyFirstName, MyLastName);
}
}
程序經編譯後執行輸出:
My first name: CornfIEld, My last name: Lee
在 函數體內所有輸出參數必須被賦值,否則編譯器報錯!out修飾符同樣應該應用在函數聲明和調用兩個地方,除了充當返回值這一特殊的功能外,out修飾符 ref修飾符有很相似的地方:傳址。我們可以看出C#完全擯棄了傳統C/C++語言賦予程序員莫大的自由度,畢竟C#是用來開發高效的下一代網絡平台,安 全性--包括系統安全(系統結構的設計)和工程安全(避免程序員經常犯的錯誤)是它設計時的重要考慮,當然我們看到C#並沒有因為安全性而喪失多少語言的 性能,這正是C#的卓越之處,“Sharp”之處!
數組參數也是我們經常用到的一個地方--傳遞大量的數組集合參數。我們先看下面的例子:
using System;
class Test
{
static int Sum(params int[] args)
{
int s=0;
foreach(int n in args)
{
s+=n;
}
return s;
}
static void Main()
{
int[] var=new int[]{1,2,3,4,5};
Console.WriteLine("The Sum:"+Sum(var));
Console.WriteLine("The Sum:"+Sum(10,20,30,40,50));
}
}
程序經編譯後執行輸出:
The Sum:15
The Sum:150
可以看出,數組參數可以是數組如:var,也可以是能夠隱式轉化為數組的參數如:10,20,30,40,50。這為我們的程序提供了很高的擴展性。
同名方法參數的不同會導致方法出現多態現象,這又叫重載(overloading)方法。需要指出的是編譯器是在編譯時便綁定了方法和方法調用。只能通過參數的不同來重載方法,其他的不同(如返回值)不能為編譯器提供有效的重載信息。
方法繼承
第 一等的面向對象機制為C#的方法引入了virtual,override,sealed,abstract四種修飾符來提供不同的繼承需求。類的虛方法是 可以在該類的繼承自類中改變其實現的方法,當然這種改變僅限於方法體的改變,而非方法頭(方法聲明)的改變。被子類改變的虛方法必須在方法頭加上 override來表示。當一個虛方法被調用時,該類的實例--亦即對象的運行時類型(run-time type)來決定哪個方法體被調用。我們看下面的例子:
using System;
class Parent
{
public void F() { Console.WriteLine("Parent.F"); }
public virtual void G() { Console.WriteLine("Parent.G"); }
}
class Child: Parent
{
new public void F() { Console.WriteLine("Child.F"); }
public override void G() { Console.WriteLine("Child.G"); }
}
class Test
{
static void Main()
{
Child b = new Child();
Parent a = b;
a.F();
b.F();
a.G();
b.G();
}
}
程序經編譯後執行輸出:
Parent.F
Child.F
Child.G
Child.G
我 們可以看到class Child中F()方法的聲明采取了重寫(new)的辦法來屏蔽class Parent中的非虛方法F()的聲明。而G()方法就采用了覆蓋(override)的辦法來提供方法的多態機制。需要注意的是重寫(new)方法和覆 蓋(override)方法的不同,從本質上講重寫方法是編譯時綁定,而覆蓋方法是運行時綁定。值得指出的是虛方法不可以是靜態方法--也就是說不可以用 static和virtual同時修飾一個方法,這由它的運行時類型辨析機制所決定。override必須和virtual配合使用,當然也不能和 static同時使用。
那麼我們如果在一個類的繼承體系中不想再使一個虛方法被覆蓋,我們該怎樣做呢?答案是sealed override (密封覆蓋),我們將sealed和override同時修飾一個虛方法便可以達到這種目的:sealed override public void F()。注意這裡一定是sealed和override同時使用,也一定是密封覆蓋一個虛方法,或者一個被覆蓋(而不是密封覆蓋)了的虛方法。密封一個非 虛方法是沒有意義的,也是錯誤的。看下面的例子:
//sealed.cs
// csc /t:library sealed.cs
using System;
class Parent
{
public virtual void F()
{
Console.WriteLine("Parent.F");
}
public virtual void G()
{
Console.WriteLine("Parent.G");
}
}
class Child: Parent
{
sealed override public void F()
{
Console.WriteLine("Child.F");
}
override public void G()
{
Console.WriteLine("Child.G");
}
}
class Grandson: Child
{
override public void G()
{
Console.WriteLine("Grandson.G");
}
}
抽 象(abstract)方法在邏輯上類似於虛方法,只是不能像虛方法那樣被調用,而只是一個接口的聲明而非實現。抽象方法沒有類似於{…}這樣的方法實 現,也不允許這樣做。抽象方法同樣不能是靜態的。含有抽象方法的類一定是抽象類,也一定要加abstract類修飾符。但抽象類並不一定要含有抽象方法。 繼承含有抽象方法的抽象類的子類必須覆蓋並實現(直接使用override)該方法,或者組合使用abstract override使之繼續抽象,或者不提供任何覆蓋和實現。後兩者的行為是一樣的。看下面的例子:
//abstract1.cs
// csc /t:library abstract1.cs
using System;
abstract class Parent
{
public abstract void F();
public abstract void G();
}
abstract class Child: Parent
{
public abstract override void F();
}
abstract class Grandson: Child
{
public override void F()
{
Console.WriteLine("Grandson.F");
}
public override void G()
{
Console.WriteLine("Grandson.G");
}
}
抽象方法可以抽象一個繼承來的虛方法,我們看下面的例子:
//abstract2.cs
// csc /t:library abstract2.cs
using System;
class Parent
{
public virtual void Method()
{
Console.WriteLine("Parent.Method");
}
}
abstract class Child: Parent
{
public abstract override void Method();
}
abstract class Grandson: Child
{
public override void Method()
{
Console.WriteLine("Grandson.Method");
}
}
歸根結底,我們抓住了運行時綁定和編譯時綁定的基本機理,我們便能看透方法呈現出的種種overload,virtual,override,sealed,abstract等形態,我們才能運用好方法這一利器!
外部方法
C#引入了extern修飾符來表示外部方法。外部方法是用C#以外的語言實現的方法如Win32 API函數。如前所是外部方法不能是抽象方法。我們看下面的一個例子:
using System;
using System.Runtime.InteropServices;
class MyClass
{
[DllImport("user32.dll")]
static extern int MessageBoxA(int hWnd, string msg,string caption, int type);
public static void Main()
{
MessageBoxA(0, "Hello, World!", "This is called from a C# app!", 0);
}
}
程序經編譯後執行輸出:
這裡我們調用了Win32 API函數int MessageBoxA(int hWnd, string msg,string caption, int type)。
返回頁首
第七講 域與屬性
域
域(FIEld) 又稱成員變量(Member Variable),它表示存儲位置,是C#中類不可缺少的一部分。域的類型可以是C#中任何數據類型。但對於除去string類型的其他引用類型由於在 初始化時涉及到一些類的構造器的操作,我們這裡將不提及,我們把這一部分內容作為“類的嵌套”放在“接口 繼承與多態”一講內來闡述。
域 分為實例域和靜態域。實例域屬於具體的對象,為特定的對象所專有。靜態域屬於類,為所有對象所共用。C#嚴格規定實例域只能通過對象來獲取,靜態域只能通 過類來獲取。例如我們有一個類型為MyClass的對象MyObject,MyClass內的實例域instanceField(存取限制為 public)只能這樣獲取:MyObject. instanceField。而MyClass的靜態域staticField(存取限制為public)只能這樣獲取: MyClass.staticField。注意靜態域不能像傳統C++那樣通過對象獲取,也就是說MyObject.staticFIEld的用法是錯誤 的,不能通過編譯器編譯。
域的存取限制集中體現了面向對象編程的封裝原則。如前所述,C#中的存取限制修飾符有5種,這5種對域都適 用。C#只是用internal擴展了C++原來的frIEnd修飾符。在有必要使兩個類的某些域互相可見時,我們將這些類的域聲明為internal, 然後將它們放在一個組合體內編譯即可。如果需要對它們的繼承子類也可見的話,聲明為protected internal即可。實際上這也是組合體的本來意思--將邏輯相關的類組合封裝在一起。
C#引入了readonly修飾符來表示只讀 域,const來表示不變常量。顧名思義對只讀域不能進行寫操作,不變常量不能被修改,這兩者到底有什麼區別呢?只讀域只能在初始化--聲明初始化或構造 器初始化--的過程中賦值,其他地方不能進行對只讀域的賦值操作,否則編譯器會報錯。只讀域可以是實例域也可以是靜態域。只讀域的類型可以是C#語言的任 何類型。但const修飾的常量必須在聲明的同時賦值,而且要求編譯器能夠在編譯時期計算出這個確定的值。const修飾的常量為靜態變量,不能夠為對象 所獲取。const修飾的值的類型也有限制,它只能為下列類型之一(或能夠轉換為下列類型的):sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, string, enum類型, 或引用類型。值得注意的是這裡的引用類型,由於除去string類型外,所有的類型出去null值以外在編譯時期都不能由編譯器計算出他們的確切的值,所 以我們能夠聲明為const的引用類型只能為string或值為null的其他引用類型。顯然當我們聲明一個null的常量時,我們已經失去了聲明的意義 --這也可以說是C#設計的尴尬之處!
這就是說,當我們需要一個const的常量時,但它的類型又限制了它不能在編譯時期被計算出確定的值來,我們可采取將之聲明為static readonly來解決。但兩者之間還是有一點細微的差別的。看下面的兩個不同的文件:
//file1.cs
//csc /t:library file1.cs
using System;
namespace MyNamespace1
{
public class MyClass1
{
public static readonly int myFIEld = 10;
}
}
//file2.cs
//csc /r:file1.dll file2.cs
using System;
namespace MyNamespace2
{
public class MyClass1
{
public static void Main()
{
Console.WriteLine(MyNamespace1.MyClass1.myFIEld);
}
}
}
我 們的兩個類分屬於兩個文件file1.cs 和file2.cs,並分開編譯。在文件file1.cs內的域myField聲明為static readonly時,如果我們由於某種需要改變了myField的值為20,我們只需重新編譯文件file1.cs為file1.dll,在執行 file2.exe時我們會得到20。但如果我們將static readonly改變為const後,再改變myField的初始化值時,我們必須重新編譯所有引用到file1.dll的文件,否則我們引用的 MyNamespace1.MyClass1.myFIEld將不會如我們所願而改變。這在大的系統開發過程中尤其需要注意。實際上,如果我們能夠理解 const修飾的常量是在編譯時便被計算出確定的值,並代換到引用該常量的每一個地方,而readonly時在運行時才確定的量--只是在初始化後我們不 希望它的值再改變,我們便能理解C#設計者們的良苦用心,我們才能徹底把握const和readonly的行為!
域的初始化是面向對象 編程中一個需要特別注意的問題。C#編譯器缺省將每一個域初始化為它的默認值。簡單的說,數值類型(枚舉類型)的默認值為0或0.0。字符類型的默認值為 '\x0000'。布爾類型的默認值為false。引用類型的默認值為null。結構類型的默認值為其內的所有類型都取其相應的默認值。雖然C#編譯器為 每個類型都設置了默認類型,但作為面向對象的設計原則,我們還是需要對變量進行正確的初始化。實際上這也是C#推薦的做法,沒有對域進行初始化會導致編譯 器發出警告信息。C#中對域進行初始化有兩個地方--聲明的同時進行初始化和在構造器內進行初始化。如前所述,域的聲明初始化實際上被編譯器作為賦值語句 放在了構造器的內部的最開始處執行。實例變量初始化會被放在實例構造器內,靜態變量初始化會被放在靜態構造器內。如果我們聲明了一個靜態的變量並同時對之 進行了初始化,那麼編譯器將為我們構造出一個靜態構造器來把這個初始化語句變成賦值語句放在裡面。而作為const修飾的常量域,從嚴格意義上講不能算作 初始化語句,我們可以將它看作類似於C++中的宏代換。
屬性
屬性可以說是C#語言的一個創新。當然你也可以說不是。不是的原因 是它背後的實現實際上還是兩個函數--一個賦值函數(get),一個取值函數(set),這從它生成的中間語言代碼可以清晰地看到。是的原因是它的的確確 在語言層面實現了面向對象編程一直以來對“屬性”這一OO風格的類的特殊接口的訴求。理解屬性的設計初衷是我們用好屬性這一工具的根本。C#不提倡將域的 保護級別設為public而使用戶在類外任意操作--那樣太不OO,或者具體點說太不安全!對所有有必要在類外可見的域,C#推薦采用屬性來表達。屬性不 表示存儲位置,這是屬性和域的根本性的區別。下面是一個典型的屬性設計:
using System;
class MyClass
{
int integer;
public int Integer
{
get {return integer;}
set {integer=value;}
}
}
class Test
{
public static void Main()
{
MyClass MyObject=new MyClass();
Console.Write(MyObject.Integer);
MyObject.Integer++;
Console.Write(MyObject.Integer);
}
}
一如我們期待的那樣,程序輸出0 1。我們可以看到屬性通過對方法的包裝向程序員提供了一個友好的域成員的存取界面。這裡的value是C#的關鍵字,是我們進行屬性操作時的set的隱含參數,也就是我們在執行屬性寫操作時的右值。
屬性提供了只讀(get),只寫(set),讀寫(get和 set)三種接口操作。對域的這三種操作,我們必須在同一個屬性名下聲明,而不可以將它們分離,看下面的實現:
class MyClass
{
private string name;
public string Name
{
get { return name; }
}
public string Name
{
set { name = value; }
}
}
上面這種分離Name屬性實現的方法是錯誤的!我們應該像前面的例子一樣將他們放在一起。值得注意的是三種屬性(只讀,只寫,讀寫)被C#認為是同一個屬性名,看下面的例子:
class MyClass
{
protected int num=0;
public int Num
{
set
{
num=value;
}
}
}
class MyClassDerived: MyClass
{
new public int Num
{
get
{
return num;
}
}
}
class Test
{
public static void Main()
{
MyClassDerived MyObject = new MyClassDerived();
//MyObject.Num= 1; //錯誤 !
((MyClass)MyObject).Num = 1;
}
}
我們可以看到MyClassDerived中的屬性Num-get{}屏蔽了MyClass中屬性Num-set{}的定義。
當然屬性遠遠不止僅僅限於域的接口操作,屬性的本質還是方法,我們可以根據程序邏輯在屬性的提取或賦值時進行某些檢查,警告等額外操作,看下面的例子:
class MyClass
{
private string name;
public string Name
{
get { return name; }
set
{
if (value==null)
name="Microsoft";
else
name=value;
}
}
}
由 於屬性的方法的本質,屬性當然也有方法的種種修飾。屬性也有5種存取修飾符,但屬性的存取修飾往往為public,否則我們也就失去了屬性作為類的公共接 口的意義。除了方法的多參數帶來的方法重載等特性屬性不具備外, virtual, sealed, override, abstract等修飾符對屬性與方法同樣的行為,但由於屬性在本質上被實現為兩個方法,它的某些行為需要我們注意。看下面的例子:
abstract class A
{
int y;
public virtual int X
{
get { return 0; }
}
public virtual int Y
{
get { return y; }
set { y = value; }
}
public abstract int Z { get; set; }
}
class B: A
{
int z;
public override int X
{
get { return base.X + 1; }
}
public override int Y
{
set { base.Y = value < 0? 0: value; }
}
public override int Z
{
get { return z; }
set { z = value; }
}
}
這個例子集中地展示了屬性在繼承上下文中的某些典型行為。這裡,類A由於抽象屬性Z的存在而必須聲明為abstract。子類B中通過base關鍵字來引用父類A的屬性。類B中可以只通過Y-set便覆蓋了類A中的虛屬性。
靜態屬性和靜態方法一樣只能存取類的靜態域變量。我們也可以像做外部方法那樣,聲明外部屬性。
返回頁首
第八講 索引器與操作符重載
索引器
索 引器(Indexer)是C#引入的一個新型的類成員,它使得對象可以像數組那樣被方便,直觀的引用。索引器非常類似於我們前面講到的屬性,但索引器可以 有參數列表,且只能作用在實例對象上,而不能在類上直接作用。下面是典型的索引器的設計,我們在這裡忽略了具體的實現。
class MyClass
{
public object this [int index]
{
get
{
// 取數據
}
set
{
// 存數據
}
}
}
索引器沒有像屬性和方法那樣的名字,關鍵字this清楚地表達了索引器引用對象的特征。和屬性一樣,value關鍵字在set後的語句塊裡有參數傳遞意義。實際上從編譯後的IL中間語言代碼來看,上面這個索引器被實現為:
class MyClass
{
public object get_Item(int index)
{
// 取數據
}
public void set_Item(int index, object value)
{
//存數據
}
}
由 於我們的索引器在背後被編譯成get_Item(int index)和set_Item(int index, object value)兩個方法,我們甚至不能再在聲明實現索引器的類裡面聲明實現這兩個方法,編譯器會對這樣的行為報錯。這樣隱含實現的方法同樣可以被我們進行調 用,繼承等操作,和我們自己實現的方法別無二致。通曉C#語言底層的編譯實現為我們下面理解C#索引器的行為提供了一個很好的基礎。
和 方法一樣,索引器有5種存取保護級別,和4種繼承行為修飾,以及外部索引器。這些行為同方法沒有任何差別,這裡不再贅述。唯一不同的是索引器不能為靜態 (static),這在對象引用的語義下很容易理解。值得注意的是在覆蓋(override)實現索引器時,應該用base[E]來存取父類的索引器。
和屬性的實現一樣,索引器的數據類型同時為get語句塊的返回類型和set語句塊中value關鍵字的類型。
索引器的參數 列表也是值得注意的地方。“索引”的特征使得索引器必須具備至少一個參數,該參數位於this關鍵字之後的中括號內。索引器的參數也只能是傳值類型,不可 以有ref(引用)和out(輸出)修飾。參數的數據類型可以是C#中的任何數據類型。C#根據不同的參數簽名來進行索引器的多態辨析。中括號內的所有參 數在get和set下都可以引用,而value關鍵字只能在set下作為傳遞參數。
下面是一個索引器的具體的應用例子,它對我們理解索引器的設計和應用很有幫助。
using System;
class BitArray
{
int[] bits;
int length;
public BitArray(int length)
{
if (length < 0)
throw new ArgumentException();
bits = new int[((length - 1) >> 5) + 1];
this.length = length;
}
public int Length
{
get { return length; }
}
public bool this[int index]
{
get
{
if (index < 0 || index >= length)
throw new IndexOutOfRangeException();
else
return (bits[index >> 5] & 1 << index) != 0;
}
set
{
if (index < 0 || index >= length)
throw new IndexOutOfRangeException();
else if(value)
bits[index >> 5] |= 1 << index;
else
bits[index >> 5] &= ~(1 << index);
}
}
}
class Test
{
static void Main()
{
BitArray Bits=new BitArray(10);
for(int i=0;i<10;i++)
Bits[i]=(i%2)==0;
Console.Write(Bits[i]+" ");
}
}
編譯並運行程序可以得到下面的輸出:
True False True False True False True False True False
上 面的程序通過索引器的使用為用戶提供了一個界面友好的bool數組,同時又大大降低了程序的存儲空間代價。索引器通常用於對象容器中為其內的對象提供友好 的存取界面--這也是為什麼C#將方法包裝成索引器的原因所在。實際上,我們可以看到索引器在.Net Framework類庫中有大量的應用。
操作符重載
操 作符是C#中用於定義類的實例對象間表達式操作的一種成員。和索引器類似,操作符仍然是對方法實現的一種邏輯界面抽象,也就是說在編譯成的IL中間語言代 碼中,操作符仍然是以方法的形式調用的。在類內定義操作符成員又叫操作符重載。C#中的重載操作符共有三種:一元操作符,二元操作符和轉換操作符。並不是 所有的操作符都可以重載,三種操作符都有相應的可重載操作符集,列於下表:
一元操作符 + - ! ~ ++ -- true false
二元操作符 + - * / % & | ^ << >> == != > < >= <=
轉換操作符 隱式轉換()和顯式轉換()
重 載操作符必須是public和static 修飾的,否則會引起編譯錯誤,這在操作符的邏輯語義下是不言而喻的。父類的重載操作符會被子類繼承,但這種繼承沒有覆蓋,隱藏,抽象等行為,不能對重載操 作符進行virtual sealed override abstract修飾。操作符的參數必須為傳值參數。我們下面來看一個具體的例子:
using System;
class Complex
{
double r, v; //r+ v i
public Complex(double r, double v)
{
this.r=r;
this.v=v;
}
public static Complex Operator +(Complex a, Complex b)
{
return new Complex(a.r+b.r, a.v+b.v);
}
public static Complex Operator -(Complex a)
{
return new Complex(-a.r,-a.v);
}
public static Complex Operator ++(Complex a)
{
double r=a.r+1;
double v=a.v+1;
return new Complex(r, v);
}
public void Print()
{
Console.Write(r+" + "+v+"i");
}
}
class Test
{
public static void Main()
{
Complex a=new Complex(3,4);
Complex b=new Complex(5,6);
Complex c=-a;
c.Print();
Complex d=a+b;
d.Print();
a.Print();
Complex e=a++;
a.Print();
e.Print();
Complex f=++a;
a.Print();
f.Print();
}
}
編譯程序並運行可得到下面的輸出:
-3 + -4i 8 + 10i 3 + 4i 4 + 5i 3 + 4i 5 + 6i 5 + 6i
我 們這裡實現了一個“+”號二元操作符,一個“-”號一元操作符(取負值),和一個“++”一元操作符。注意這裡,我們都沒有對傳進來的參數作任何改變-- 這在參數是引用類型的變量是尤其重要,雖然重載操作符的參數只能是傳值方式。而我們在返回值時,往往需要“new”一個新的變量--除了true和 false操作符。這在重載“++”和“--” 操作符時尤其顯得重要。也就是說我們做在a++時,我們將丟棄原來的a值,而取代的是新的new出來的值給a! 值得注意的是e=a++或f=++a中e的值或f的值根本與我們重載的操作符返回值沒有一點聯系!它們的值僅僅是在前置和後置的情況下獲得a的舊值或新值 而已!前置和後置的行為不難理解。
操作符重載對返回值和參數類型有著相當嚴格的要求。一元操作符中只有一個參數。操作符“++”和“- -”返回值類型和參數類型必須和聲明該操作符的類型一樣。操作符“+ - ! ~”的參數類型必須和聲明該操作符的類型一樣,返回值類型可以任意。true和false操作符的參數類型必須和聲明該操作符的類型一樣,而返回值類型必 須為bool,而且必須配對出現--也就是說只聲明其中一個是不對的,會引起編譯錯誤。參數類型的不同會導致同名的操作符的重載--實際上這是方法重載的 表現。
二元操作符參數必須為兩個,而且兩個必須至少有一個的參數類型為聲明該操作符的類型。返回值類型可以任意。有三對操作符也需要必 須配對聲明出現,它們是“==”和“!=”,“>”和“<”,“>=”和“<=”。需要注, 意的是兩個參數的類型不同,雖然類型相 同但順序不同都會導致同名的操作符的重載。
轉換操作符為不同類型之間提供隱式轉換和顯式轉換,主要用於方法調用,轉型表達和賦值操作。 轉換操作符對其參數類型(被轉換類型)和返回值類型(轉換類型)也有嚴格的要求。參數類型和返回值類型不能相同,且兩者之間必須至少有一個和定義操作符的 類型相同。轉換操作符必須定義在被轉換類型或轉換類型任何其中一個裡面。不能對系統定義過的轉換操作進行重新定義。兩個類型也都不能是object或接口 類型,兩者之間不能有直接或間接的繼承關系--這三種情況系統已經默認轉換。我們來看一個例子:
using System;
public struct Digit
{
byte value;
public Digit(byte value)
{
if (value < 0 || value > 9)
throw new ArgumentException();
this.value = value;
}
public static implicit Operator byte(Digit d)
{
return d.value;
}
public static explicit Operator Digit(byte b)
{
return new Digit(b);
}
}
上 面的例子提供了Digit類型和byte類型之間的隱式轉換和顯式轉換。從Digit到byte的轉換為隱式轉換,轉換過程不會因為丟失任何信息而拋出異 常。從byte到Digit的轉換為顯式轉換,轉換過程有可能因丟失信息而拋出異常。實際上這也為我們揭示了什麼時候聲明隱式轉換,什麼時候聲明顯示轉換 的設計原則。不能對同一參數類型同時聲明隱式轉換和顯式轉換。隱式轉換和顯式轉換無需配對使用--雖然C#推薦這樣做。
實際上可以看 到,對於屬性,索引器和操作符這些C#提供給我們的界面操作,都是方法的某種形式的邏輯抽象包裝,它旨在為我們定義的類型的用戶提供一個友好易用的界面- -我們完全可以通過方法來實現它們實現的功能。理解了這樣的設計初衷,我們才會恰當,正確地用好這些操作,而不致導致濫用和錯用。