国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 開發(fā) > 綜合 > 正文

C# Sharp Experience

2024-07-21 02:27:01
字體:
供稿:網(wǎng)友

c# 語言是一門簡單,現(xiàn)代,優(yōu)雅,面向?qū)ο螅愋桶踩脚_獨立的一門新型組件編程語言。其語法風格源自c/c++家族,融合了visual basic的高效和c/c++強大,是微軟為奠定其下一互聯(lián)網(wǎng)霸主地位而打造的microsoft.net平臺的主流語言。其一經(jīng)推出便以其強大的操作能 力,優(yōu)雅的語法風格,創(chuàng)新的語言特性,第一等的面向組件編程的支持而深受世界各地程序員的好評和喜愛。“它就是我多年來夢寐以求的計算機語言!”--很多 資深程序員拿到c#都是這樣的驚訝。從c#語言的名字(c sharp)我們也可見微軟用其打造其下一代互聯(lián)網(wǎng)絡(luò)深度服務(wù)的勃勃雄心。c#語言目前已由微軟提交歐洲計算機制造商協(xié)會ecma,經(jīng)過標準化后的c#將 可由任何廠商在任何平臺上實現(xiàn)其開發(fā)工具及其支持軟件,這為c#的發(fā)展提供了強大的驅(qū)動力,我們也可從這里看到微軟前所未有的眼光和智慧。

組 件編程已經(jīng)成為當今世界軟件業(yè)面向下一代程序開發(fā)的一致選擇,是90年代面向?qū)ο缶幊痰纳疃劝l(fā)展。c#生逢其時,占盡天時地利,“第一等的面向組件編程的 支持”也決不是簡單說說那么輕松。實際上,組件特性已經(jīng)深深植入c#語言的各個層面,是為c#銳利(sharp)之處。在下面的文章中筆者將從c#語言的 各個層面來展現(xiàn)c#語言中無處不見的組件特性,深度闡述c#面向組件編程。整個專題共分為十講:“第一講 ‘hello,world!’程序”,“第二講 c#語言基礎(chǔ)介紹”,“第三講 microsoft.net平臺基礎(chǔ)構(gòu)造”,“第四講 類與對象”,“第五講 構(gòu)造器與析構(gòu)器”,“第六講 方法”,“第七講 域與屬性”,“第八講 索引器與操作符重載”,“第九講 數(shù)組與字符串”,“第十講 特征與映射”,“第十一講 com互操作 非托管編程與異常處理”,“第十二講 用c#編織未來--c#編程模型概述”。

本頁內(nèi)容
 第一講 “hello,world!”程序
 第二講 c#語言基礎(chǔ)介紹
 第三講 microsoft.net平臺基礎(chǔ)構(gòu)造
 第四講 類與對象
 第五講 構(gòu)造器與析構(gòu)器
 第六講 方法
 第七講 域與屬性
 第八講 索引器與操作符重載

第一講 “hello,world!”程序
“hello world!”程序是程序員一直以來的一個浪漫約定,也是一個偉大的夢想--總有一天,出自人類之手的計算機會面對這個美麗的世界說一聲“hello world!”。它是學(xué)習(xí)一門新語言的一個很好的起點,我們就從這里開始,看下面例子:

//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#編譯器的命令行環(huán)境里鍵入"csc helloworld.cs"編譯文件。可以看到編譯輸出文件helloworld.exe。我們鍵入helloworld執(zhí)行這個文件可得到下面的輸 出:

hello world !

下面我們來仔細分析上面的代碼和整個程序的編譯輸出及執(zhí)行過程。先看文件開始的兩行代碼,這是c#語言的單行注釋語句。和c++語言類似,c#支持兩種注釋方法:以"http://"開始的單行注釋和以"/*","*/"配對使用的多行注釋。注釋之間不能嵌套。

再 來看下面的"using system;"語句,這是c#語言的using命名空間指示符,這里的"system"是microsoft.net系統(tǒng)提供的類庫。c#語言沒有自己 的語言類庫,它直接獲取microsoft.net系統(tǒng)類庫。microsoft.net類庫為我們的編程提供了非常強大的通用功能。該語句使得我們可以 用簡短的別名"console"來代替類型"system.console"。當然using指示符并不是必須的,我們可以用類型的全局名字來獲取類型。 實際上,using語句采用與否根本不會對c#編譯輸出的程序有任何影響,它僅僅是簡化了較長的命名空間的類型引用方式。

接著我們聲明 并實現(xiàn)了一個含有靜態(tài)main()函數(shù)的helloworld類。c#所有的聲明和實現(xiàn)都要放在同一個文件里,不像c++那樣可以將兩者分離。main ()函數(shù)在c#里非常特殊,它是編譯器規(guī)定的所有可執(zhí)行程序的入口點。由于其特殊性,對main()函數(shù)我們有以下幾條準則:

1.
 main()函數(shù)必須封裝在類或結(jié)構(gòu)里來提供可執(zhí)行程序的入口點。c#采用了完全的面向?qū)ο蟮木幊谭绞剑琧#中不可以有像c++那樣的全局函數(shù)。
 
2.
 main()函數(shù)必須為靜態(tài)函數(shù)(static)。這允許c#不必創(chuàng)建實例對象即可運行程序。
 
3.
 main()函數(shù)保護級別沒有特殊要求, public,protected,private等都可,但一般我們都指定其為public。
 
4.
 main()函數(shù)名的第一個字母要大寫,否則將不具有入口點的語義。c#是大小寫敏感的語言。
 
5.
 main ()函數(shù)的參數(shù)只有兩種參數(shù)形式:無參數(shù)和string 數(shù)組表示的命令行參數(shù),即static void main()或static void main(string[]args) ,后者接受命令行參數(shù)。一個c#程序中只能有一個main()函數(shù)入口點。其他形式的參數(shù)不具有入口點語義,c#不推薦通過其他參數(shù)形式重載main() 函數(shù),這會引起編譯警告。
 
6.
 main()函數(shù)返回值只能為void(無類型)或int(整數(shù)類型)。其他形式的返回值不具有入口點語義。
 

我 們再來看"helloworld.cs"程序中main()函數(shù)的內(nèi)部實現(xiàn)。前面提過,console是在命名空間system下的一個類,它表示我們通 常打交道的控制臺。而我們這里是調(diào)用其靜態(tài)方法writeline()。如同c++一樣,靜態(tài)方法允許我們直接作用于類而非實例對象。writeline ()函數(shù)接受字符串類型的參數(shù)"hello world !",并把它送入控制臺顯示。如前所述,c#沒有自己的語言類庫,它直接獲取microsoft.net系統(tǒng)類庫。我們這里正是通過獲取 microsoft.net系統(tǒng)類庫中的system.console.writeline()來完成我們想要的控制臺輸出操作。這樣我們便完成了 "hello world!"程序。

但事情遠沒那么簡單!在我們編譯輸出執(zhí)行程序的同時,microsoft.net底層的諸多機制卻 在暗地里涌動,要想體驗c#的銳利,我們沒有理由忽視其背靠的microsoft.net平臺。實際上如果沒有microsoft.net平臺,我們很難 再說c#有何銳利之處。我們先來看我們對"helloworld.cs"文件用csc.exe命令編譯后發(fā)生了什么。是的,我們得到了 helloworld.exe文件。但那僅僅是事情的表象,實際上那個helloworld.exe根本不是一個可執(zhí)行文件!那它是什么?又為什么能夠執(zhí) 行?

好的,下面正是回答這些問題的地方。首先,編譯輸出的helloworld.exe是一個由中間語言(il),元數(shù)據(jù) (metadata)和一個額外的被編譯器添加的目標平臺的標準可執(zhí)行文件頭(比如win32平臺就是加了一個標準win32可執(zhí)行文件頭)組成的pe (portable executable,可移植執(zhí)行體)文件,而不是傳統(tǒng)的二進制可執(zhí)行文件--雖然他們有著相同的擴展名。中間語言是一組獨立于cpu的指令集,它可以被 即時編譯器jitter翻譯成目標平臺的本地代碼。中間語言代碼使得所有microsoft.net平臺的高級語言c#,vb.net,vc.net等得 以平臺獨立,以及語言之間實現(xiàn)互操作。元數(shù)據(jù)是一個內(nèi)嵌于pe文件的表的集合。元數(shù)據(jù)描述了代碼中的數(shù)據(jù)類型等一些通用語言運行時(common language runtime)需要在代碼執(zhí)行時知道的信息。元數(shù)據(jù)使得.net應(yīng)用程序代碼具備自描述特性,提供了類型安全保障,這在以前需要額外的類型庫或接口定義 語言(interface definition language,簡稱idl)。

這樣的解釋可能還是有點讓人困惑,那么我們來實際 的解剖一下這個pe文件。我們采用的工具是.net sdk beta2自帶的ildasm.exe,它可以幫助我們提取pe文件中的有關(guān)數(shù)據(jù)。我們鍵入命令"ildasm /output:helloworld.il helloworld.exe",一般可以得到兩個輸出文件:helloworld.il和helloworld.res。其中后者是提取的資源文件,我 們暫且不管,我們來看helloworld.il文件。我們用"記事本"程序打開可以看到元數(shù)據(jù)和中間語言(il)代碼,由于篇幅關(guān)系,我們只將其中的中 間語言代碼提取出來列于下面,有關(guān)元數(shù)據(jù)的表項我們暫且不談:

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

我 們粗略的感受是它很類似于早先的匯編語言,但它具有了對象定義和操作的功能。我們可以看到它定義并實現(xiàn)了一個繼承自system.object 的helloworld類及兩個函數(shù):main()和.ctor()。其中.ctor()是helloworld類的構(gòu)造函數(shù),可在 "helloworld.cs"源代碼中我們并沒有定義構(gòu)造函數(shù)呀--是的,我們沒有定義構(gòu)造函數(shù),但c#的編譯器為我們添加了它。你還可以看到c#編譯 器也強制helloworld類繼承system.object類,雖然這個我們也沒有指定。關(guān)于這些高級話題我們將在以后的講座中予以剖析。

那么pe文件是怎么執(zhí)行的呢?下面是一個典型的c#/.net應(yīng)用程序的執(zhí)行過程:

1.
 用戶執(zhí)行編譯器輸出的應(yīng)用程序(pe文件),操作系統(tǒng)載入pe文件,以及其他的dll(.net動態(tài)連接庫)。
 
2.
 操作系統(tǒng)裝載器根據(jù)前面pe文件中的可執(zhí)行文件頭跳轉(zhuǎn)到程序的入口點。顯然,操作系統(tǒng)并不能執(zhí)行中間語言,該入口點也被設(shè)計為跳轉(zhuǎn)到mscoree.dll(.net平臺的核心支持dll)的_ corexemain()函數(shù)入口。
 
