手頭項目基本算完成了,沒有任務,公司網站升級項目被放棄,學了一段時間java現被要求熟悉dotnet,還要熟悉基于ARM處理器的嵌入式linux平臺,還有php。。。搞得這段老是忘記東西每隔10年左右,編程人員就需要花費大量的時間和精力去學習新的編程技術。在80年代是Unix和C,90年代是Windows和C++,現在又輪到了微軟的.NETFramework和C#。盡管需要學習新的技術,但由此帶來的好處卻遠高于付出的勞動。幸運的是,使用C#和.NET進行的大多數工程的分析和設計與在C++和Windows中沒有本質的變化。在本篇文章中,我將介紹如何實現由C++到C#的飛躍。
已經有許多文章介紹過C#對C++的改進,在這里我就不再重復這些問題了。在這里,我將重點討論由C++轉向C#時最大的變化:由不可管理的環境向可管理的環境的變化。此外,我還會提出一些C#編程人員容易犯的錯誤供大家參考,此外,還將說明一些C#語言的能夠影響編程的新功能。
轉向可管理的環境
C++的設計目標是低級的、與平臺無關的面向對象編程語言,C#則是一種高級的面向組件的編程語言。向可管理環境的轉變意味著你編程方式思考的重大轉變,C#不再處理細微的控制,而是讓架構幫助你處理這些重要的問題。例如,在C++中,我們就可以使用new在棧中、堆中、甚至是內存中的某一特定位置創建一個對象。
在.NET的可管理環境中,我們再不用進行那樣細微的控制了。在選擇了要創建的類型后,它的位置就是固定的了。簡單類型(ints、double和long)的對象總是被創建在棧中(除非它們是被包含在其他的對象中),類總是被創建在堆中。我們無法控制對象是創建在堆中哪個位置的,也沒有辦法得到這個地址,不能將對象放置在內存中的某一特定位置。(當然也有突破這些限制的方法,但那是很另類的方法。)我們再也不能控制對象的生存周期,C#沒有destructor。碎片收集程序會將對象所占用的內存進行回收,但這是非顯性地進行的。
正是C#的這種結構反映了其基礎架構,其中沒有多重繼承和模板,因為在一個可管理的碎片收集環境中,多重繼承是很難高效地實現的。
C#中的簡單類型僅僅是對通用語言運行庫(CLR)中類型的簡單映射,例如,C#中的int是對System.Int32的映射。C#中的數據類型不是由語言本身決定的,而是由CLR決定的。事實上,如果仍然想在C#中使用在VisualBasic中創建的對象,就必須使自己的編程習慣更符合CLR的規定。
另一方面,可管理的環境和CLR也給我們帶來了好處。除了碎片收集和所有.NET語言中統一的數據類型外,它還提供給我們一個功能強大的面向組件的編程語言,無須對后期綁定提供特別的支持,類型發現和后期綁定都是被內置在語言中的。屬性是C#語言中的第一類的成員,事件和代理也是。
可管理環境最主要的優點是.NETFramework。盡管在所有的.NET語文中都可以使用這種框架,但C#可以更好地使用.NET框架中豐富的類、接口和對象。
Traps
C#看起來與C++非常相似,這使得我們在由C++轉向C#時比較輕松,但其中也有一些容易出錯的地方。在C++中編寫得非常漂亮的代碼,在C#中會不能通過編譯,甚至會出現意想不到的結果。C#與C++之間在語法上的變化并不大,編譯器能夠發現這二者之間大部分的差異,我在這里就不再多費筆墨了,在這里我介紹幾個容易出問題的比較重要的變化:
引用類型和值類型
在C#中,值類型和引用類型數據是有區別的。簡單類型(int、long、double等)和結構屬于值類型數據,類和對象屬于引用類型數據。除非是包含在引用類型的變量中,與在C++中一樣,值類型變量的值存儲在棧中。引用類型的變量也存儲在棧中,但它的值是一個存儲在堆中的對象的地址,這一點也與C++類似。值類型變量是將自己的值傳遞給方法,而引用類型變量則將自己的指針傳遞給方法。
結構
C#中的結構與C++中有非常明顯的區別。在C++中,結構更象是類,除了缺省的繼承外,其缺省的訪問權限是public而不是private。在C#中,結構與類截然不同,它是用來封裝輕型對象的,是值類型的數據類型,在傳遞時傳送的是變量的值,而不是其地址。此外,它們也有一些不適用于類的限制,例如,它是不能繼承的,也沒有除System.ValueType之外的基本類。結構還不能定義一個缺省的constructor。
另一方面,由于結構比類的效率要高,因此它非常適合于創建輕型對象。因此,如果它的缺點對你的軟件沒有影響,使用結構比使用類效率要高得多,尤其是對于小對象而言。
所有的一切都是對象
在C#中,所有的東西都是由繼承Object得到的,包括創建的類和int、structs等值類型的變量。Object類提供了一些有用的方法,例如ToString,使用ToString的一個例子是與System.Console.WriteLine一起使用,它可以接受一個字符串和許多對象。與使用printf語句不同,要使用WriteLine,需要提供代換變量。假設myEmployee是用戶定義的Employee類的一個實例,myCounter是用戶定義的Counter類的一個實例:
Console.WriteLine("Theemployee:{0},thecountervalue:{1}", myEmployee,myCounter); |
其中的WriteLine會調用每個對象的Object.ToString方法,替換作為參數返回的變量。如果Employee類不覆蓋ToString,就會調用缺省的實現(由System.Object繼承得到的),它將把類的名字作為一個字符串返回。Counter會覆蓋ToString,返回一個整型的變量,因此,上面代碼的輸出為:
Theemployee:Employee,thecountervalue:12 |
如果向WriteLine傳遞一個整型變量會發生什么情況呢?由于不能對整型變量調用ToString,編譯器將自動將整型變量封裝在一個對象的實例中。當WriteLine調用ToString時,對象就會返回表示整型變量值的字符串。下面的代碼就說明了這個問題:
類的使用
usingSystem; //不覆蓋ToString的類 publicclassEmployee { } //覆蓋了ToString的類 publicclassCounter { privateinttheVal; publicCounter(inttheVal) { this.theVal=theVal; } publicoverridestringToString() { Console.WriteLine("CallingCounter.ToString()"); returntheVal.ToString(); } } publicclassTester { publicstaticvoidMain() { //創建類的實例 Testert=newTester(); //調用非靜態成員 //(mustbethroughaninstance) t.Run(); } //演示調用ToString的非靜態方法 publicvoidRun() { EmployeemyEmployee=newEmployee(); CountermyCounter=newCounter(12); Console.WriteLine("Theemployee:{0},thecountervalue:{1}", myEmployee,myCounter); intmyInt=5; Console.WriteLine("Herearetwointegers:{0}and{1}",17,myInt); } } |
引用型參數和輸出型參數
與C++中相同,C#中的方法也只能有一個返回值。在C++中,我們通過將指針或索引作為參數而克服了這個限制,被調用的方法改變其中的參數,調用方法就可以得到新的值了。
向方法中傳遞一個索引作為參數時,只能嚴格地按傳遞索引或指針所能夠提供的方式訪問原來的對象。對于值類型變量而言,就不能采用這種方法了。如果要通過引用型參數傳遞值型變量,就需要在其前面加上ref關健字。如下所示:
publicvoidGetStats(refintage,refintID,refintyearsServed) |
需要注意的是,既需要在方法的定義中使用ref關健字,也需要在對方法的實際調用中使用ref關健字。
Fred.GetStats(refage,refID,refyearsServed); |
現在,我們可以在調用方法中定義age、ID和yearsServed變量,并將它們傳遞給GetStats,得到改變后的值。
C#要求明確的賦值,也就是說,在調用GetStats方法之前,必須對age、ID和yearsServed這三個局部變量進行初始化,這一工作似乎有點多余,因為我們僅僅使用它們從GetStats中得到新的變量的值。為了解決這一問題,C#提供了out關健字,表示我們可以向方法中傳遞沒有被初始化的變量,這些變量將通過引用變量的方式進行傳遞:
publicvoidGetStats(outintage,outintID,outintyearsServed) |
當然了,調用方法也必須作出相應的變化:
Fred.GetStats(outage,outID,outyearsServed); |
New的調用
在C++中,new關健字可以在堆上生成一個對象。在C#中卻不是這樣。對于引用類型變量而言,new關健字在堆上生成一個對象;對于結構等值類型變量而言,new關健字在棧中生成一個對象,并需要調用constructor。
事實上,我們可以不使用new關健字而在棧上生成一個結構類型的變量,但這時需要注意的是,New關健字能夠初始化對象。如果不使用new,則在使用前必須手工地對結構中的所有成員進行初始化,否則在編譯時會出錯。
對象的初始化
usingSystem;//有二個成員變量和一個構造器的簡單結構 publicstructPoint { publicPoint(intx,inty) { this.x=x; this.y=y; } publicintx; publicinty; } publicclassTester { publicstaticvoidMain() { Testert=newTester(); t.Run(); } publicvoidRun() { Pointp1=newPoint(5,12); SomeMethod(p1);//fine Pointp2;//不調用new而直接創建 //編譯器編譯到這里時會出錯,因為p2的成員變量沒有被初始化 //SomeMethod(p2); //手工對它們進行初始化 p2.x=1; p2.y=2; SomeMethod(p2); } //一個可以接受Point作為參數的方法 privatevoidSomeMethod(Pointp) { Console.WriteLine("Pointat{0}x{1}", p.x,p.y); } } |
屬性
大多數的C++編程人員都希望使成員變量的屬性為private,這種隱藏數據的想法促進了數據封裝概念的出現,使我們能夠在不改變用戶依賴的接口的情況下而改變類的實現。通常情況下,我們只希望客戶獲取或設置這些成員變量的值。因此,C++編程人員開發出了用來存取private成員變量的存取器。
在C#中,屬性是類的第一級成員。對于客戶而言,屬性看起來象一個成員變量。對于類的實現者而言,它看起來更象是方法。這種設計很巧妙,既可以實現數據的隱藏和封裝,又可以使客戶很方便地訪問成員變量。
我們可以在Employee類中添加一個Age屬性,使客戶可以很方便地獲取和設置員工年齡這個類的成員:
publicintAge { get { returnage; } set { age=value; } } |
關健字value可以被屬性隱性地使用。如果編寫如下的代碼:
Fred.Age=17; |
編譯器將會把值17傳遞給value。
通過只采用Get而不采用Set,我們可以為YearsServed創建一個只讀的屬性:
publicintYearsServed { get { returnyearsServed; } }Accessors的使用 privatevoidRun() { EmployeeFred=newEmployee(25,101,7); Console.WriteLine("Fred'sage:{0}", Fred.Age); Fred.Age=55; Console.WriteLine("Fred'sage:{0}", Fred.Age); Console.WriteLine("Fred'sservice:{0}", Fred.YearsServed); //Fred.YearsServed=12;//是不被允許的 } |
我們可以通過屬性獲取Fred的年齡,也可以使用這一屬性設置年齡。我們雖然可以訪問YearsServed屬性獲得它的值,但不能設置值。如果沒有注釋掉最后一行的代碼,在編譯時就會出錯。
如果以后決定從數據庫中獲取Employee的年齡,我們就只需要改變存取器的實現,而客戶不會受到任何影響。
數組
C#提供了一個數組類,它比C/C++中傳統的數組更智能化。例如,在C#中寫數組時不會超出邊界。此外,數組還有一個更智能的伙伴—ArrayList,可以動態地增長,管理對數組大小不斷變化的需求。
C#中的數組有三種形式:一維數組、多維均勻數組(象C++中傳統的數組那樣)、非均勻數組(數組的數組)。我們可以通過下面的代碼創建一維數組:
int[]myIntArray=newint[5]; |
另外,還可以以如下的方式對它進行初始化:
int[]myIntArray={2,4,6,8,10}; |
我們可以通過如下方式創建一個4×3的均勻數組:
int[,]myRectangularArray=newint[rows,columns]; |
我們可以按如下方式對該數組進行初始化:
int[,]myRectangularArray= { {0,1,2},{3,4,5},{6,7,8},{9,10,11} }; |
由于非均勻數組是數組的數組,因此,我們只能創建一維非均勻數組:
int[][]myJaggedArray=newint[4][]; |
然后再創建內部的每個數組:
myJaggedArray[0]=newint[5]; myJaggedArray[1]=newint[2]; myJaggedArray[2]=newint[3]; myJaggedArray[3]=newint[5]; |
由于數組是由繼承System.Array對象而得到的,因此,它們帶有許多包括Sort、Reverse在內的許多有用的方法。
索引器
我們可以創建象數組一樣的對象。例如,我們可以創建一個顯示一系列字符串的列表框,可以把列表框當作一個數組,使用一個索引就可以很方便地訪問列表框中的內容。
stringtheFirstString=myListBox[0]; stringtheLastString=myListBox[Length-1]; |
這是通過索引器完成的。索引器在很大程度上象一個屬性,但支持索引操作的語法。圖4顯示了一個后面跟著索引操作符的屬性,圖5顯示如何完成一個很簡單的ListBox類并對它進行索引:
界面
軟件界面是二種對象之間如何進行交互的契約。如果一個對象發布了一個界面,就等于向所有可能的客戶聲明:我支持下面的方法、屬性、事件和索引器。
C#是一種面向對象的語言,因此這些契約被封裝在一個被稱作界面的實體中,界面定義了封裝著契約的引用型類型的對象。從概念上來講,界面與抽象類非常相似,二者的區別是抽象類可以作為一系列衍生類的基礎類,界面則是與其他繼承樹結合在一起的。
IEnumerable界面
再回到上面的例子中。象在普通的數組中那樣,使用foreach-loop循環結構就能夠很好地打印ListBoxTest類中的字符串,通過在類中實現IEnumerable界面就能實現,這是由foreach-loop循環結構隱性地完成的。在任何支持枚舉和foreach-loop循環的類中都可以實現IEnumerable界面。
IEnumerable界面只有一個方法GetEnumerator,其任務是返回一個特別的IEnumerator的實現。從語法的角度來看,Enumerable類能夠提供一個IEnumerator。
Figure5ListBoxClass usingSystem; //簡化的ListBox控制 publicclassListBoxTest { //用字符串初始化該ListBox publicListBoxTest(paramsstring[]initialStrings) { //為字符串分配空間 myStrings=newString[256]; //把字符串拷貝到構造器中 foreach(stringsininitialStrings) { myStrings[myCtr++]=s; } } //在ListBox的末尾添加一個字符串 publicvoidAdd(stringtheString) { myStrings[myCtr++]=theString; } publicstringthis[intindex] { get { if(index<0||index>=myStrings.Length) { //處理有問題的索引 } returnmyStrings[index]; } set { myStrings[index]=value; } } //返回有多少個字符串 publicintGetNumEntries() { returnmyCtr; } privatestring[]myStrings; privateintmyCtr=0; } publicclassTester { staticvoidMain() { //創建一個新的列表并初始化 ListBoxTestlbt=newListBoxTest("Hello","World"); //添加一些新字符串 lbt.Add("Who"); lbt.Add("Is"); lbt.Add("John"); lbt.Add("Galt"); stringsubst="Universe"; lbt[1]=subst; //訪問所有的字符串 for(inti=0;i<lbt.GetNumEntries();i++) { Console.WriteLine("lbt[{0}]:{1}",i,lbt[i]); } } } |
Enumerator必須實現IEnumerator方法,這可以直接通過一個容器類或一個獨立的類實現,后一種方法經常被選用,因為它可以將這一任務封裝在Enumerator類中,而不會使容器類顯得很混亂。我們將在上面代碼中的ListBoxTest中添加Enumerator類,由于Enumerator類是針對我們的容器類的(因為ListBoxEnumerator必須清楚ListBoxTest的許多情況),我們將使它在ListBoxTest中成為不公開的。在本例中,ListBoxTest被定義來完成IEnumerable界面,IEnumerable界面必須返回一個Enumerator。
publicIEnumeratorGetEnumerator() { return(IEnumerator)newListBoxEnumerator(this); } |
注意,方法將當前的ListBoxTest對象(this)傳遞給Enumerator,這將使Enumerator枚舉這一指定的ListBoxTest對象中的元素。
實現這一類的Enumerator在這里被實現為ListBoxEnumerator,它在ListBoxTest中被定義成一個私有類,這一工作是相當簡單的。
被枚舉的ListBoxTest作為一個參數被傳遞給constructor,ListBoxTest被賦給變量myLBT,構造器還會將成員變量index設置為-1,表明對象的枚舉還沒有開始。
publicListBoxEnumerator(ListBoxTesttheLB) { myLBT=theLB; index=-1; } |
MoveNext方法對index進行加1的操作,然后確保沒有超過枚舉的對象的邊界。如果超過邊界了,就會返回false值,否則返回true值。
publicboolMoveNext() { index++; if(index>=myLBT.myStrings.Length) returnfalse; else returntrue; } |
Reset的作用僅僅是將index的值設置為-1。
Current返回最近添加的字符串,這是一個任意的設定,在其他類中,Current可以有設計人員確定的意義。無論是如何設計的,每個進行枚舉的方法必須能夠返回當前的成員。
publicobjectCurrent { get { return(myLBT[index]); } } |
對foreach循環結構的調用能夠獲取枚舉的方法,并用它處理數組中的每個成員。由于foreach循環結構將顯示每一個字符串,而無論我們是否添加了一個有意義的值,我們將myStrings的初始化改為8個條目,以保證顯示的易于處理。
myStrings=newString[8]; |
使用基本類庫
為了更好地理解C#與C++的區別和解決問題方式的變化,我們先來看一個比較簡單的例子。我們將創建一個讀取文本文件的類,并在屏幕上顯示其內容。我將把它做成多線程程序,以便在從磁盤上讀取數據時還可以做其他的工作。
在C++中,我們可能會創建一個讀文件的線程和另一個做其他工作的線程,這二個線程將各自獨立地運行,但可能會需要對它們進行同步。在C#中,我們也可以完成同樣的工作,由于.NET框架提供了功能強大的異步I/O機制,在編寫線程時,我們會節省不少的時間。
異步I/O支持是內置在CLR中的,而且幾乎與使用正常的I/O流類一樣簡單。在程序的開始,我們首先通知編譯器,我們將在程序中使用許多名字空間中的對象:
usingSystem; usingSystem.IO; usingSystem.Text; |
在程序中包含System,并不會自動地包含其所有的子名字空間,必須使用using關健字明確地包含每個子名字空間。我們在例子中會用到I/O流類,因此需要包含System.IO名字空間,我們還需要System.Text名字空間支持字節流的ASCII編碼。
由于.NET架構為完成了大部分的工作,編寫這一程序所需的步驟相當簡單。我們將用到Stream類的BeginRead方法,它提供異步I/O功能,將數據讀入到一個緩沖區中,當緩沖區可以處理時調用相應的處理程序。
我們需要使用一個字節數組作為緩沖區和回叫方法的代理,并將這二者定義為驅動程序類的private成員變量。
publicclassAsynchIOTester { privateStreaminputStream; privatebyte[]buffer; privateAsyncCallbackmyCallBack; |
inputStream是一個Stream類型的變量,我們將對它調用BeginRead方法。代理與成員函數的指針非常相似。代理是C#的第一類元素。
當緩沖區被磁盤上的文件填滿時,.NET將調用被代理的方法對數據進行處理。在等待讀取數據期間,我們可以讓計算機完成其他的工作。(在本例中是將1個整型變量由1增加到50000,但在實際的應用程序中,我們可以讓計算機與用戶進行交互或作其他有意義的工作。)
本例中的代理被定義為AsyncCallback類型的過程,這是Stream的BeginRead方法所需要的。System空間中AsyncCallback類型代理的定義如下所示:
publicdelegatevoidAsyncCallback(IAsyncResultar); |
這一代理可以是與任何返回void類型值、將IAsyncResult界面作為參數的方法相關聯的。在該方法被調用時,CLR可以在運行時傳遞IAsyncResult界面對象作為參數。我們需要如下所示的形式定義該方法:
voidOnCompletedRead(IAsyncResultasyncResult) |
然后在構造器中與代理連接起來:
AsynchIOTester() { ??? myCallBack=newAsyncCallback(this.OnCompletedRead); } |
上面的代碼將代理的實例賦給成員變量myCallback。下面是全部程序的詳細工作原理。在Main函數中,創建了一個類的實例,并讓它開始運行:
publicstaticvoidMain() { AsynchIOTestertheApp=newAsynchIOTester(); theApp.Run(); } |
new關健字能夠啟動構造器。在構造器中我們打開一個文件,并得到一個Stream對象。然后在緩沖中分配空間并與回調機制聯結起來。
AsynchIOTester() { inputStream=File.OpenRead(@"C:\MSDN\fromCppToCS.txt"); buffer=newbyte[BUFFER_SIZE]; myCallBack=newAsyncCallback(this.OnCompletedRead); } |
在Run方法中,我們調用了BeginRead,它將以異步的方式讀取文件。
inputStream.BeginRead( buffer,//存放結果 0,//偏移量 buffer.Length,//緩沖區中有多少字節 myCallBack,//回調代理 null);//本地對象 |
這時,我們可以完成其他的工作。
for(longi=0;i<50000;i++) { if(i%1000==0) { Console.WriteLine("i:{0}",i); } } |
文件讀取操作結束后,CLR將調用回調方法。
voidOnCompletedRead(IAsyncResultasyncResult) { |
在OnCompletedRead中要做的第一件事就是通過調用Stream對象的EndRead方法找出讀取了多少字節:
intbytesRead=inputStream.EndRead(asyncResult); |
對EndRead的調用將返回讀取的字節數。如果返回的數字比0大,則將緩沖區轉換為一個字符串,然后將它寫到控制臺上,然后再次調用BeginRead,開始另一次異步讀的過程。
if(bytesRead>0) { Strings=Encoding.ASCII.GetString(buffer,0,bytesRead); Console.WriteLine(s); inputStream.BeginRead(buffer,0,buffer.Length, myCallBack,null); } |
現在,在讀取文件的過程中就可以作別的工作了(在本例中是從1數到50000),但我們可以在每次緩沖區滿了時對讀取的數據進行處理(在本例中是向控制臺輸出緩沖區中的數據)。有興趣的讀者可以點擊此處下載完整的源代碼。
異步I/O的管理完全是由CLR提供的,這樣,在網絡上讀取文件時,會更好些。
在網絡上讀取文件
在C++中,在網絡上讀取文件需要有相當的編程技巧,.NET對此提供了廣泛的支持。事實上,在網絡上讀取文件僅僅是基礎類庫中Stream類的另一種應用。
首先,為了對TCP/IP端口(在本例中是65000)進行監聽,我們需要創建一個TCPListener類的實例。
TCPListenertcpListener=newTCPListener(65000); |
一旦創建后,就讓它開始進行監聽。
tcpListener.Start(); |
現在就要等待客戶連接的要求了。
SocketsocketForClient=tcpListener.Accept(); |
TCPListener對象的Accept方法返回一個Socket對象,Accept是一個同步的方法,除非接收到一個連接請求它才會返回。如果連接成功,就可以開始向客戶發送文件了。
if(socketForClient.Connected) { ??? |
接下來,我們需要創建一個NetworkStream類,將報路傳遞給constructor:
NetworkStreamnetworkStream=newNetworkStream(socketForClient); |
然后創建一個StreamWriter對象,只是這次不是在文件上而是在剛才創建的NetworkStream類上創建該對象:
System.IO.StreamWriterstreamWriter= newSystem.IO.StreamWriter(networkStream); |
當向該流寫內容時,流就通過網絡被傳輸給客戶端。
客戶端的創建
客戶端軟件就是一個TCPClient類的具體例子,TCPClient類代表連向主機的一個TCP/IP連接。
TCPClientsocketForServer; socketForServer=newTCPClient("localHost",65000); |
有了TCPClient對象后,我們就可以創建NetworkStream對象了,然后在其上創建StreamReader類:
NetworkStreamnetworkStream=socketForServer.GetStream(); System.IO.StreamReaderstreamReader= newSystem.IO.StreamReader(networkStream); |
現在,只要其中有數據就讀取該流,并將結果輸出到控制臺上。
do { outputString=streamReader.ReadLine(); if(outputString!=null) { Console.WriteLine(outputString); } } while(outputString!=null); |
為了對這一段代碼進行測試,可以創建如下一個測試用的文件:
Thisislineone Thisislinetwo Thisislinethree Thisislinefour |
這是來自服務器的輸出:
Output(Server) Clientconnected SendingThisislineone SendingThisislinetwo SendingThisislinethree SendingThisislinefour Disconnectingfromclient... Exiting... |
下面是來自客戶端的輸出:
Thisislineone Thisislinetwo Thisislinethree Thisislinefour |
屬性和元數據
C#和C++之間一個顯著的區別是它提供了對元數據的支持:有關類、對象、方法等其他實體的數據。屬性可以分為二類:一類以CLR的一部分的形式出現,另一種是我們自己創建的屬性,CLR屬性用來支持串行化、排列和COM協同性等。一些屬性是針對一個組合體的,有些屬性則是針對類或界面,它們也被稱作是屬性目標。
將屬性放在屬性目標前的方括號內,屬性就可以作用于它們的屬性目標。
[assembly:AssemblyDelaySign(false)] [assembly:AssemblyKeyFile(".\\keyFile.snk")] |
或用逗號將各個屬性分開:
[assembly:AssemblyDelaySign(false), assembly:AssemblyKeyFile(".\\keyFile.snk")] |
自定義的屬性
我們可以任意創建自定義屬性,并在認為合適的時候使用它們。假設我們需要跟蹤bug的修復情況,就需要建立一個包含bug的數據庫,但需要將bug報告與專門的修正情況綁定在一塊兒,則可能在代碼中添加如下所示的注釋:
//Bug323fixedbyJesseLiberty1/1/2005. |
這樣,在源代碼中就可以一目了然地了解bug的修正情況,但如果如果把相關的資料保存在數據庫中可能會更好,這樣就更方便我們的查詢工作了。如果所有的bug報告都使用相同的語法那就更好了,但這時我們就需要一個定制的屬性了。我們可能使用下面的內容代替代碼中的注釋:
[BugFix(323,"JesseLiberty","1/1/2005")Comment="Offbyoneerror"] |
與C#中的其他元素一樣,屬性也是類。定制化的屬性類需要繼承System.Attribute:
publicclassBugFixAttribute:System.Attribute
我們需要讓編譯器知道這個屬性可以跟什么類型的元素,我們可以通過如下的方式來指定該類型的元素:
[AttributeUsage(AttributeTargets.ClassMembers,AllowMultiple=true)] |
AttributeUsage是一個作用于屬性的屬性━━元屬性,它提供的是元數據的元數據,也即有關元數據的數據。在這種情況下,我們需要傳遞二個參數,第一個是目標(在本例中是類成員。),第二個是表示一個給定的元素是否可以接受多于一個屬性的標記。AllowMultiple的值被設置為true,意味著類成員可以有多于一個BugFixAttribute屬性。如果要聯合二個屬性目標,可以使用OR操作符連接它們。
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Interface,AllowMultiple=true)] |
上面的代碼將使一個屬性隸屬于一個類或一個界面。
新的自定義屬性被命名為BugFixAttribute。命名的規則是在屬性名之后添加Attribute。在將屬性指派給一個元素后,編譯器允許我們使用精簡的屬性名調用這一屬性。因此,下面的代碼是合法的:
[BugFix(123,"JesseLiberty","01/01/05",Comment="Offbyone")] |
編譯器將首先查找名字為BugFix的屬性,如果沒有發現,則查找BugFixAttribute。
每個屬性必須至少有一個構造器。屬性可以接受二種類型的參數:環境參數和命名參數。在前面的例子中,bugID、編程人員的名字和日期是環境參數,注釋是命名參數。環境參數被傳遞到構造器中的,而且必須按在構造器中定義的順序傳遞。
publicBugFixAttribute(intbugID,stringprogrammer,stringdate) { this.bugID=bugID; this.programmer=programmer; this.date=date; } Namedparametersareimplementedasproperties. |
屬性的使用
為了對屬性進行測試,我們創建一個名字為MyMath的簡單類,并給它添加二個函數,然后給它指定bugfix屬性。
[BugFixAttribute(121,"JesseLiberty","01/03/05")] [BugFixAttribute(107,"JesseLiberty","01/04/05", Comment="Fixedoffbyoneerrors")] publicclassMyMath |
這些數據將與元數據存儲在一起。下面是完整的源代碼及其輸出:
自定義屬性
usingSystem; //創建被指派給類成員的自定義屬性 [AttributeUsage(AttributeTargets.Class, AllowMultiple=true)] publicclassBugFixAttribute:System.Attribute { //位置參數的自定義屬性構造器 publicBugFixAttribute (intbugID, stringprogrammer, stringdate) { this.bugID=bugID; this.programmer=programmer; this.date=date; } publicintBugID { get { returnbugID; } } //命名參數的屬性 publicstringComment { get { returncomment; } set { comment=value; } } publicstringDate { get { returndate; } } publicstringProgrammer { get { returnprogrammer; } } //專有成員數據 privateintbugID; privatestringcomment; privatestringdate; privatestringprogrammer; } //把屬性指派給類 [BugFixAttribute(121,"JesseLiberty","01/03/05")] [BugFixAttribute(107,"JesseLiberty","01/04/05", Comment="Fixedoffbyoneerrors")] publicclassMyMath { publicdoubleDoFunc1(doubleparam1) { returnparam1+DoFunc2(param1); } publicdoubleDoFunc2(doubleparam1) { returnparam1/3; } } publicclassTester { publicstaticvoidMain() { MyMathmm=newMyMath(); Console.WriteLine("CallingDoFunc(7).Result:{0}", mm.DoFunc1(7)); } } |
輸出:
CallingDoFunc(7).Result:9.3333333333333339 |
象我們看到的那樣,屬性對輸出絕對沒有影響,創建屬性也不會影響代碼的性能。到目前為止,讀者也只是在聽我論述有關屬性的問題,使用ILDASM瀏覽元數據,就會發現屬性確實是存在的。
映射
在許多情況下,我們需要一種方法,能夠從元數據中訪問屬性,C#提供了對映射的支持以訪問元數據。通過初始化MemberInfo類型對象,System.Reflection名字空間中的這個對象可以用來發現成員的屬性,對元數據進行訪問。
System.Reflection.MemberInfoinf=typeof(MyMath); |
對MyMath類型調用typeof操作符,它返回一個由繼承MemberInfo而生成的Type類型的變量。
下一步是對MemberInfo對象調用GetCustomAttributes,并將希望得到的屬性的類型作為一個參數傳遞給GetCustomAttributes。我們將得到一個對象數組,數組的每個成員的類型都是BugFixAttribute。
object[]attributes; attributes=Attribute.GetCustomAttributes(inf,typeof(BugFixAttribute)); |
我們就可以遍歷這個數組了,打印BugFixAttribute對象的數組,代碼下所示:
屬性的打印
publicstaticvoidMain() { MyMathmm=newMyMath(); Console.WriteLine("CallingDoFunc(7).Result:{0}", mm.DoFunc1(7)); //獲取成員信息并使用它訪問自定義的屬性 System.Reflection.MemberInfoinf=typeof(MyMath); object[]attributes; attributes= Attribute.GetCustomAttributes(inf,typeof(BugFixAttribute)); //遍歷所有的屬性 foreach(Objectattributeinattributes) { BugFixAttributebfa=(BugFixAttribute)attribute; Console.WriteLine("\nBugID:{0}",bfa.BugID); Console.WriteLine("Programmer:{0}",bfa.Programmer); Console.WriteLine("Date:{0}",bfa.Date); Console.WriteLine("Comment:{0}",bfa.Comment); } } |
類型發現
我們可以通過映象的方法來研究一個組合實體的內容,如果要建立需要顯示組合體內部信息的工具或動態地調用組合體中的途徑,這一方法是非常有用的。
通過映象的方法,我們可以知道一個模塊、方法、域、屬性的類型,以及該類型的每個方法的信號、該類支持的界面和該類的超級類。我們可以通過如下的形式,用Assembly.Load靜態方法動態地加載一個組合體:
publicstaticAssembly.Load(AssemblyName) |
然后,可以將它傳遞到核心庫中。
Assemblya=Assembly.Load("Mscorlib.dll"); |
一旦加載了組合體,我們可以通過調用GetTypes返回一個Type對象數組。Type對象是映射的核心,它表示類、界面、數組、值和枚舉等的類型定義。
Type[]types=a.GetTypes(); |
組合休會返回一個類型的數組,我們可以使用foreach-loop結構顯示該數組,其輸出將有好幾頁文檔之多,下面我們從中找一小段:
TypeisSystem.TypeCode TypeisSystem.Security.Util.StringExpressionSet TypeisSystem.Text.UTF7Encoding$Encoder TypeisSystem.ArgIterator TypeisSystem.Runtime.Remoting.JITLookupTable 1205typesfound |
我們得到了一個內容為核心庫中類型的數組,可以將它們都打印出來,該數組將有1205個項。
對一種類型映射我們也可以對組合體中一種類型進行映射。為此,我們可以使用GetType方法從組合體中解析出一個類型:
publicclassTester { publicstaticvoidMain() { //檢查一個對象 TypetheType=Type.GetType("System.Reflection.Assembly"); Console.WriteLine("\nSingleTypeis{0}\n",theType); } } |
輸出如下所示:
SingleTypeisSystem.Reflection.Assembly |
發現成員
我們還可以得到所有成員的類型,顯示所有的方法、屬性、域,下面的代碼演示了實現上述目標的代碼。
Figure9GettingAllMembers publicclassTester { publicstaticvoidMain() { //檢查一個單一的對象 TypetheType=Type.GetType("System.Reflection.Assembly"); Console.WriteLine("\nSingleTypeis{0}\n",theType); //獲取所有的成員 MemberInfo[]mbrInfoArray= theType.GetMembers(BindingFlags.LookupAll); foreach(MemberInfombrInfoinmbrInfoArray) { Console.WriteLine("{0}isa{1}", mbrInfo,mbrInfo.MemberType.Format()); } } } |
盡管得到的輸出還非常長,但在輸出中我們可以得到如下面的不甘落后民示的域、方法、構造器和屬性:
System.Strings_localFilePrefixisaField BooleanIsDefined(System.Type)isaMethod Void.ctor()isaConstructor System.StringCodeBaseisaProperty System.StringCopiedCodeBaseisaProperty |
只發現方法
我們可能會只關心方法,而不關心域、屬性等,為此,我們需要刪除如下的對GetMembers的調用:
MemberInfo[]mbrInfoArray= theType.GetMembers(BindingFlags.LookupAll); |
然后添加調用GetMethods的語句:
mbrInfoArray=theType.GetMethods(); |
現在,輸出中就只剩下方法了。
Output(excerpt) BooleanEquals(System.Object)isaMethod System.StringToString()isaMethod System.StringCreateQualifiedName(System.String,System.String) isaMethod System.Reflection.MethodInfoget_EntryPoint()isaMethod |
發現特定的成員
最后,為了進一步地縮小范圍,我們可以使用FindMembers方法來發現某一類型的特定的方法。例如,在下面的代碼中,我們可以只搜索以"Get"開頭的方法。
publicclassTester { publicstaticvoidMain() { //檢查一個單一的對象 TypetheType=Type.GetType("System.Reflection.Assembly"); //只獲取以Get開頭的成員 MemberInfo[]mbrInfoArray theType.FindMembers(MemberTypes.Method, BindingFlags.Default, Type.FilterName,"Get*"); foreach(MemberInfombrInfoinmbrInfoArray) { Console.WriteLine("{0}isa{1}", mbrInfo,mbrInfo.MemberType.Format()); } } } |
其輸出的一部分如下所示:
System.Type[]GetTypes()isaMethod System.Type[]GetExportedTypes()isaMethod System.TypeGetType(System.String,Boolean)isaMethod System.TypeGetType(System.String)isaMethod System.Reflection.AssemblyNameGetName(Boolean)isaMethod System.Reflection.AssemblyNameGetName()isaMethod Int32GetHashCode()isaMethod System.Reflection.AssemblyGetAssembly(System.Type)isaMethod System.TypeGetType(System.String,Boolean,Boolean)isaMethod |
動態調用
一旦發現一個方法,可以使用映射的方法調用它。例如,我們可能需要調用System.Math中的Cos方法(返回一個角的余弦值)。為此,我們需要獲得System.Math類的類型信息,如下所示:
TypetheMathType=Type.GetType("System.Math"); |
有了類型信息,我們就可以動態地加載一個類的實例:
ObjecttheObj=Activator.CreateInstance(theMathType); |
CreateInstance是Activator類的一個靜態方法,可以用來對對象進行初始化。
有了System.Math類的實例后,我們就可以調用Cos方法了。我們還需要準備好一個定義參數類型的數組,因為Cos只需要一個參數(需要求余弦值的角度),因此數組中只需要有一個成員。我們將在數組中賦予一個System.Double類型的Type對象,也就是Cos方法需要的參數的類型:
Type[]paramTypes=newType[1]; paramTypes[0]=Type.GetType("System.Double"); |
現在我們就可以傳遞方法的名字了,這個數組定義了Type對象中GetMethod方法的參數的類型:
MethodInfoCosineInfo= theMathType.GetMethod("Cos",paramTypes); |
我們現在得到了MethodInfo類型的對象,我們可以在其上調用相應的方法。為此,我們需要再次在數組中傳入參數的實際值:
Object[]parameters=newObject[1]; parameters[0]=45; ObjectreturnVal=CosineInfo.Invoke(theObj,parameters); |
需要注意的是,我創建了二個數組,第一個名字為paramTypes的數組存儲著參數的類型,第二個名字為parameters的數組保存實際的參數值。如果方法需要二個參數,我們就需要使這二個數組每個保持二個參數。如果方法不需要參數,我們仍然需要創建這二個數組,只是無需在里面存儲數據即可。
Type[]paramTypes=newType[0]; |
盡管看起來有點奇怪,但它是正確的。下面是完整的代碼:
映射方法的使用
usingSystem; usingSystem.Reflection;publicclassTester { publicstaticvoidMain() { TypetheMathType=Type.GetType("System.Math"); ObjecttheObj=Activator.CreateInstance(theMathType); //只有一個成員的數組 Type[]paramTypes=newType[1]; paramTypes[0]=Type.GetType("System.Double"); //獲得Cos()方法的信息 MethodInfoCosineInfo= theMathType.GetMethod("Cos",paramTypes); //將實際的參數填寫在一個數組中 Object[]parameters=newObject[1]; parameters[0]=45; ObjectreturnVal=CosineInfo.Invoke(theObj,parameters); Console.WriteLine( "Thecosineofa45degreeangle{0}",returnVal); } } |
結論
盡管有許多小錯誤等著C++編程人員去犯,但C#的語法與C++并沒有太大的不同,向新語言的轉換是相當容易的。使用C#的有趣的部分是使用通用語言運行庫,這篇文章只能涉及幾個重點問題。CLR和.NETFramework提供了對線程、集合、互聯網應用開發、基于Windows的應用開發等方面提供了更多的支持。語言功能和CLR功能之間的區分是非常模糊的,但組合在一起就是一種功能非常強大的開發工具了。