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