3.
 corexemain()函數(shù)開始執(zhí)行pe文件中的中間語言代碼。這里的執(zhí)行的意思是通用語言運行時按照調(diào)用的對象方法為單位,用即時編譯器將中間語言編譯成本地機二進制代碼,執(zhí)行并根據(jù)需要存于機器緩存。
 
4.
 程序的執(zhí)行過程中,垃圾收集器負責內(nèi)存的分配,釋放等管理功能。
 
5.
 程序執(zhí)行完畢,操作系統(tǒng)卸載應(yīng)用程序。
 

清 楚的知曉編譯輸出的pe文件的執(zhí)行過程是深度掌握c#語言編程的關(guān)鍵,這種過程的本身就詮釋著c#語言的高級內(nèi)核機制以及其背后 microsoft.net平臺種種詭秘的性質(zhì)。一個"hello world !"程序的概括力已經(jīng)足夠,在我們對c#語言有了一個很好的起點之后,下面的專題會和大家一起領(lǐng)略c#基礎(chǔ)語言,窺探microsoft.net平臺構(gòu) 造,步步體驗c#銳利編程的極樂世界,let's go!

返回頁首
第二講 c#語言基礎(chǔ)介紹
在體驗c#的銳利之前,關(guān)乎語言基本知識的掌握是必不可少的一環(huán)。由于c#基本語言很多源自c/c++,在這里對那些和c/c++類似的地方僅作簡單介紹,我們將體驗專注于那些區(qū)別于傳統(tǒng)c/c++的關(guān)鍵的語言基礎(chǔ)知識。

數(shù)據(jù)類型
c# 語言的數(shù)據(jù)類型主要分為兩類:值類型和引用類型。另外一種數(shù)據(jù)類型"指針"是為unsafe上下文編程專門設(shè)定的,其中unsafe上下文指對代碼進行 unsafe標示以滿足利用指針對內(nèi)存直接進行操作要求的c#非托管代碼,這些代碼將失去microsoft.net平臺的垃圾收集等clr性質(zhì),我們放 在"com互操作 非托管編程與異常處理"專題里闡述。值類型的變量本身包含他們的數(shù)據(jù),而引用類型的變量包含的是指向包含數(shù)據(jù)的內(nèi)存塊的引用或者叫句柄。從下面這幅圖中可 以清晰地看出兩者的差別:


引用類型帶來的可能的問題便是當多個變量引用同樣的內(nèi)存塊時,對任何一個引用變量的修改都會導(dǎo)致該對象的值的改變。null值表示引用類型沒有對任何實際地址進行引用。

值 類型可分為結(jié)構(gòu)類型和枚舉類型。結(jié)構(gòu)類型包括簡單類型和用戶自定義結(jié)構(gòu)類型。枚舉類型和用戶自定義結(jié)構(gòu)類型我們將在"第九講 結(jié)構(gòu),枚舉,數(shù)組與字符串"專題里詳細闡述。簡單類型又可分為布爾類型和數(shù)值類型。c#語言中布爾類型嚴格與數(shù)值類型區(qū)分,只有true和false兩種 取值,不存在像c/c++里那樣和其他類型之間的轉(zhuǎn)換。數(shù)值類型包括整值,浮點和decimal三種類型。整值類型有sbyte,byte,short, ushort,int,uint,long,ulong,char共九種。除了char類型外,其他8種兩兩一組分別為有符號和無符號兩種。浮點值有 float和double兩種。decimal主要用于金融,貨幣等對精度要求比較高的計算環(huán)境。下表是對這些簡單類型的一個詳細的描述:

簡單類型 描 述 示 例
sbyte
 8-bit 有符號整數(shù)
 sbyte val = 12;
 
short
 16-bit 有符號整數(shù)
 short val = 12;
 
int
 32-bit有符號整數(shù)
 int val = 12;
 
long
 64-bit有符號整數(shù)
 long val1 = 12; long val2 = 34l;
 
byte
 8-bit無符號整數(shù)
 byte val1 = 12; byte val2 = 34u;
 
ushort
 16-bit 無符號整數(shù)
 ushort val1 = 12; ushort val2 = 34u;
 
uint
 32-bit 無符號整數(shù)
 uint val1 = 12; uint val2 = 34u;
 
ulong
 64-bit 無符號整數(shù)
 ulong val1 = 12; ulong val2 = 34u; ulong val3 = 56l; ulong val4 = 78ul;
 
float
 32-bit單精度浮點數(shù)
 float val = 1.23f;
 
double
 64-bit雙精度浮點數(shù)
 double val1 = 1.23; double val2 = 4.56d;
 
bool
 布爾類型
 bool val1 = true; bool val2 = false;
 
char
 字符類型 ,unicode 編碼
 char val = 'h';
 
decimal
 28個有效數(shù)字的128-bit十進制類型
 decimal val = 1.23m;
 

引 用類型共分四種類型:類,接口,數(shù)組,委派。類除了我們可以定義自己的類型外,又包括兩個比較特殊的類型object和string。object是c# 中所有類型(包括所有的值類型和引用類型)的繼承的根類。string類型是一個密封類型(不能被繼承),其實例表示unicode字符串,它和數(shù)組類型 我們將放在"第九講 結(jié)構(gòu),枚舉,數(shù)組與字符串"中詳述。接口類型定義一個方法的合同,我們將在"第七講 接口 繼承與多態(tài)"中講述。委派類型是一個指向靜態(tài)或?qū)嵗椒ǖ暮灻愃朴赾/c++中的函數(shù)指針,將在"第八講 委派與事件"中講述。實際上我們將從后面的專題中看到這些類型都是類的某種形式的包裝。

每種數(shù)據(jù)類型都有對應(yīng)的缺省值。數(shù)值類型的缺省 值為0或0.0,其中char的缺省為'/x0000'。布爾類型的缺省值為false。枚舉類型的缺省值為0。結(jié)構(gòu)類型的缺省值是將它所有的值類型的域 設(shè)置為對應(yīng)值類型的缺省值,將其引用類型的域設(shè)置為null。所有引用類型的缺省值為null。

不同類型的數(shù)據(jù)之間可以轉(zhuǎn)換,c#的類 型轉(zhuǎn)換有隱含轉(zhuǎn)換,明晰轉(zhuǎn)換,標準轉(zhuǎn)換,自定義轉(zhuǎn)換共四種方式。隱含轉(zhuǎn)換與明晰轉(zhuǎn)換和c++里一樣,數(shù)據(jù)從"小類型"到"大類型"的轉(zhuǎn)換時為隱含轉(zhuǎn)換,從 "大類型"到"小類型"的轉(zhuǎn)換為明晰轉(zhuǎn)換,明晰轉(zhuǎn)換需要如"(type)data"一般的括號轉(zhuǎn)換操作符。標準轉(zhuǎn)換和自定義轉(zhuǎn)換是針對系統(tǒng)內(nèi)建轉(zhuǎn)換和用戶 定義的轉(zhuǎn)換而言的,兩者都是對類或結(jié)構(gòu)這樣的自定義類型而言的。

變量與常量
變量表示存儲位置,變量必須有確定的數(shù)據(jù)類型。c#的類型安全的含義之一就是確保變量的存儲位置容納著合適的類型。可以將c#中的變量分為靜態(tài)變量,實例變量,傳值參數(shù),引用參數(shù),輸出參數(shù),數(shù)組參數(shù)和本地變量共七種。本地變量則是在方法體內(nèi)的臨時變量。

靜 態(tài)變量和實例變量主要是針對類或結(jié)構(gòu)內(nèi)的數(shù)據(jù)成員(又叫域)而言的。靜態(tài)變量在它寄存的類或結(jié)構(gòu)類型被裝載后得到存儲空間,如果沒有對它進行初始化賦值, 靜態(tài)變量的初始值將是它的類型所持有的缺省值。實例變量在它的類實例被創(chuàng)建后獲得存儲空間,如果沒有經(jīng)過初始化賦值,它的初始值與靜態(tài)變量的定義相同。兩 者更詳細的說明我們放在"第六講 域 方法 屬性與索引器"專題里。

傳值參數(shù),引用參數(shù),輸出參數(shù),數(shù)組參數(shù)主要針對方法的參數(shù)類型而 言的。簡單的講傳值參數(shù)是對變量的值的一種傳遞,方法內(nèi)對變量的改變在方法體外不起作用。對于傳值參數(shù)本身是引用型的變量稍有不同,方法內(nèi)對該引用(句 柄)變量指向的數(shù)據(jù)成員即實際內(nèi)存塊的改變將在方法體外仍然保留改變,但對于引用(句柄)本身的改變不起作用。引用參數(shù)是對變量的句柄的一種傳遞,方法內(nèi) 對該變量的任何改變都將在方法體外保留。輸出參數(shù)是c#專門為有多個返回值的方法而量身定做的,它類似于引用變量,但可以在進入方法體之前不進行初始化, 而其他的參數(shù)在進入方法體內(nèi)c#都要求明確的初始化。數(shù)組參數(shù)是為傳遞大量的數(shù)組元素而專門設(shè)計的,它從本質(zhì)上講是一種引用型變量的傳值參數(shù)。它們更詳細 的闡述我們也放在"第六講 域 方法 屬性與索引器"專題里。

本地變量嚴格的講是在c#的塊語句,for語句,switch語句,using語句內(nèi)聲明的變量,它的生命周期嚴格地被限制在這些語句塊內(nèi)部。

常量在編譯時便確定它的值,在整個程序中也不許修改。常量聲明的同時必須賦值。由于它的編譯時確定值的特性,引用類型可能的值只能為string和null(除string外,引用類型的構(gòu)建器必須在運行時才能確定引用類型的值)。

操作符與表達式
c# 保留了c++所有的操作符,其中指針操作符(*和->)與引用操作符(&)需要有unsafe的上下文。c#擯棄了范圍辨析操作符 (::),一律改為單點操作符(.)。我們不再闡述那些保留的c++的操作符,這里主要介紹c#引入的具有特殊意義的幾個操作符:as,is,new, typeof,sizeof,stackalloc。

as操作符用于執(zhí)行兼容類型之間的轉(zhuǎn)換,當轉(zhuǎn)換失敗時,as 操作符結(jié)果為null。is 操作符用于檢查對象的運行時類型是否與給定類型兼容,當表達式非null且可以轉(zhuǎn)化為指定類型時,is操作符結(jié)果為true,否則為false。as和 is操作符是基于同樣的類型鑒別和轉(zhuǎn)換而設(shè)計的,兩者有相似的應(yīng)用場合。實際上expression as type相當于expression is type ? (type)expression : (type)null。

