|
第八章 用C#寫(xiě)組件
這一章關(guān)于用C#寫(xiě)組件。你學(xué)到如何寫(xiě)一個(gè)組件,如何編譯它,且如何在一個(gè)客戶程序中使用它。更深入一步是運(yùn)用 名字空間來(lái)組織你的應(yīng)用程序。 這章由兩個(gè)主要大節(jié)構(gòu)成: 。你的第一個(gè)組件 。使用名字空間工作
8.1 你的第一個(gè)組件 到目前為止,在本書(shū)中提到的例子都是在同一個(gè)應(yīng)用程序中直接使用一個(gè)類(lèi)。類(lèi)和它的使用者被包含在同一個(gè)執(zhí)行文 件中,F(xiàn)在我們將把類(lèi)和使用者分離到組件和客戶,它們分別位于不同的二進(jìn)制文件中(可執(zhí)行文件)。 盡管你仍然為組件創(chuàng)建一個(gè) DLL,但其步驟與用C++寫(xiě)一個(gè)COM組件差別很大。你很少涉及到底層結(jié)構(gòu)。以下小節(jié)說(shuō)明 了如何構(gòu)建一個(gè)組件以及使用到它的客戶:
。構(gòu)建組件 。編譯組件 。創(chuàng)建一個(gè)簡(jiǎn)單的客戶應(yīng)用程序
8.1.1 構(gòu)建組件 因?yàn)槲沂且粋(gè)使用范例迷,我決定創(chuàng)建一個(gè)相關(guān)Web的類(lèi),以方便你們使用。它返回一個(gè)Web網(wǎng)頁(yè)并儲(chǔ)存在一個(gè)字符串 變量中,以供后來(lái)重用。所有這些編寫(xiě)都參考了.NET框架的幫助文檔。 類(lèi)名為RequestWebPage;它有兩個(gè)構(gòu)造函數(shù)—— 一個(gè)屬性和一個(gè)方法。屬性被命名為URL,且它儲(chǔ)存了網(wǎng)頁(yè)的Web地 址,由方法GetContent返回。這個(gè)方法為你做了所有的工作(見(jiàn)清單8.1)。
清單 8.1 用于從Web服務(wù)器返回HTML網(wǎng)頁(yè)的RequestWebPage 類(lèi)
1: using System; 2: using System.Net; 3: using System.IO; 4: using System.Text; 5: 6: public class RequestWebPage 7: { 8: private const int BUFFER_SIZE = 128; 9: private string m_strURL; 10: 11: public RequestWebPage() 12: { 13: } 14: 15: public RequestWebPage(string strURL) 16: { 17: m_strURL = strURL; 18: } 19: 20: public string URL 21: { 22: get { return m_strURL; } 23: set { m_strURL = value; } 24: } 25: public void GetContent(out string strContent) 26: { 27: // 檢查 URL 28: if (m_strURL == "") 29: throw new ArgumentException("URL must be provided."); 30: 31: WebRequest theRequest = (WebRequest) WebRequestFactory.Create(m_strURL); 32: WebResponse theResponse = theRequest.GetResponse(); 33: 34: // 給回應(yīng)設(shè)置字節(jié)緩沖區(qū) 35: int BytesRead = 0; 36: Byte[] Buffer = new Byte[BUFFER_SIZE]; 37: 38: Stream ResponseStream = theResponse.GetResponseStream(); 39: BytesRead = ResponseStream.Read(Buffer, 0, BUFFER_SIZE); 40: 41: //使用 StringBuilder 以加速分配過(guò)程 42: StringBuilder strResponse = new StringBuilder(""); 43: while (BytesRead != 0 ) 44: { 45: strResponse.Append(Encoding.ASCII.GetString(Buffer,0,BytesRead)); 46: BytesRead = ResponseStream.Read(Buffer, 0, BUFFER_SIZE); 47: } 48: 49: // 賦給輸出參數(shù) 50: strContent = strResponse.ToString(); 51: } 52: }
本應(yīng)該利用無(wú)參數(shù)構(gòu)造函數(shù)完成工作,但我決定在構(gòu)造函數(shù)中初始化URL,這可能會(huì)很有用。當(dāng)后來(lái)決定要改變URL 時(shí)——為了返回第二個(gè)網(wǎng)頁(yè),例如,通過(guò)URL屬性的get和set訪問(wèn)標(biāo)志使它被公開(kāi)了。 有趣的事始于GetContent方法。首先,代碼對(duì)URL實(shí)行十分簡(jiǎn)單的檢查,如果它不適合,就會(huì)引發(fā)一個(gè) ArgumentException 異常。之后,我請(qǐng)求WebRequestFactory ,以創(chuàng)建一個(gè)基于傳遞給它的URL的WebRequest對(duì)象。 因?yàn)槲也幌氚l(fā)送cookies、附加頭和詢(xún)問(wèn)串等,所以立即訪問(wèn)WebResponse(第32行)。如果你需要請(qǐng)求上述任何的功 能,必須在這一行之前實(shí)現(xiàn)它們。 第35和36行初始化一個(gè)字節(jié)緩沖區(qū),它用于從返回流中讀數(shù)據(jù)。暫時(shí)忽略StringBuilder 類(lèi),只要返回流中仍然有要 讀的數(shù)據(jù),while循環(huán)就會(huì)簡(jiǎn)單地重復(fù)。最后的讀操作將返回零,因此結(jié)束了該循環(huán)。 現(xiàn)在我想回到StringBuilder類(lèi)。為什么用這個(gè)類(lèi)的實(shí)例而不是簡(jiǎn)單地把字節(jié)緩沖區(qū)合并到一個(gè)字符串變量?看下面這 個(gè)例子: strMyString = strMyString + "some more text"; 這里很清楚,你正在拷貝值。常量 "some more text" 以一個(gè)字符串變量類(lèi)型被加框,且根據(jù)加法操作創(chuàng)建了一個(gè)新 的字符串變量。接著被賦給了 strMyString。有很多次拷貝,是嗎? 但你可能引起爭(zhēng)論 strMyString += "some more text"; 不要炫耀這種行為。對(duì)不起,對(duì)于C#這是一個(gè)錯(cuò)誤的答案。其操作完全與所描述的賦值操作相同。 不涉及該問(wèn)題的另外的途徑是使用StringBuilder類(lèi)。它利用一個(gè)緩沖區(qū)進(jìn)行工作,接著,在沒(méi)有發(fā)生我所描述的拷貝 行為的情況下,你進(jìn)行追加、插入、刪除和替換操作。這就是為什么我在類(lèi)中使用它來(lái)合并那些讀自緩沖區(qū)中的內(nèi)容。 該緩沖區(qū)把我?guī)нM(jìn)了這個(gè)類(lèi)中最后重要的代碼片段——第45行的編碼轉(zhuǎn)換。它只不過(guò)涉及到我獲得請(qǐng)求的字符集。 最后,當(dāng)所有的內(nèi)容被讀入且被轉(zhuǎn)換時(shí),我顯式地從 StringBuilder請(qǐng)求一個(gè)字符串對(duì)象并把它賦給了輸出變量。一 個(gè)返回值仍然會(huì)導(dǎo)致另外的拷貝操作。
8.1.2 編譯組件 到目前為止,你所做的工作與在正常應(yīng)用程序的內(nèi)部編寫(xiě)一個(gè)類(lèi)沒(méi)有什么區(qū)別。所不同的是編譯過(guò)程。你必須創(chuàng)建一 個(gè)庫(kù)而不是一個(gè)應(yīng)用程序: csc /r:System.Net.dll /t:library /out:wrq.dll webrequest.cs 編譯開(kāi)關(guān)/t:library 告訴C#編譯,要?jiǎng)?chuàng)建一個(gè)庫(kù)而不是搜尋一個(gè)靜態(tài) Main方法。同樣,因?yàn)槲艺谑褂? System.Net名字空間,所以必須引用 (/r:)它的庫(kù),這個(gè)庫(kù)就是System.Net.dll。 你的庫(kù)命名為 wrq.dll,現(xiàn)在它準(zhǔn)備用于一個(gè)客戶應(yīng)用程序。因?yàn)樵谶@章中我僅使用私有組件工作,所以你不必把庫(kù) 拷貝到一個(gè)特殊的位置,而是拷貝到客戶應(yīng)用程序目錄。
8.1.3 創(chuàng)建一個(gè)簡(jiǎn)單的客戶應(yīng)用程序 當(dāng)一個(gè)組件被寫(xiě)成且被成功地編譯時(shí),你所要做的就是在客戶應(yīng)用程序中使用它。我再次創(chuàng)建了一個(gè)簡(jiǎn)單的命令行應(yīng) 用程序,它返回了我維護(hù)的一個(gè)開(kāi)發(fā)站點(diǎn)的首頁(yè)(見(jiàn)清單8.2)。
清單 8.2 用 RequestWebPage 類(lèi)返回一個(gè)簡(jiǎn)單的網(wǎng)頁(yè)
1: using System; 2: 3: class TestWebReq 4: { 5: public static void Main() 6: { 7: RequestWebPage wrq = new RequestWebPage(); 8: wrq.URL = "http://www.alphasierrapapa.com/iisdev/"; 9: 10: string strResult; 11: try 12: { 13: wrq.GetContent(out strResult); 14: } 15: catch (Exception e) 16: { 17: Console.WriteLine(e); 18: return; 19: } 20: 21: Console.WriteLine(strResult); 22: } 成員
注意,我已經(jīng)在一個(gè)try catch語(yǔ)句中包含了對(duì) GetContent的調(diào)用。其中的一個(gè)原因是GetContent可能引發(fā)一個(gè) ArgumentException異常。此外,我在組件內(nèi)部調(diào)用的.NET框架類(lèi)也可以引發(fā)異常。因?yàn)槲也荒茉陬?lèi)的內(nèi)部處理這些異常, 所以我必須在這里處理它們。 其余的代碼只不過(guò)是簡(jiǎn)單的組件使用——調(diào)用標(biāo)準(zhǔn)的構(gòu)造函數(shù),存取一個(gè)屬性,并執(zhí)行一個(gè)方法。但等一下:你需要 注意何時(shí)編譯應(yīng)用程序。一定要告訴編譯器,讓它引用你的新組件庫(kù)DLL: csc /r:wrq.dll wrclient.cs 現(xiàn)在萬(wàn)事俱備,你可以測(cè)試程序了。輸出結(jié)果會(huì)滾屏,但你可以看到應(yīng)用程序工作。使用了常規(guī)的表達(dá)式,你也可以 增加代碼,以解析返回的HTML,并依據(jù)你個(gè)人的喜好,提取信息。我預(yù)想會(huì)使用到這個(gè)類(lèi)新版本的SSL(安全套接字層), 用于ASP+網(wǎng)頁(yè)中的在線信用卡驗(yàn)證。 你可能會(huì)注意到,沒(méi)有特殊的using 語(yǔ)句用于你所創(chuàng)建的庫(kù)。原因是你在組件的源文件中沒(méi)有定義名字空間。
8.2 使用名字空間工作 你經(jīng)常使用到名字空間,例如System 和System.Net。C#利用名字空間來(lái)組織程序,而且分層的組織使一個(gè)程序的成員 傳到另一個(gè)程序變得更容易。 盡管不強(qiáng)制,但你總要?jiǎng)?chuàng)建名字空間,以清楚地識(shí)別應(yīng)用程序的層次。.NET框架會(huì)給出構(gòu)建這種分層的良好思想。 以下的代碼片段顯示了在C#原文件中簡(jiǎn)單的名字空間 My.Test(點(diǎn)號(hào)表示一個(gè)分層等級(jí))的聲明:
namespace My.Test { //這里的任何東西屬于名字空間 }
當(dāng)你訪問(wèn)名字空間中的一個(gè)成員時(shí),也有必要使用名字空間標(biāo)識(shí)符完全地驗(yàn)證它,或者利用using標(biāo)志把所有的成員引 入到你當(dāng)前的名字空間。本書(shū)前面的例子演示了如何應(yīng)用這些技術(shù)。 在開(kāi)始使用名字空間之前,只有少數(shù)有關(guān)存取安全的詞。如果你不增加一個(gè)特定的存取修飾符,所有的類(lèi)型將被默認(rèn) 為internal 。當(dāng)你想從外部訪問(wèn)該類(lèi)型時(shí),使用 public 。不允許其它的修飾符。 這是關(guān)于名字空間充分的理論。讓我們繼續(xù)實(shí)現(xiàn)該理論——以下小節(jié)說(shuō)明了當(dāng)構(gòu)建組件應(yīng)用程序時(shí),如何使用名字空 間 。在名字空間中包裝類(lèi) 。在客戶應(yīng)用程序中使用名字空間 。為名字空間增加多個(gè)類(lèi)
8.2.1 在名字空間中包裝類(lèi) 既然你知道了名字空間的理論含義,那么讓我們?cè)诂F(xiàn)實(shí)生活中實(shí)現(xiàn)它吧。在這個(gè)和即將討論到的例子中,自然選擇到 的名字空間是Presenting.CSharp。為了不使你厭煩,僅僅是把RequestWebPage包裝到Presenting.CSharp中,我決定寫(xiě)一 個(gè)類(lèi),用于 Whois查找(見(jiàn)清單8.3)。
清單 8.3 在名字空間中實(shí)現(xiàn) WhoisLookup類(lèi)
1: using System; 2: using System.Net.Sockets; 3: using System.IO; 4: using System.Text; 5: 6: namespace Presenting.CSharp 7: { 8: public class WhoisLookup 9: { 10: public static bool Query(string strDomain, out string strWhoisInfo) 11: { 12: const int BUFFER_SIZE = 128; 13: 14: if ("" == strDomain) 15: throw new ArgumentException("You must specify a domain name."); 16: 17: TCPClient tcpc = new TCPClient(); 18: strWhoisInfo = "N/A"; 19: 20: // 企圖連接 whois 服務(wù)器 21: if (tcpc.Connect("whois.networksolutions.com", 43) != 0) 22: return false; 23: 24: // 獲取流 25: Stream s = tcpc.GetStream(); 26: 27: // 發(fā)送請(qǐng)求 28: strDomain += "\r\n"; 29: Byte[] bDomArr = Encoding.ASCII.GetBytes(strDomain.ToCharArray()); 30: s.Write(bDomArr, 0, strDomain.Length); 31: 32: Byte[] Buffer = new Byte[BUFFER_SIZE]; 33: StringBuilder strWhoisResponse = new StringBuilder(""); 34: 35: int BytesRead = s.Read(Buffer, 0, BUFFER_SIZE); 36: while (BytesRead != 0 ) 37: { 38: strWhoisResponse.Append(Encoding.ASCII.GetString(Buffer,0,BytesRead)); 39: BytesRead = s.Read(Buffer, 0, BUFFER_SIZE); 40: } 41: 42: tcpc.Close(); 43: strWhoisInfo = strWhoisResponse.ToString(); 44: return true; 45: } 46: } 47: }
名字空間在第6行被聲明,而且它用第7行和第47行的大括弧括住了WhoisLookup類(lèi)。要聲明自己新的名字空間,實(shí)際要 做的就是這些。 在WhoisLookup類(lèi)中當(dāng)然具有一些有趣代碼,特別是由于它說(shuō)明了使用C#進(jìn)行socket編程是多么的容易。在static Query method中經(jīng)過(guò) not-so-stellar域名檢查之后,我實(shí)例化了TCPClient類(lèi)型的一個(gè)對(duì)象,它用來(lái)完成具有 Whois服務(wù) 器的43端口上的所有通訊。在第21行建立了服務(wù)器連接: if (tcpc.Connect("whois.networksolutions.com", 43) != 0) 因?yàn)檫B接失敗是預(yù)料到的結(jié)果,所以這個(gè)方法不能引發(fā)一個(gè)異常。(你還記住異常處理的“要”和“不要”嗎?) 返 回值是一個(gè)錯(cuò)誤代碼,而返回零則說(shuō)明連接成功。 對(duì)于 Whois 查找,我必須首先發(fā)出一些信息給服務(wù)器——我要查找的域名。要完成此項(xiàng)工作,首先獲得一個(gè)引用給當(dāng) 前TCP連接的雙向流(第25行)。接著附加上一個(gè)回車(chē)/換行對(duì) 給域名,以表示詢(xún)問(wèn)結(jié)束。重新以字節(jié)數(shù)組打包,向Whois 服務(wù)器發(fā)送一個(gè)請(qǐng)求(第30行)。 余下的代碼和RequestWebPage類(lèi)極其相似。在該類(lèi)中,我再次利用一個(gè)緩沖區(qū)從遠(yuǎn)程服務(wù)器讀入回應(yīng)。當(dāng)緩沖區(qū)完成 讀入后,連接被斷開(kāi)。返回的回應(yīng)被轉(zhuǎn)給了調(diào)用者。我明確地調(diào)用 Close 方法的原因是我不想等待垃圾收集器毀壞連接。 連接時(shí)間不要過(guò)長(zhǎng),以免占用TCP端口這種稀有資源。 在可以使用.NET 組件中的類(lèi)之前,你必須把它作為一個(gè)庫(kù)來(lái)編譯。盡管現(xiàn)在有了一個(gè)已定義的名字空間,該編譯命令 仍然沒(méi)有變: csc /r:System.Net.dll /t:library /out:whois.dll whois.cs 注意,如果你想該庫(kù)按與C#源文件相同的方法命名,就沒(méi)有必要規(guī)定 /out:開(kāi)關(guān)。規(guī)定該開(kāi)關(guān)是一個(gè)良好的習(xí)慣,因 為很多項(xiàng)目不會(huì)只由單個(gè)源文件組成。如果你規(guī)定了多個(gè)源文件,該庫(kù)以名單中的第一個(gè)命名。
8.2.2 在客戶應(yīng)用程序中使用名字空間 由于你使用了名字空間開(kāi)發(fā)組件,所以客戶也要引入名字空間 using Presenting.CSharp; 或者給名字空間中的成員使用完全資格名(fully qualified name),例如 Presenting.CSharp.WhoisLookup.Query(...);
如果你不期望在名字空間中引入的成員之間出現(xiàn)沖突,using 標(biāo)志( directive)是首選,特別是由于你具有很少的 類(lèi)型時(shí)。使用組件的客戶程序樣本在清單8.4中給出。
清單 8.4 測(cè)試 WhoisLookup 組件
1: using System; 2: using Presenting.CSharp; 3: 4: class TestWhois 5: { 6: public static void Main() 7: { 8: string strResult; 9: bool bReturnValue; 10: 11: try 12: { 13: bReturnValue = WhoisLookup.Query("microsoft.com", out strResult); 14: } 15: catch (Exception e) 16: { 17: Console.WriteLine(e); 18: return; 19: } 20: if (bReturnValue) 21: Console.WriteLine(strResult); 22: else 23: Console.WriteLine("Could not obtain information from server."); 24: } 25: }
第2行利用using 標(biāo)志引入了Presenting.CSharp名字空間,F(xiàn)在,我無(wú)論什么時(shí)候引用WhoisLookup ,都可以忽略名 字空間的完全資格名了。 該程序?qū)?microsoft.com 域進(jìn)行一次Whois 查找——你也可以用自己的域名代替microsoft.com 。允許命令行參數(shù)傳 遞域名,可使客戶的用途更廣。清單8.5 實(shí)現(xiàn)了該功能,但它不能實(shí)現(xiàn)適當(dāng)?shù)漠惓L幚恚榱耸钩绦蚋蹋?br> 清單 8.5 傳遞命令行參數(shù)給Query 方法
1: using System; 2: using Presenting.CSharp; 3: 4: class WhoisShort 5: { 6: public static void Main(string[] args) 7: { 8: string strResult; 9: bool bReturnValue; 10: 11: bReturnValue = WhoisLookup.Query(args[0], out strResult); 12: 13: if (bReturnValue) 14: Console.WriteLine(strResult); 15: else 16: Console.WriteLine("Lookup failed."); 17: } 18: }
你所必須做的就是編譯這個(gè)應(yīng)用程序: csc /r:whois.dll whoisclnt.cs 接著可以使用命令行參數(shù)執(zhí)行該應(yīng)用程序。例如,以 microsoft.com參數(shù)執(zhí)行 whoisclnt microsoft.com 當(dāng)查詢(xún)運(yùn)行成功時(shí),就會(huì)出現(xiàn) microsoft.com的注冊(cè)信息。(清單8.6 顯示了輸出的簡(jiǎn)略版本) 這是一個(gè)很方便的 小程序,通過(guò)組件化的途徑寫(xiě)成的,花不到一個(gè)小時(shí)。如果用C++編寫(xiě),要花多長(zhǎng)時(shí)間?很幸運(yùn),我再也想不起當(dāng)?shù)谝淮斡?br>C++這樣做時(shí),花了多長(zhǎng)的時(shí)間。
清單 8.6 有關(guān) microsoft.com (簡(jiǎn)略) 的Whois 信息 D:\CSharp\Samples\Namespace>whoisclient ...
Registrant: Microsoft Corporation (MICROSOFT-DOM) 1 microsoft way redmond, WA 98052 US Domain Name: MICROSOFT.COM
Administrative Contact: Microsoft Hostmaster (MH37-ORG) msnhst@MICROSOFT.COM Technical Contact, Zone Contact: MSN NOC (MN5-ORG) msnnoc@MICROSOFT.COM Billing Contact: Microsoft-Internic Billing Issues (MDB-ORG) msnbill@MICROSOFT.COM
Record last updated on 20-May-2000. Record expires on 03-May-2010. Record created on 02-May-1991. Database last updated on 9-Jun-2000 13:50:52 EDT.
Domain servers in listed order:
ATBD.MICROSOFT.COM 131.107.1.7 DNS1.MICROSOFT.COM 131.107.1.240 DNS4.CP.MSFT.NET 207.46.138.11 DNS5.CP.MSFT.NET 207.46.138.12
8.2.3 增加多個(gè)類(lèi)到名字空間 使WhoisLookup和RequestWebPage 類(lèi)共存于同一個(gè)名字空間是多么的美妙。既然WhoisLookup已是名字空間的一部分, 所以你只須使RequestWebPage 類(lèi)也成為該名字空間的一部分。 必要的改變很容易被應(yīng)用。你只需使用名字空間封裝RequestWebPage 類(lèi)就可以了:
namespace Presenting.CSharp { public class RequestWebPage { ... } }
盡管兩個(gè)類(lèi)包含于兩個(gè)不同的文件,但在編譯后,它們都是相同名字空間的一部分: csc /r:System.Net.dll /t:library /out:presenting.csharp.dll whois.cs webrequest.cs
你不必要按照名字空間的名字給DLL命名。然而,這樣做會(huì)有助你更容易你記住,當(dāng)編譯一個(gè)客戶應(yīng)用程序時(shí)要引用哪 一個(gè)庫(kù)。
8.3 小結(jié) 在這一章中,你學(xué)到了如何構(gòu)建一個(gè)可以在客戶程序中使用的組件。最初,你不必關(guān)心名字空間,但后面第二個(gè)組件 中介紹了該特性。名字空間在內(nèi)外部均是組織應(yīng)用程序的好辦法。 C#中的組件很容易被構(gòu)建,而且只要庫(kù)和應(yīng)用程序共存于相同的目錄,你甚至不必進(jìn)行特殊的安裝。當(dāng)要?jiǎng)?chuàng)建必須被 多個(gè)客戶使用的類(lèi)庫(kù)時(shí),步驟就有所改變——而下一章將會(huì)告訴你為什么。
|