作為操作符的new用于在堆上創(chuàng)建對象和調(diào)用構(gòu)造函數(shù), 值得注意的是值類型對象(例如結(jié)構(gòu))是在堆棧上創(chuàng)建的,而引用類型對象(例如類)是在堆上創(chuàng)建的。new也用于修飾符,用于隱藏基類成員的繼承成員。為隱 藏繼承的成員,使用相同名稱在派生類中聲明該成員并用 new 修飾符修改它。typeof 運算符用于獲得某一類型的 system.type 對象,我們將在"第十講 特征與映射"里結(jié)合microsoft.net的類型系統(tǒng)對它作詳細的闡述。sizeof 運算符用于獲得值類型(不適用于引用類型)的大小(以字節(jié)為單位)。stackalloc用于在堆棧上分配內(nèi)存塊, 僅在局部變量的初始值設(shè)定項中有效,類似于c/c++語言的_alloca。sizeof和statckalloc都由于涉及內(nèi)存的直接操作而需要 unsafe上下文。

c#里的某些操作符可以像c++里那樣被重載。操作符重載使得自定義類型(類或結(jié)構(gòu))可以用簡單的操作符來方便的表達某些常用的操作。

為完成一個計算結(jié)果的一系列操作符和操作數(shù)的組合稱為表達式。和c++一樣,c#的表達式可以分為賦值表達式和布爾表達式兩種,c#沒有引入新的表達式形式,我們對此不再贅述。

命名空間與語句
c# 采用命名空間(namespace)來組織程序。命名空間可以嵌套。using指示符可以用來簡化命名空間類型的引用。using指示符有兩種用法。 "using system;"語句可以使我們用簡短的類型名"console"來代替類型"system.console"。"using output = system.console;"語句可以使我們用別名"output"來代替類型"system.console"。命名空間的引入大大簡化了c#程序 的組織方式。

c#語句可以分為標號語句,聲明語句,塊語句,空語句,表達式語句,選擇語句,反復(fù)語句,跳轉(zhuǎn)語句,try語句,checked/unchecked語句,lock語句,using語句。

標 號語句主要為goto跳轉(zhuǎn)設(shè)計,c#不允許跨方法的跳轉(zhuǎn),但允許小規(guī)模的方法內(nèi)的跳轉(zhuǎn)。聲明語句可以同時進行初始化賦值,對象的實例化聲明需要new關(guān)鍵 字。塊語句采用"{"和"}"定義語句塊,主要是界定局部變量的作用范圍。空語句在c#中用分號";"表示,沒有執(zhí)行語義。表達式語句通過表達式構(gòu)成語 句。

選擇語句有if語句和switch語句兩種,與c++別無二致。反復(fù)語句除了while,do,for三種循環(huán)結(jié)構(gòu)外引入了foreach語句用于遍歷集合中所有的元素,但這需要特定的接口支持,我們在后面的章節(jié)里對之作詳細闡述。

跳轉(zhuǎn)語句有break,continue,goto,return,throw五種語句,前四種與c++里的語義相同,throw語句與后面的try語句我們將在"第十一講 com互操作 非托管編程與異常處理"闡述。

checked/unchecked語句主要用于數(shù)值運算中溢出檢查的上下文。lock語句主要用于線程信號量的鎖控制。using語句主要用于片斷資源管理。這些我們在后續(xù)章節(jié)里都會有具體的涉及。

返回頁首
第三講 microsoft.net平臺基礎(chǔ)構(gòu)造
拋 開microsoft.net平臺去談c#是沒有意義的,c#之“sharp”也正在其后端強大的平臺。僅僅拘泥于語法層面是體驗不了c#的銳利之處的, c#程序很多詭秘之處必須依靠microsoft.net平臺才能深度的掌握和運用。簡單的講,microsoft.net平臺是一個建立在開放互聯(lián)網(wǎng)絡(luò) 協(xié)議和標準之上,采用新的工具和服務(wù)來滿足人們的計算和通信需求的革命性的新型xml web智能計算服務(wù)平臺。它允許應(yīng)用程序在因特網(wǎng)上方便快捷地互相通信,而不必關(guān)心使用何種操作系統(tǒng)和編程語言。

從技術(shù)層面具體來說, microsoft.net平臺主要包括兩個內(nèi)核,即通用語言運行時(common language runtime,簡稱clr)和microsoft.net框架類庫,它們?yōu)閙icrosoft.net平臺的實現(xiàn)提供了底層技術(shù)支持。通用語言運行時是 建立在操作系統(tǒng)最底層的服務(wù),為microsoft.net平臺的執(zhí)行引擎。microsoft.net框架包括一套可被用于任何編程語言的類庫,其目的 是使得程序員更容易地建立基于網(wǎng)絡(luò)的應(yīng)用和服務(wù)。在此之上是許多應(yīng)用程序模板,這些模板為開發(fā)網(wǎng)絡(luò)應(yīng)用和服務(wù)提供高級的組件和服務(wù)。 microsoft.net平臺之浩瀚絕非這里的幾千字能夠廓清,我們下面將著重體驗?zāi)切ξ覀冇胏#開發(fā)應(yīng)用程序至關(guān)重要的平臺基礎(chǔ)構(gòu)造。

通用語言運行時(clr)
通 用語言運行時是整個microsoft.net框架賴以建構(gòu)的基礎(chǔ),它為microsoft.net應(yīng)用程序提供了一個托管的代碼執(zhí)行環(huán)境。它實際上是駐 留在內(nèi)存里的一段代理代碼,負責應(yīng)用程序在整個執(zhí)行期間的代碼管理工作,比較典型的有:內(nèi)存管理,線程管理,安全管理,遠程管理,即時編譯,代碼強制安全 類型檢查等。這些都可稱得上microsoft.net框架的生命線。

實際上我們可以看出來,clr代理了一部分傳統(tǒng)操作系統(tǒng)的管理功 能。在clr下的代碼稱之為托管代碼,否則稱為非托管代碼。我們也可將clr看作一個技術(shù)規(guī)范,無論程序使用什么語言編寫,只要能編譯成微軟中間語言 (msil),就可以在它的支持下運行,這使得應(yīng)用程序得以獨立于語言。目前支持clr的編程語言多達二三十種。微軟中間語言是我們在 microsoft.net平臺下編譯器輸出的pe文件的語言。它是microsoft.net平臺最完整的語言集,非常類似于pc機上的匯編語言。即時 編譯器在運行時將中間語言編譯成本地二進制代碼。它為microsoft.net平臺提供了多語言的底層技術(shù)支持。另外根據(jù)需要, microsoft.net即時編譯器提供了特殊情況下的經(jīng)濟型即時編譯和安裝時編譯技術(shù)。

clr的設(shè)計目的便是直接在應(yīng)用程序運行環(huán) 境中為基于組件的編程提供第一等的支持。正如在windows中添加了對窗口、控件、圖形和菜單的直接支持,為基于消息的編程添加了底層結(jié)構(gòu),為支持設(shè)備 無關(guān)性添加了抽象內(nèi)容一樣,clr直接支持組件(包括屬性和事件)、對象、繼承性、多態(tài)性和接口。對屬性和事件的直接支持使得基于組件的編程變得更簡單, 而不需要特殊的接口和適配設(shè)計模式。在組件運行時,clr負責管理內(nèi)存分配、啟動和中止線程和進程、強化安全系數(shù),同時還調(diào)整任何該組件涉及到的其他組件 的附屬配置。序列化支持允許以多種格式操作存儲在磁盤上的組件,包括基于業(yè)界標準xml的soap。clr提供了處理錯誤條件的有力、協(xié)調(diào)的方式。每個模 塊都具有內(nèi)置的完整的元數(shù)據(jù),這意味著諸如動態(tài)創(chuàng)建和方法調(diào)用之類的功能更容易,也更安全。映射甚至允許我們靈活地創(chuàng)建和執(zhí)行代碼。我們可以控制應(yīng)用程序 使用的組件的版本,這使應(yīng)用程序更加可靠。組件代碼是與處理器無關(guān)的和易于驗證的中間語言 ( il),而不是某一種特定的機器語言,這意味著組件不但可以在多種計算機上運行,而且可以確保組件不會覆蓋它們不使用的內(nèi)存,也不會潛在地導(dǎo)致系統(tǒng)崩潰。 clr根據(jù)托管組件的來源(例如來自因特網(wǎng),企業(yè)局域網(wǎng),本地機)等因素對他們判定以適當?shù)男湃味龋@樣clr會根據(jù)他們的信任度來限定他們執(zhí)行如讀取文 件,修改注冊表等某些敏感操作的權(quán)限。借助通用類型系統(tǒng)(common type system,簡稱cts)對代碼類型進行嚴格的安全檢查避免了不同組件之間可能存在的類型不匹配的問題。clr下的編程全部是圍繞組件進行的。

值得指出的是clr通常寄宿在其他高性能的服務(wù)器應(yīng)用程序中,比如:因特網(wǎng)信息服務(wù)器(iis),microsoft sql server。這使得我們可以充分利用通用語言運行時諸多的安全,高效的優(yōu)點來部署自己的商業(yè)邏輯。

內(nèi)存管理
clr 對程序員影響最大的就是它的內(nèi)存管理功能,以至于我們很有必要單獨把它列出來闡述。它為應(yīng)用程序提供了高性能的垃圾收集環(huán)境。垃圾收集器自動追蹤應(yīng)用程序 操作的對象,程序員再也用不著和復(fù)雜的內(nèi)存管理打交道。這在某些喜歡張口閉口底層編程的所謂的高手來說,自動內(nèi)存管理從來都是他們嘲笑的對象。的確,為通 用軟件環(huán)境設(shè)計的自動化內(nèi)存管理器永遠都抵不上自己為特定程序量身訂制的手工制作。但現(xiàn)代軟件業(yè)早已不再是幾百行代碼的作坊作業(yè),動輒成千上萬行的代碼, 大量的商業(yè)邏輯凸現(xiàn)的已不再是算法的靈巧,而是可管理性,可維護性的工程代碼。.net/c#不是為那樣的作坊高手準備的,c語言才是他們的尤物。在 microsoft.net托管環(huán)境下,clr負責處理對象的內(nèi)存布局,管理對象的引用,釋放系統(tǒng)不再使用的內(nèi)存(自動垃圾收集)。這從根本上解決了長期 以來困擾軟件的內(nèi)存泄漏和無效內(nèi)存引用問題,大大減輕了程序員的開發(fā)負擔,提高了程序的健壯性。實際上我們在托管環(huán)境下根本找不到關(guān)于內(nèi)存操作或釋放的語 言指令。值得指出的是microsoft.net應(yīng)用程序可以使用托管數(shù)據(jù),也可以使用非托管數(shù)據(jù),但clr并不能判斷托管數(shù)據(jù)與非托管數(shù)據(jù)。

垃 圾收集器負責管理.net應(yīng)用程序內(nèi)存的分配和釋放。當用new操作符創(chuàng)建新的對象時,垃圾收集器在托管堆(managed heap)中為對象分配內(nèi)存資源。只要托管堆內(nèi)的內(nèi)存空間可用,垃圾收集器就為每一個新創(chuàng)建的對象分配內(nèi)存。當應(yīng)用程序不再持有某個對象的引用,垃圾收集 器將會探測到并釋放該對象。值得注意的是垃圾收集器并不是在對象引用無效時就立即開始釋放工作,而是根據(jù)一定算法來決定什么時候進行收集和對什么對象進行 收集。任何一個機器的內(nèi)存資源總是有限的,當托管堆內(nèi)的內(nèi)存空間不夠用時,垃圾收集器啟動收集線程來釋放系統(tǒng)內(nèi)存。垃圾收集器根據(jù)對象的存活時間,對象歷 經(jīng)的收集次數(shù)等來決定對哪些對象的內(nèi)存進行釋放。宏觀的看,我們并不知道垃圾收集的確切行為,但microsoft.net類庫為我們提供了控制垃圾收集 行為的部分功能,在某些特殊情況下,我們有必要進行一些受限的操作。

垃圾收集器并不意味著程序員從此可以一勞永逸,如果正在操作一個包裝了如文件,網(wǎng)絡(luò)連接,windows句柄,位圖等底層操作系統(tǒng)資源的對象,我們還是需要明確地釋放這些非托管資源的。這在“第五講 構(gòu)造器與析構(gòu)器”里有詳細的闡述。

microsoft.net框架類庫
microsoft.net 框架類庫是一組廣泛的,面向?qū)ο蟮目芍赜妙惖募希瑸閼?yīng)用程序提供各種高級的組件和服務(wù)。它將程序員從繁重的編程細節(jié)中解放出來專注于程序的商業(yè)邏輯,為 應(yīng)用程序提供各種開發(fā)支持--不管是傳統(tǒng)的命令行程序還是windows圖形界面程序,擬或是面向下一代因特網(wǎng)分布式計算平臺的asp.net或xml web服務(wù)。下面是對這些組件和服務(wù)的一個概括。

• 系統(tǒng)框架服務(wù)

服務(wù)框架包括一套開發(fā)人員希望在標準語言庫中存在的基類庫,例如:集合、輸入/輸出,字符串及數(shù)據(jù)類。另外,基類庫提供訪問操作系統(tǒng)服務(wù)如圖畫、網(wǎng)絡(luò)、線程、全球化和加密的類。服務(wù)框架也包括數(shù)據(jù)訪問類庫,及開發(fā)工具,如調(diào)試和剖析服務(wù),能夠使用的類。
 
• ado.net組件

ado.net為基于網(wǎng)絡(luò)的可擴展的應(yīng)用程序和服務(wù)提供數(shù)據(jù)訪問服務(wù)。ado.net不僅支持傳統(tǒng)的基于連接指針風格的數(shù)據(jù)訪問,同時也為更適合于把數(shù)據(jù)返回到客戶端應(yīng)用程序的無連接的數(shù)據(jù)模板提供高性能的訪問支持。
 
• xml數(shù)據(jù)組件

所有的數(shù)據(jù)都可被看作xml,開發(fā)人員可以通過xml為任何數(shù)據(jù)使用轉(zhuǎn)換,傳輸和確認服務(wù)。系統(tǒng)框架對xml數(shù)據(jù)提供第一等的操作支持。系統(tǒng)也支持ado.net數(shù)據(jù)與xml數(shù)據(jù)之間的通用轉(zhuǎn)換。
 
• windows表單組件

windows表單組件為開發(fā)人員提供了強大的windows應(yīng)用程序模型和豐富的windows用戶接口,包括傳統(tǒng)的activex控件和windows xp的新界面,如透明的、分層的、浮動窗口。對設(shè)計時的強大支持也是windows表單組件令人興奮的地方。
 
• asp.net應(yīng)用服務(wù)

asp.net 的核心是高性能的用于處理基于低級結(jié)構(gòu)的http請求的運行語言。編譯運行方式大大提高了它的性能。asp.net使用基于構(gòu)件的microsoft .net框架配制模板,因此它獲得了如xcopy配制、構(gòu)件并行配制、基于xml配制等優(yōu)點。它支持應(yīng)用程序的實時更新,提供高速緩沖服務(wù)改善性能。
 
• asp.net web表單

asp.net web表單把基于vb的表單的高生產(chǎn)性的優(yōu)點帶到了網(wǎng)絡(luò)應(yīng)用程序的開發(fā)中來。asp.net web表單支持傳統(tǒng)的將html內(nèi)容與角本代碼混合的asp語法,但是它提出了一種將應(yīng)用程序代碼和用戶接口內(nèi)容分離的更加結(jié)構(gòu)化的方法。asp.net 提供了一套映射傳統(tǒng)的html用戶接口部件(包括列表框,文本框和按鈕)的asp.net web表單控件和一套更加復(fù)雜強大的網(wǎng)絡(luò)應(yīng)用控件(如日歷和廣告轉(zhuǎn)板)。
 
• xml web服務(wù)

asp.net應(yīng)用服務(wù) 體系架構(gòu)為用asp.net建立xml web服務(wù)提供了一個高級的可編程模板。雖然建立xml web服務(wù)并不限定使用特定的服務(wù)平臺,但是它提供許多的優(yōu)點將簡化開發(fā)過程。使用這個編程模型,開發(fā)人員甚至不需要理解http、soap或其它任何網(wǎng) 絡(luò)服務(wù)規(guī)范。 asp.net xml web服務(wù)為在internet上綁定應(yīng)用程序提供了一個利用現(xiàn)存體系架構(gòu)和應(yīng)用程序的簡單的、靈活的、基于產(chǎn)業(yè)標準的模型。
 

返回頁首
第四講 類與對象
組件編程不是對傳統(tǒng)面向?qū)ο蟮膾仐墸喾唇M件編程正是面向?qū)ο缶幊痰纳罨桶l(fā)展。類作為面向?qū)ο蟮撵`魂在c#語言里有著相當廣泛深入的應(yīng)用,很多非常“sharp”的組件特性甚至都是直接由類包裝而成。對類的深度掌握自然是我們“sharp xp”重要的一環(huán)。


c# 的類是一種對包括數(shù)據(jù)成員,函數(shù)成員和嵌套類型進行封裝的數(shù)據(jù)結(jié)構(gòu)。其中數(shù)據(jù)成員可以是常量,域。函數(shù)成員可以是方法,屬性,索引器,事件,操作符,實例 構(gòu)建器,靜態(tài)構(gòu)建器,析構(gòu)器。我們將在“第五講 構(gòu)造器與析構(gòu)器”和“第六講 域 方法 屬性與索引器”對這些成員及其特性作詳細的剖析。除了某些導(dǎo)入的外部方法,類及其成員在c#中的聲明和實現(xiàn)通常要放在一起。

c#用多種修飾符來表達類的不同性質(zhì)。根據(jù)其保護級c#的類有五種不同的限制修飾符:

1.
 public可以被任意存取;
 
2.
 protected只可以被本類和其繼承子類存取;
 
3.
 internal只可以被本組合體(assembly)內(nèi)所有的類存取,組合體是c#語言中類被組合后的邏輯單位和物理單位,其編譯后的文件擴展名往往是“.dll”或“.exe”。
 
4.
 protected internal唯一的一種組合限制修飾符,它只可以被本組合體內(nèi)所有的類和這些類的繼承子類所存取。
 
5.
 private只可以被本類所存取。
 

如果不是嵌套的類,命名空間或編譯單元內(nèi)的類只有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的實現(xiàn)
  }
}

抽象類a內(nèi)含一個抽象方法f(),它不能被實例化。類b繼承自類a,其內(nèi)包含了一個實例方法g(),但并沒有實現(xiàn)抽象方法f(),所以仍然必須聲明為抽象類。類c繼承自類b,實現(xiàn)類抽象方法f(),于是可以進行對象實例化。

sealed用來修飾類為密封類,阻止該類被繼承。同時對一個類作abstract和sealed的修飾是沒有意義的,也是被禁止的。

對象與this關(guān)鍵字
類 與對象的區(qū)分對我們把握oo編程至關(guān)重要。我們說類是對其成員的一種封裝,但類的封裝設(shè)計僅僅是我們編程的第一步,對類進行對象實例化,并在其數(shù)據(jù)成員上 實施操作才是我們完成現(xiàn)實任務(wù)的根本。實例化對象采用myclass myobject=new myclass()語法,這里的new語義將調(diào)用相應(yīng)的構(gòu)建器。c#所有的對象都將創(chuàng)建在托管堆上。實例化后的類型我們稱之為對象,其核心特征便是擁有了 一份自己特有的數(shù)據(jù)成員拷貝。這些為特有的對象所持有的數(shù)據(jù)成員我們稱之為實例成員。相反那些不為特有的對象所持有的數(shù)據(jù)成員我們稱之為靜態(tài)成員,在類中 用static修飾符聲明。僅對靜態(tài)數(shù)據(jù)成員實施操作的稱為靜態(tài)函數(shù)成員。c#中靜態(tài)數(shù)據(jù)成員和函數(shù)成員只能通過類名引用獲取,看下面的代碼:

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引用。對于靜態(tài)成員name和g()我們只能通過類型a來引用,而不可以這樣a1.name,或a1.g()。

在 上面的程序中,我們看到在實例方法f()中我們才用this來引用變量count。這里的this是什么意思呢?this 關(guān)鍵字引用當前對象實例的成員。在實例方法體內(nèi)我們也可以省略this,直接引用count,實際上兩者的語義相同。理所當然的,靜態(tài)成員函數(shù)沒有 this 指針。this 關(guān)鍵字一般用于從構(gòu)造函數(shù)、實例方法和實例訪問器中訪問成員。

在構(gòu)造函數(shù)中this用于限定被相同的名稱隱藏的成員,例如:

class employee
{
public employee(string name, string alias)
{
   this.name = name;
   this.alias = alias;
}
}

將對象作為參數(shù)傳遞到其他方法時也要用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關(guān)鍵字表示,兩者完全等同。自然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的兩個靜態(tài)方法equals(object obja,object objb),referenceequals(object obja,object objb)和一個實例方法equals(object obj)。在我們闡述這兩個方法之前我們首先要清楚面向?qū)ο缶幊虄蓚€重要的相等概念:值相等和引用相等。值相等的意思是它們的數(shù)據(jù)成員按內(nèi)存位分別相等。 引用相等則是指它們指向同一個內(nèi)存地址,或者說它們的對象句柄相等。引用相等必然推出值相等。對于值類型關(guān)系等號“= =”判斷兩者是否值相等(結(jié)構(gòu)類型和枚舉類型沒有定義關(guān)系等號“= =”,我們必須自己定義)。對于引用類型關(guān)系等號“= =”判斷兩者是否引用相等。值類型在c#里通常沒有引用相等的表示,只有在非托管編程中采用取地址符“&”來間接判斷二者的地址是否相等。

靜 態(tài)方法equals(object obja,object objb)首先檢查兩個對象obja和objb是否都為null,如果是則返回true,否則進行obja.equals(objb)調(diào)用并返回其值。問 題歸結(jié)到實例方法equals(object obj)。該方法缺省的實現(xiàn)其實就是{return this= =obj;}也就是判斷兩個對象是否引用相等。但我們注意到該方法是一個虛方法,c#推薦我們重寫此方法來判斷兩個對象是否值相等。實際上 microsoft.net框架類庫內(nèi)提供的許多類型都重寫了該方法,如:system.string(string),system.int32 (int)等,但也有些類型并沒有重寫該方法如:system.array等,我們在使用時一定要注意。對于引用類型,如果沒有重寫實例方法equals (object obj),我們對它的調(diào)用相當于this= =obj,即引用相等判斷。所有的值類型(隱含繼承自system.valuetype類)都重寫了實例方法equals(object obj)來判斷是否值相等。

注意對于對象x,x.equals(null)返回false,這里x顯然不能為null(否則不能完成 equals()調(diào)用,系統(tǒng)拋出空引用錯誤)。從這里我們也可看出設(shè)計靜態(tài)方法equals(object obja,object objb)的原因了--如果兩個對象obja和objb都可能為null,我們便只能用object. equals(object obja,object objb)來判斷它們是否值相等了--當然如果我們沒有改寫實例方法equals(object obj),我們得到的仍是引用相等的結(jié)果。我們可以實現(xiàn)接口icomparable(有關(guān)接口我們將在“第七講 接口 繼承與多態(tài)”里闡述)來強制改寫實例方法equals(object obj)。

對于值類型,實例方法equals(object obj)應(yīng)該和關(guān)系等號“= =”的返回值一致,也就是說如果我們重寫了實例方法equals(object obj),我們也應(yīng)該重載或定義關(guān)系等號“= =”操作符,反之亦然。雖然值類型(繼承自system.valuetype類)都重寫了實例方法equals(object obj),但c#推薦我們重寫自己的值類型的實例方法equals(object obj),因為系統(tǒng)的system.valuetype類重寫的很低效。對于引用類型我們應(yīng)該重寫實例方法equals(object obj)來表達值相等,一般不應(yīng)該重載關(guān)系等號“= =”操作符,因為它的缺省語義是判斷引用相等。

靜態(tài)方法referenceequals(object obja,object objb)判斷兩個對象是否引用相等。如果兩個對象為引用類型,那么它的語義和沒有重載的關(guān)系等號“= =”操作符相同。如果兩個對象為值類型,那么它的返回值一定是false。

實 例方法gethashcode()為相應(yīng)的類型提供哈希(hash)碼值,應(yīng)用于哈希算法或哈希表中。需要注意的是如果我們重寫了某類型的實例方法 equals(object obj),我們也應(yīng)該重寫實例方法gethashcode()--這理所應(yīng)當,兩個對象的值相等,它們的哈希碼也應(yīng)該相等。下面的代碼是對前面幾個方法的 一個很好的示例:

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的語義相同,它們都通過查詢對象的元數(shù)據(jù)來確定對象的運行時類型,我們在“第十講 特征與映射”對此作詳細的闡述。

實例方法tostring()返回對象的字符串表達形式。如果我們沒有重寫該方法,系統(tǒng)一般將類型名作為字符串返回。

受保護的finalize()方法在c#中有特殊的語義,我們將在“第五講 構(gòu)造器與析構(gòu)器”里詳細闡述。

受 保護的memberwiseclone()方法返回目前對象的一個“影子拷貝”,該方法不能被子類重寫。“影子拷貝”僅僅是對象的一份按位拷貝,其含義是 對對象內(nèi)的值類型變量進行賦值拷貝,對其內(nèi)的引用類型變量進行句柄拷貝,也就是拷貝后的引用變量將持有對同一塊內(nèi)存的引用。相對于“影子拷貝”的是深度拷 貝,它對引用類型的變量進行的是值復(fù)制,而非句柄復(fù)制。例如x是一個含有對象a,b引用的對象,而對象a又含有對象m的引用。y是x的一個“影子拷貝”。 那么y將擁有同樣的a,b的引用。但對于x的一個“深度拷貝”z來說,它將擁有對象c和d的引用,以及一個間接的對象n的引用,其中c是a的一份拷貝,d 是b的一份拷貝,n是m的一份拷貝。深度拷貝在c#里通過實現(xiàn)icloneable接口(提供clone()方法)來完成。

對對象和system.object的把握為類的學(xué)習(xí)作了一個很好的鋪墊,但這僅僅是我們銳利之行的一小步,關(guān)乎對象成員初始化,內(nèi)存引用的釋放,繼承與多態(tài),異常處理等等諸多“sharp”特技堪為浩瀚,讓我們繼續(xù)期待下面的專題!

返回頁首
第五講 構(gòu)造器與析構(gòu)器
構(gòu)造器
構(gòu) 造器負責類中成員變量(域)的初始化。c#的類有兩種構(gòu)造器:實例構(gòu)造器和靜態(tài)構(gòu)造器。實例構(gòu)造器負責初始化類中的實例變量,它只有在用戶用new關(guān)鍵字 為對象分配內(nèi)存時才被調(diào)用。而且作為引用類型的類,其實例化后的對象必然是分配在托管堆(managed heap)上。這里的托管的意思是指該內(nèi)存受.net的clr運行時管理。和c++不同的是,c#中的對象不可以分配在棧中,用戶只聲明對象是不會產(chǎn)生構(gòu) 造器調(diào)用的。

實例構(gòu)造器分為缺省構(gòu)造器和非缺省構(gòu)造器。缺省構(gòu)造器是在一個類沒有聲明任何構(gòu)造器的情況下,編譯器強制為該類添加的一個 無參數(shù)的構(gòu)造器,該構(gòu)造器僅僅調(diào)用父類的無參數(shù)構(gòu)造器。缺省構(gòu)造器實際上是c#編譯器為保證每一個類都有至少一個構(gòu)造器而采取的附加規(guī)則。注意這里的三個 要點:

1.
 子類沒有聲明任何構(gòu)造器;
 
2.
 編譯器為子類加的缺省構(gòu)造器一定為無參數(shù)的構(gòu)造器;
 
3.
 父類一定要存在一個無參數(shù)的構(gòu)造器。
 

看下面例子的輸出:

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的無參構(gòu)造器public myclass1()看看編譯結(jié)果。

構(gòu) 造器在繼承時需要特別的注意,為了保證父類成員變量的正確初始化,子類的任何構(gòu)造器默認的都必須調(diào)用父類的某一構(gòu)造器,具體調(diào)用哪個構(gòu)造器要看構(gòu)造器的初 始化參數(shù)列表。如果沒有初始化參數(shù)列表,那么子類的該構(gòu)造器就調(diào)用父類的無參數(shù)構(gòu)造器;如果有初始化參數(shù)列表,那么子類的該構(gòu)造器就調(diào)用父類對應(yīng)的參數(shù)構(gòu) 造器。看下面例子的輸出:

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#支持變量的聲明初始化。類內(nèi)的成員變量聲明初始化被編譯器轉(zhuǎn)換成賦值語句強加在類的每一個構(gòu)造器的內(nèi)部。那么初始化語句與調(diào)用父類構(gòu)造器的語句的順序是什么呢?看下面例子的輸出:

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

容易看到初始化語句在父類構(gòu)造器調(diào)用之前,最后執(zhí)行的才是本構(gòu)造器內(nèi)的語句。也就是說變量初始化的優(yōu)先權(quán)是最高的。

我 們看到類的構(gòu)造器的聲明中有public修飾符,那么當然也可以有protected/private/ internal修飾符。根據(jù)修飾符規(guī)則,我們?nèi)绻麑⒁粋€類的構(gòu)造器修飾為private,那么我們在繼承該類的時候,我們將不能對這個private的 構(gòu)造器進行調(diào)用,我們是否就不能對它進行繼承了嗎?正是這樣。實際上這樣的類在我們的類內(nèi)的成員變量都是靜態(tài)(static)時,而又不想讓類的用戶對它 進行實例化,這時必須屏蔽編譯器為我們暗中添加的構(gòu)造器(編譯器添加的構(gòu)造器都為public),就很有必要作一個private的實例構(gòu)造器了。 protected/internal也有類似的用法。

類的構(gòu)造器沒有返回值,這一點是不言自明的。

靜態(tài)構(gòu)造器初始化類中的靜態(tài)變量。靜態(tài)構(gòu)造器不象實例構(gòu)造器那樣在繼承中被隱含調(diào)用,也不可以被用戶直接調(diào)用。掌握靜態(tài)構(gòu)造器的要點是掌握它的執(zhí)行時間。靜態(tài)構(gòu)造器的執(zhí)行并不確定(編譯器沒有明確定義)。但有四個準則需要掌握:

1.
 在一個程序的執(zhí)行過程中,靜態(tài)構(gòu)造器最多只執(zhí)行一次。
 
2.
 靜態(tài)構(gòu)造器在類的靜態(tài)成員初始化之后執(zhí)行。或者講編譯器會將靜態(tài)成員初始化語句轉(zhuǎn)換成賦值語句放在靜態(tài)構(gòu)造器執(zhí)行的最開始。
 
3.
 靜態(tài)構(gòu)造器在任何類的靜態(tài)成員被引用之前執(zhí)行。
 
4.
 靜態(tài)構(gòu)造器在任何類的實例變量被分配之前執(zhí)行。
 

看下面例子的輸出:

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

值得指出的是實例構(gòu)造器內(nèi)可以引用實例變量,也可引用靜態(tài)變量。而靜態(tài)構(gòu)造器內(nèi)能引用靜態(tài)變量。這在類與對象的語義下是很容易理解的。

實際上如果我們能夠深刻地把握類的構(gòu)造器的唯一目的就是保證類內(nèi)的成員變量能夠得到正確的初始化,我們對各種c#中形形色色的構(gòu)造器便有會心的理解--它沒有理由不這樣!

析構(gòu)器
由 于.net平臺的自動垃圾收集機制,c#語言中類的析構(gòu)器不再如傳統(tǒng)c++那么必要,析構(gòu)器不再承擔對象成員的內(nèi)存釋放--自動垃圾收集機制保證內(nèi)存的回 收。實際上c#中已根本沒有delete操作!析構(gòu)器只負責回收處理那些非系統(tǒng)的資源,比較典型的如:打開的文件,獲取的窗口句柄,數(shù)據(jù)庫連接,網(wǎng)絡(luò)連接 等等需要用戶自己動手釋放的非內(nèi)存資源。我們看下面例子的輸出:

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

其 中程序中最后兩句是保證類的析構(gòu)器得到調(diào)用。gc.collect()是強迫通用語言運行時進行啟動垃圾收集線程進行回收工作。而 gc.waitforpendingfinalizers()是掛起目前的線程等待整個終止化(finalizaion)操作的完成。終止化 (finalizaion)操作保證類的析構(gòu)器被執(zhí)行,這在下面會詳細說明。

析構(gòu)器不會被繼承,也就是說類內(nèi)必須明確的聲明析構(gòu)器,該 類才存在析構(gòu)器。用戶實現(xiàn)析構(gòu)器時,編譯器自動添加調(diào)用父類的析構(gòu)器,這在下面的finalize方法中會詳細說明。析構(gòu)器由于垃圾收集機制會被在合適的 的時候自動調(diào)用,用戶不能自己調(diào)用析構(gòu)器。只有實例析構(gòu)器,而沒有靜態(tài)析構(gòu)器。

那么析構(gòu)器是怎么被自動調(diào)用的?這在 .net垃圾回收機制由一種稱作終止化(finalizaion)的操作來支持。.net系統(tǒng)缺省的終止化操作不做任何操作,如果用戶需要釋放非受管資 源,用戶只要在析構(gòu)器內(nèi)實現(xiàn)這樣的操作即可--這也是c#推薦的做法。我們看下面這段代碼:

using system;
class myclass1
{
~myclass1()
{
console.writleline("myclass1 destructor");
}
}

而實際上,從生成的中間代碼來看我們可以發(fā)現(xiàn),這些代碼被轉(zhuǎn)化成了下面的代碼:

using system;
class myclass1
{
protected override void finalize()
{
try
{
console.writleline("my class1 destructor");
}
finally
{
base.finalize();
}
}
}

實 際上c#編譯器不允許用戶自己重載或調(diào)用finalize方法--編譯器徹底屏蔽了父類的finalize方法(由于c#的單根繼承性質(zhì), 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方法已經(jīng)徹底脫離了“終止化操作”的語義,而成為c#語言的一個一般方法了。值得注意的是這也屏蔽了父類system.object的finalize方法,所以要格外小心!

終 止化操作在.net運行時里有很多限制,往往不被推薦實現(xiàn)。當對一個對象實現(xiàn)了終止器(finalizer)后,運行時便會將這個對象的引用加入一個稱作 終止化對象引用集的隊列,作為要求終止化的標志。當垃圾收集開始時,若一個對象不再被引用但它被加入了終止化對象引用集的隊列,那么運行時并不立即對此對 象進行垃圾收集工作,而是將此對象標志為要求終止化操作對象。待垃圾收集完成后,終止化線程便會被運行時喚醒執(zhí)行終止化操作。顯然這之后要從終止化對象引 用集的鏈表中將之刪去。而只有到下一次的垃圾收集時,這個對象才開始真正的垃圾收集,該對象的內(nèi)存資源才被真正回收。容易看出來,終止化操作使垃圾收集進 行了兩次,這會給系統(tǒng)帶來不小的額外開銷。終止化是通過啟用線程機制來實現(xiàn)的,這有一個線程安全的問題。.net運行時不能保證終止化執(zhí)行的順序,也就是 說如果對象a有一個指向?qū)ο骲的引用,兩個對象都有終止化操作,但對象a在終止化操作時并不一定有有效的對象a引用。.net運行時不允許用戶在程序運行 中直接調(diào)用finalize()方法。如果用戶迫切需要這樣的操作,可以實現(xiàn)idisposable接口來提供公共的dispose()方法。需要說明的 是提供了dispose()方法后,依然需要提供finalize方法的操作,即實現(xiàn)假托的析構(gòu)函數(shù)。因為dispose()方法并不能保證被調(diào)用。所 以.net運行時不推薦對對象進行終止化操作即提供析構(gòu)函數(shù),只是在有非受管資源如數(shù)據(jù)庫的連接,文件的打開等需要嚴格釋放時,才需要這樣做。

大 多數(shù)時候,垃圾收集應(yīng)該交由.net運行時來控制,但有些時候,可能需要人為地控制一下垃圾回收操作。例如在操作了一次大規(guī)模的對象集合后,我們確信不再 在這些對象上進行任何的操作了,那我們可以強制垃圾回收立即執(zhí)行,這通過調(diào)用system.gc.collect() 方法即可實現(xiàn),但頻繁的收集會顯著地降低系統(tǒng)的性能。還有一種情況,已經(jīng)將一個對象放到了終止化對象引用集的鏈上了,但如果我們在程序中某些地方已經(jīng)做了 終止化的操作,即明確調(diào)用了dispose()方法,在那之后便可以通過調(diào)用system.gc.supressfinalize()來將對象的引用從終 止化對象引用集鏈上摘掉,以忽略終止化操作。終止化操作的系統(tǒng)負擔是很重的。

在深入了解了.net運行時的自動垃圾收集功能后,我們便會領(lǐng)會c#中的析構(gòu)器為什么繞了這么大的彎來實現(xiàn)我們的編程需求,才能把內(nèi)存資源和非內(nèi)存資源的回收做的游刃有余--這也正是析構(gòu)的本原!

返回頁首
第六講 方法
方 法又稱成員函數(shù)(member function),集中體現(xiàn)了類或?qū)ο蟮男袨椤7椒ㄍ瑯臃譃殪o態(tài)方法和實例方法。靜態(tài)方法只可以操作靜態(tài)域,而實例方法既可以操作實例域,也可以操作靜 態(tài)域--雖然這不被推薦,但在某些特殊的情況下會顯得很有用。方法也有如域一樣的5種存取修飾符--public,protected, internal,protected internal,private,它們的意義如前所述。

方法參數(shù)
方法的參數(shù)是個值得特 別注意的地方。方法的參數(shù)傳遞有四種類型:傳值(by value),傳址(by reference),輸出參數(shù)(by output),數(shù)組參數(shù)(by array)。傳值參數(shù)無需額外的修飾符,傳址參數(shù)需要修飾符ref,輸出參數(shù)需要修飾符out,數(shù)組參數(shù)需要修飾符params。傳值參數(shù)在方法調(diào)用過 程中如果改變了參數(shù)的值,那么傳入方法的參數(shù)在方法調(diào)用完成以后并不因此而改變,而是保留原來傳入時的值。傳址參數(shù)恰恰相反,如果方法調(diào)用過程改變了參數(shù) 的值,那么傳入方法的參數(shù)在調(diào)用完成以后也隨之改變。實際上從名稱上我們可以清楚地看出兩者的含義--傳值參數(shù)傳遞的是調(diào)用參數(shù)的一份拷貝,而傳址參數(shù)傳 遞的是調(diào)用參數(shù)的內(nèi)存地址,該參數(shù)在方法內(nèi)外指向的是同一個存儲位置。看下面的例子及其輸出:

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);
}
}

程序經(jīng)編譯后執(zhí)行輸出:

i = 2, j = 1
i = 2, j = 1

我們可以清楚地看到兩個交換函數(shù)swap()由于參數(shù)的差別--傳值與傳址,而得到不同的調(diào)用結(jié)果。注意傳址參數(shù)的方法調(diào)用無論在聲明時還是調(diào)用時都要加上ref修飾符。

籠統(tǒng)地說傳值不會改變參數(shù)的值在有些情況下是錯誤的,我們看下面一個例子:

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);
}
}

程序經(jīng)編譯后執(zhí)行輸出:

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);
}
}

程序經(jīng)編譯后執(zhí)行輸出:

10
10

傳 值方式根本沒有改變類型為element類的對象t!實際上,如果我們能夠理解類這一c#中的引用類型(reference type)的特性,我們便能看出上面兩個例子差別!在傳值過程中,引用類型本身不會改變(t不會改變),但引用類型內(nèi)含的域卻會改變(t.number改 變了)!c#語言的引用類型有:object類型(包括系統(tǒng)內(nèi)建的class類型和用戶自建的class類型--繼承自object類型),string 類型,interface類型,array類型,delegate類型。它們在傳值調(diào)用中都有上面兩個例子展示的特性。

在傳值和傳址情 況下,c#強制要求參數(shù)在傳入之前由用戶明確初始化,否則編譯器報錯!但我們?nèi)绻幸粋€并不依賴于參數(shù)初值的函數(shù),我們只是需要函數(shù)返回時得到它的值是該 怎么辦呢?往往在我們的函數(shù)返回值不至一個時我們特別需要這種技巧。答案是用out修飾的輸出參數(shù)。但需要記住輸出參數(shù)與通常的函數(shù)返回值有一定的區(qū)別: 函數(shù)返回值往往存在堆棧里,在返回時彈出;而輸出參數(shù)需要用戶預(yù)先制定存儲位置,也就是用戶需要提前聲明變量--當然也可以初始化。看下面的例子:

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);
}
}

程序經(jīng)編譯后執(zhí)行輸出:

my first name: cornfield, my last name: lee

在 函數(shù)體內(nèi)所有輸出參數(shù)必須被賦值,否則編譯器報錯!out修飾符同樣應(yīng)該應(yīng)用在函數(shù)聲明和調(diào)用兩個地方,除了充當返回值這一特殊的功能外,out修飾符 ref修飾符有很相似的地方:傳址。我們可以看出c#完全擯棄了傳統(tǒng)c/c++語言賦予程序員莫大的自由度,畢竟c#是用來開發(fā)高效的下一代網(wǎng)絡(luò)平臺,安 全性--包括系統(tǒng)安全(系統(tǒng)結(jié)構(gòu)的設(shè)計)和工程安全(避免程序員經(jīng)常犯的錯誤)是它設(shè)計時的重要考慮,當然我們看到c#并沒有因為安全性而喪失多少語言的 性能,這正是c#的卓越之處,“sharp”之處!

數(shù)組參數(shù)也是我們經(jīng)常用到的一個地方--傳遞大量的數(shù)組集合參數(shù)。我們先看下面的例子:

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));
}
}

程序經(jīng)編譯后執(zhí)行輸出:

the sum:15
the sum:150

可以看出,數(shù)組參數(shù)可以是數(shù)組如:var,也可以是能夠隱式轉(zhuǎn)化為數(shù)組的參數(shù)如:10,20,30,40,50。這為我們的程序提供了很高的擴展性。

同名方法參數(shù)的不同會導(dǎo)致方法出現(xiàn)多態(tài)現(xiàn)象,這又叫重載(overloading)方法。需要指出的是編譯器是在編譯時便綁定了方法和方法調(diào)用。只能通過參數(shù)的不同來重載方法,其他的不同(如返回值)不能為編譯器提供有效的重載信息。

方法繼承
第 一等的面向?qū)ο髾C制為c#的方法引入了virtual,override,sealed,abstract四種修飾符來提供不同的繼承需求。類的虛方法是 可以在該類的繼承自類中改變其實現(xiàn)的方法,當然這種改變僅限于方法體的改變,而非方法頭(方法聲明)的改變。被子類改變的虛方法必須在方法頭加上 override來表示。當一個虛方法被調(diào)用時,該類的實例--亦即對象的運行時類型(run-time type)來決定哪個方法體被調(diào)用。我們看下面的例子:

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();
}
}

程序經(jīng)編譯后執(zhí)行輸出:

parent.f
child.f
child.g
child.g

我 們可以看到class child中f()方法的聲明采取了重寫(new)的辦法來屏蔽class parent中的非虛方法f()的聲明。而g()方法就采用了覆蓋(override)的辦法來提供方法的多態(tài)機制。需要注意的是重寫(new)方法和覆 蓋(override)方法的不同,從本質(zhì)上講重寫方法是編譯時綁定,而覆蓋方法是運行時綁定。值得指出的是虛方法不可以是靜態(tài)方法--也就是說不可以用 static和virtual同時修飾一個方法,這由它的運行時類型辨析機制所決定。override必須和virtual配合使用,當然也不能和 static同時使用。

那么我們?nèi)绻谝粋€類的繼承體系中不想再使一個虛方法被覆蓋,我們該怎樣做呢?答案是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)方法在邏輯上類似于虛方法,只是不能像虛方法那樣被調(diào)用,而只是一個接口的聲明而非實現(xiàn)。抽象方法沒有類似于{…}這樣的方法實 現(xiàn),也不允許這樣做。抽象方法同樣不能是靜態(tài)的。含有抽象方法的類一定是抽象類,也一定要加abstract類修飾符。但抽象類并不一定要含有抽象方法。 繼承含有抽象方法的抽象類的子類必須覆蓋并實現(xiàn)(直接使用override)該方法,或者組合使用abstract override使之繼續(xù)抽象,或者不提供任何覆蓋和實現(xiàn)。后兩者的行為是一樣的。看下面的例子:

//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");
}
}

歸根結(jié)底,我們抓住了運行時綁定和編譯時綁定的基本機理,我們便能看透方法呈現(xiàn)出的種種overload,virtual,override,sealed,abstract等形態(tài),我們才能運用好方法這一利器!

外部方法
c#引入了extern修飾符來表示外部方法。外部方法是用c#以外的語言實現(xiàn)的方法如win32 api函數(shù)。如前所是外部方法不能是抽象方法。我們看下面的一個例子:

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);
}
}

程序經(jīng)編譯后執(zhí)行輸出:


這里我們調(diào)用了win32 api函數(shù)int messageboxa(int hwnd, string msg,string caption, int type)。

返回頁首
第七講 域與屬性

域(field) 又稱成員變量(member variable),它表示存儲位置,是c#中類不可缺少的一部分。域的類型可以是c#中任何數(shù)據(jù)類型。但對于除去string類型的其他引用類型由于在 初始化時涉及到一些類的構(gòu)造器的操作,我們這里將不提及,我們把這一部分內(nèi)容作為“類的嵌套”放在“接口 繼承與多態(tài)”一講內(nèi)來闡述。

域 分為實例域和靜態(tài)域。實例域?qū)儆诰唧w的對象,為特定的對象所專有。靜態(tài)域?qū)儆陬悾瑸樗袑ο笏灿谩#嚴格規(guī)定實例域只能通過對象來獲取,靜態(tài)域只能通 過類來獲取。例如我們有一個類型為myclass的對象myobject,myclass內(nèi)的實例域instancefield(存取限制為 public)只能這樣獲取:myobject. instancefield。而myclass的靜態(tài)域staticfield(存取限制為public)只能這樣獲取: myclass.staticfield。注意靜態(tài)域不能像傳統(tǒng)c++那樣通過對象獲取,也就是說myobject.staticfield的用法是錯誤 的,不能通過編譯器編譯。

域的存取限制集中體現(xiàn)了面向?qū)ο缶幊痰姆庋b原則。如前所述,c#中的存取限制修飾符有5種,這5種對域都適 用。c#只是用internal擴展了c++原來的friend修飾符。在有必要使兩個類的某些域互相可見時,我們將這些類的域聲明為internal, 然后將它們放在一個組合體內(nèi)編譯即可。如果需要對它們的繼承子類也可見的話,聲明為protected internal即可。實際上這也是組合體的本來意思--將邏輯相關(guān)的類組合封裝在一起。

c#引入了readonly修飾符來表示只讀 域,const來表示不變常量。顧名思義對只讀域不能進行寫操作,不變常量不能被修改,這兩者到底有什么區(qū)別呢?只讀域只能在初始化--聲明初始化或構(gòu)造 器初始化--的過程中賦值,其他地方不能進行對只讀域的賦值操作,否則編譯器會報錯。只讀域可以是實例域也可以是靜態(tài)域。只讀域的類型可以是c#語言的任 何類型。但const修飾的常量必須在聲明的同時賦值,而且要求編譯器能夠在編譯時期計算出這個確定的值。const修飾的常量為靜態(tài)變量,不能夠為對象 所獲取。const修飾的值的類型也有限制,它只能為下列類型之一(或能夠轉(zhuǎn)換為下列類型的):sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, string, enum類型, 或引用類型。值得注意的是這里的引用類型,由于除去string類型外,所有的類型出去null值以外在編譯時期都不能由編譯器計算出他們的確切的值,所 以我們能夠聲明為const的引用類型只能為string或值為null的其他引用類型。顯然當我們聲明一個null的常量時,我們已經(jīng)失去了聲明的意義 --這也可以說是c#設(shè)計的尷尬之處!

這就是說,當我們需要一個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內(nèi)的域myfield聲明為static readonly時,如果我們由于某種需要改變了myfield的值為20,我們只需重新編譯文件file1.cs為file1.dll,在執(zhí)行 file2.exe時我們會得到20。但如果我們將static readonly改變?yōu)閏onst后,再改變myfield的初始化值時,我們必須重新編譯所有引用到file1.dll的文件,否則我們引用的 mynamespace1.myclass1.myfield將不會如我們所愿而改變。這在大的系統(tǒng)開發(fā)過程中尤其需要注意。實際上,如果我們能夠理解 const修飾的常量是在編譯時便被計算出確定的值,并代換到引用該常量的每一個地方,而readonly時在運行時才確定的量--只是在初始化后我們不 希望它的值再改變,我們便能理解c#設(shè)計者們的良苦用心,我們才能徹底把握const和readonly的行為!

域的初始化是面向?qū)ο?編程中一個需要特別注意的問題。c#編譯器缺省將每一個域初始化為它的默認值。簡單的說,數(shù)值類型(枚舉類型)的默認值為0或0.0。字符類型的默認值為 '/x0000'。布爾類型的默認值為false。引用類型的默認值為null。結(jié)構(gòu)類型的默認值為其內(nèi)的所有類型都取其相應(yīng)的默認值。雖然c#編譯器為 每個類型都設(shè)置了默認類型,但作為面向?qū)ο蟮脑O(shè)計原則,我們還是需要對變量進行正確的初始化。實際上這也是c#推薦的做法,沒有對域進行初始化會導(dǎo)致編譯 器發(fā)出警告信息。c#中對域進行初始化有兩個地方--聲明的同時進行初始化和在構(gòu)造器內(nèi)進行初始化。如前所述,域的聲明初始化實際上被編譯器作為賦值語句 放在了構(gòu)造器的內(nèi)部的最開始處執(zhí)行。實例變量初始化會被放在實例構(gòu)造器內(nèi),靜態(tài)變量初始化會被放在靜態(tài)構(gòu)造器內(nèi)。如果我們聲明了一個靜態(tài)的變量并同時對之 進行了初始化,那么編譯器將為我們構(gòu)造出一個靜態(tài)構(gòu)造器來把這個初始化語句變成賦值語句放在里面。而作為const修飾的常量域,從嚴格意義上講不能算作 初始化語句,我們可以將它看作類似于c++中的宏代換。

屬性
屬性可以說是c#語言的一個創(chuàng)新。當然你也可以說不是。不是的原因 是它背后的實現(xiàn)實際上還是兩個函數(shù)--一個賦值函數(shù)(get),一個取值函數(shù)(set),這從它生成的中間語言代碼可以清晰地看到。是的原因是它的的確確 在語言層面實現(xiàn)了面向?qū)ο缶幊桃恢币詠韺Α皩傩浴边@一oo風格的類的特殊接口的訴求。理解屬性的設(shè)計初衷是我們用好屬性這一工具的根本。c#不提倡將域的 保護級別設(shè)為public而使用戶在類外任意操作--那樣太不oo,或者具體點說太不安全!對所有有必要在類外可見的域,c#推薦采用屬性來表達。屬性不 表示存儲位置,這是屬性和域的根本性的區(qū)別。下面是一個典型的屬性設(shè)計:

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#的關(guān)鍵字,是我們進行屬性操作時的set的隱含參數(shù),也就是我們在執(zhí)行屬性寫操作時的右值。

屬性提供了只讀(get),只寫(set),讀寫(get和 set)三種接口操作。對域的這三種操作,我們必須在同一個屬性名下聲明,而不可以將它們分離,看下面的實現(xiàn):

class myclass
{
private string name;
public string name
{          
get { return name; }
}
public string name
{         
set { name = value; }
}
}

上面這種分離name屬性實現(xiàn)的方法是錯誤的!我們應(yīng)該像前面的例子一樣將他們放在一起。值得注意的是三種屬性(只讀,只寫,讀寫)被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{}的定義。

當然屬性遠遠不止僅僅限于域的接口操作,屬性的本質(zhì)還是方法,我們可以根據(jù)程序邏輯在屬性的提取或賦值時進行某些檢查,警告等額外操作,看下面的例子:

class myclass
{
private string name;
public string name
{          
get { return name; }
set
{
if (value==null)
name="microsoft";
else
name=value;
}
}
}

由 于屬性的方法的本質(zhì),屬性當然也有方法的種種修飾。屬性也有5種存取修飾符,但屬性的存取修飾往往為public,否則我們也就失去了屬性作為類的公共接 口的意義。除了方法的多參數(shù)帶來的方法重載等特性屬性不具備外, virtual, sealed, override, abstract等修飾符對屬性與方法同樣的行為,但由于屬性在本質(zhì)上被實現(xiàn)為兩個方法,它的某些行為需要我們注意。看下面的例子:

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關(guān)鍵字來引用父類a的屬性。類b中可以只通過y-set便覆蓋了類a中的虛屬性。

靜態(tài)屬性和靜態(tài)方法一樣只能存取類的靜態(tài)域變量。我們也可以像做外部方法那樣,聲明外部屬性。

返回頁首
第八講 索引器與操作符重載
索引器
索 引器(indexer)是c#引入的一個新型的類成員,它使得對象可以像數(shù)組那樣被方便,直觀的引用。索引器非常類似于我們前面講到的屬性,但索引器可以 有參數(shù)列表,且只能作用在實例對象上,而不能在類上直接作用。下面是典型的索引器的設(shè)計,我們在這里忽略了具體的實現(xiàn)。

class myclass
{
    public object this [int index]
    {
        get
        {
            // 取數(shù)據(jù)
        }
        set
        {
            // 存數(shù)據(jù)
        }
    }
}

索引器沒有像屬性和方法那樣的名字,關(guān)鍵字this清楚地表達了索引器引用對象的特征。和屬性一樣,value關(guān)鍵字在set后的語句塊里有參數(shù)傳遞意義。實際上從編譯后的il中間語言代碼來看,上面這個索引器被實現(xiàn)為:

class myclass
{
    public object get_item(int index)
    {
          // 取數(shù)據(jù)
    }
    public void set_item(int index, object value)
 {
//存數(shù)據(jù)
    }
}

由 于我們的索引器在背后被編譯成get_item(int index)和set_item(int index, object value)兩個方法,我們甚至不能再在聲明實現(xiàn)索引器的類里面聲明實現(xiàn)這兩個方法,編譯器會對這樣的行為報錯。這樣隱含實現(xiàn)的方法同樣可以被我們進行調(diào) 用,繼承等操作,和我們自己實現(xiàn)的方法別無二致。通曉c#語言底層的編譯實現(xiàn)為我們下面理解c#索引器的行為提供了一個很好的基礎(chǔ)。

和 方法一樣,索引器有5種存取保護級別,和4種繼承行為修飾,以及外部索引器。這些行為同方法沒有任何差別,這里不再贅述。唯一不同的是索引器不能為靜態(tài) (static),這在對象引用的語義下很容易理解。值得注意的是在覆蓋(override)實現(xiàn)索引器時,應(yīng)該用base[e]來存取父類的索引器。

和屬性的實現(xiàn)一樣,索引器的數(shù)據(jù)類型同時為get語句塊的返回類型和set語句塊中value關(guān)鍵字的類型。

索引器的參數(shù) 列表也是值得注意的地方。“索引”的特征使得索引器必須具備至少一個參數(shù),該參數(shù)位于this關(guān)鍵字之后的中括號內(nèi)。索引器的參數(shù)也只能是傳值類型,不可 以有ref(引用)和out(輸出)修飾。參數(shù)的數(shù)據(jù)類型可以是c#中的任何數(shù)據(jù)類型。c#根據(jù)不同的參數(shù)簽名來進行索引器的多態(tài)辨析。中括號內(nèi)的所有參 數(shù)在get和set下都可以引用,而value關(guān)鍵字只能在set下作為傳遞參數(shù)。

下面是一個索引器的具體的應(yīng)用例子,它對我們理解索引器的設(shè)計和應(yīng)用很有幫助。

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數(shù)組,同時又大大降低了程序的存儲空間代價。索引器通常用于對象容器中為其內(nèi)的對象提供友好 的存取界面--這也是為什么c#將方法包裝成索引器的原因所在。實際上,我們可以看到索引器在.net framework類庫中有大量的應(yīng)用。

操作符重載
操 作符是c#中用于定義類的實例對象間表達式操作的一種成員。和索引器類似,操作符仍然是對方法實現(xiàn)的一種邏輯界面抽象,也就是說在編譯成的il中間語言代 碼中,操作符仍然是以方法的形式調(diào)用的。在類內(nèi)定義操作符成員又叫操作符重載。c#中的重載操作符共有三種:一元操作符,二元操作符和轉(zhuǎn)換操作符。并不是 所有的操作符都可以重載,三種操作符都有相應(yīng)的可重載操作符集,列于下表:

一元操作符 + - ! ~ ++ -- true false
二元操作符 + - * / % & | ^ << >> == != > < >= <=
轉(zhuǎn)換操作符 隱式轉(zhuǎn)換()和顯式轉(zhuǎn)換()

重 載操作符必須是public和static 修飾的,否則會引起編譯錯誤,這在操作符的邏輯語義下是不言而喻的。父類的重載操作符會被子類繼承,但這種繼承沒有覆蓋,隱藏,抽象等行為,不能對重載操 作符進行virtual sealed override abstract修飾。操作符的參數(shù)必須為傳值參數(shù)。我們下面來看一個具體的例子:

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

我 們這里實現(xiàn)了一個“+”號二元操作符,一個“-”號一元操作符(取負值),和一個“++”一元操作符。注意這里,我們都沒有對傳進來的參數(shù)作任何改變-- 這在參數(shù)是引用類型的變量是尤其重要,雖然重載操作符的參數(shù)只能是傳值方式。而我們在返回值時,往往需要“new”一個新的變量--除了true和 false操作符。這在重載“++”和“--” 操作符時尤其顯得重要。也就是說我們做在a++時,我們將丟棄原來的a值,而取代的是新的new出來的值給a! 值得注意的是e=a++或f=++a中e的值或f的值根本與我們重載的操作符返回值沒有一點聯(lián)系!它們的值僅僅是在前置和后置的情況下獲得a的舊值或新值 而已!前置和后置的行為不難理解。

操作符重載對返回值和參數(shù)類型有著相當嚴格的要求。一元操作符中只有一個參數(shù)。操作符“++”和“- -”返回值類型和參數(shù)類型必須和聲明該操作符的類型一樣。操作符“+ - ! ~”的參數(shù)類型必須和聲明該操作符的類型一樣,返回值類型可以任意。true和false操作符的參數(shù)類型必須和聲明該操作符的類型一樣,而返回值類型必 須為bool,而且必須配對出現(xiàn)--也就是說只聲明其中一個是不對的,會引起編譯錯誤。參數(shù)類型的不同會導(dǎo)致同名的操作符的重載--實際上這是方法重載的 表現(xiàn)。

二元操作符參數(shù)必須為兩個,而且兩個必須至少有一個的參數(shù)類型為聲明該操作符的類型。返回值類型可以任意。有三對操作符也需要必 須配對聲明出現(xiàn),它們是“==”和“!=”,“>”和“<”,“>=”和“<=”。需要注, 意的是兩個參數(shù)的類型不同,雖然類型相 同但順序不同都會導(dǎo)致同名的操作符的重載。

轉(zhuǎn)換操作符為不同類型之間提供隱式轉(zhuǎn)換和顯式轉(zhuǎn)換,主要用于方法調(diào)用,轉(zhuǎn)型表達和賦值操作。 轉(zhuǎn)換操作符對其參數(shù)類型(被轉(zhuǎn)換類型)和返回值類型(轉(zhuǎn)換類型)也有嚴格的要求。參數(shù)類型和返回值類型不能相同,且兩者之間必須至少有一個和定義操作符的 類型相同。轉(zhuǎn)換操作符必須定義在被轉(zhuǎn)換類型或轉(zhuǎn)換類型任何其中一個里面。不能對系統(tǒng)定義過的轉(zhuǎn)換操作進行重新定義。兩個類型也都不能是object或接口 類型,兩者之間不能有直接或間接的繼承關(guān)系--這三種情況系統(tǒng)已經(jīng)默認轉(zhuǎn)換。我們來看一個例子:

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類型之間的隱式轉(zhuǎn)換和顯式轉(zhuǎn)換。從digit到byte的轉(zhuǎn)換為隱式轉(zhuǎn)換,轉(zhuǎn)換過程不會因為丟失任何信息而拋出異 常。從byte到digit的轉(zhuǎn)換為顯式轉(zhuǎn)換,轉(zhuǎn)換過程有可能因丟失信息而拋出異常。實際上這也為我們揭示了什么時候聲明隱式轉(zhuǎn)換,什么時候聲明顯示轉(zhuǎn)換 的設(shè)計原則。不能對同一參數(shù)類型同時聲明隱式轉(zhuǎn)換和顯式轉(zhuǎn)換。隱式轉(zhuǎn)換和顯式轉(zhuǎn)換無需配對使用--雖然c#推薦這樣做。

實際上可以看 到,對于屬性,索引器和操作符這些c#提供給我們的界面操作,都是方法的某種形式的邏輯抽象包裝,它旨在為我們定義的類型的用戶提供一個友好易用的界面- -我們完全可以通過方法來實現(xiàn)它們實現(xiàn)的功能。理解了這樣的設(shè)計初衷,我們才會恰當,正確地用好這些操作,而不致導(dǎo)致濫用和錯用。

發(fā)表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發(fā)表
主站蜘蛛池模板: 柳州市| 繁峙县| 都江堰市| 清水县| 凤城市| 汶川县| 日喀则市| 宜黄县| 广安市| 宁波市| 龙里县| 璧山县| 类乌齐县| 广水市| 贵港市| 吉隆县| 云南省| 新巴尔虎左旗| 前郭尔| 昂仁县| 郑州市| 新建县| 香港| 图木舒克市| 石门县| 大足县| 旌德县| 扶绥县| 石柱| 建瓯市| 英超| 巨野县| 武强县| 龙山县| 色达县| 麻江县| 平江县| 昌乐县| 乌兰浩特市| 军事| 洪江市|