第八章 用C#寫組件
這一章關于用C#寫組件。你學到如何寫一個組件,如何編譯它,且如何在一個客戶程序中使用它。更深入一步是運用 名字空間來組織你的應用程序。 這章由兩個主要大節構成: 。你的第一個組件 。使用名字空間工作
8.1 你的第一個組件 到目前為止,在本書中提到的例子都是在同一個應用程序中直接使用一個類。類和它的使用者被包含在同一個執行文 件中。現在我們將把類和使用者分離到組件和客戶,它們分別位于不同的二進制文件中(可執行文件)。 盡管你仍然為組件創建一個 DLL,但其步驟與用C++寫一個COM組件差別很大。你很少涉及到底層結構。以下小節說明 了如何構建一個組件以及使用到它的客戶:
。構建組件 。編譯組件 。創建一個簡單的客戶應用程序
8.1.1 構建組件 因為我是一個使用范例迷,我決定創建一個相關Web的類,以方便你們使用。它返回一個Web網頁并儲存在一個字符串 變量中,以供后來重用。所有這些編寫都參考了.NET框架的幫助文檔。 類名為RequestWebPage;它有兩個構造函數—— 一個屬性和一個方法。屬性被命名為URL,且它儲存了網頁的Web地 址,由方法GetContent返回。這個方法為你做了所有的工作(見清單8.1)。
清單 8.1 用于從Web服務器返回HTML網頁的RequestWebPage 類
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: // 給回應設置字節緩沖區 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 以加速分配過程 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: // 賦給輸出參數 50: strContent = strResponse.ToString(); 51: } 52: }
本應該利用無參數構造函數完成工作,但我決定在構造函數中初始化URL,這可能會很有用。當后來決定要改變URL 時——為了返回第二個網頁,例如,通過URL屬性的get和set訪問標志使它被公開了。 有趣的事始于GetContent方法。首先,代碼對URL實行十分簡單的檢查,如果它不適合,就會引發一個 ArgumentException 異常。之后,我請求WebRequestFactory ,以創建一個基于傳遞給它的URL的WebRequest對象。 因為我不想發送cookies、附加頭和詢問串等,所以立即訪問WebResponse(第32行)。如果你需要請求上述任何的功 能,必須在這一行之前實現它們。 第35和36行初始化一個字節緩沖區,它用于從返回流中讀數據。暫時忽略StringBuilder 類,只要返回流中仍然有要 讀的數據,while循環就會簡單地重復。最后的讀操作將返回零,因此結束了該循環。 現在我想回到StringBuilder類。為什么用這個類的實例而不是簡單地把字節緩沖區合并到一個字符串變量?看下面這 個例子: strMyString = strMyString + "some more text"; 這里很清楚,你正在拷貝值。常量 "some more text" 以一個字符串變量類型被加框,且根據加法操作創建了一個新 的字符串變量。接著被賦給了 strMyString。有很多次拷貝,是嗎? 但你可能引起爭論 strMyString += "some more text"; 不要炫耀這種行為。對不起,對于C#這是一個錯誤的答案。其操作完全與所描述的賦值操作相同。 不涉及該問題的另外的途徑是使用StringBuilder類。它利用一個緩沖區進行工作,接著,在沒有發生我所描述的拷貝 行為的情況下,你進行追加、插入、刪除和替換操作。這就是為什么我在類中使用它來合并那些讀自緩沖區中的內容。 該緩沖區把我帶進了這個類中最后重要的代碼片段——第45行的編碼轉換。它只不過涉及到我獲得請求的字符集。 最后,當所有的內容被讀入且被轉換時,我顯式地從 StringBuilder請求一個字符串對象并把它賦給了輸出變量。一 個返回值仍然會導致另外的拷貝操作。
8.1.2 編譯組件 到目前為止,你所做的工作與在正常應用程序的內部編寫一個類沒有什么區別。所不同的是編譯過程。你必須創建一 個庫而不是一個應用程序: csc /r:System.Net.dll /t:library /out:wrq.dll webrequest.cs 編譯開關/t:library 告訴C#編譯,要創建一個庫而不是搜尋一個靜態 Main方法。同樣,因為我正在使用 System.Net名字空間,所以必須引用 (/r:)它的庫,這個庫就是System.Net.dll。 你的庫命名為 wrq.dll,現在它準備用于一個客戶應用程序。因為在這章中我僅使用私有組件工作,所以你不必把庫 拷貝到一個特殊的位置,而是拷貝到客戶應用程序目錄。
8.1.3 創建一個簡單的客戶應用程序 當一個組件被寫成且被成功地編譯時,你所要做的就是在客戶應用程序中使用它。我再次創建了一個簡單的命令行應 用程序,它返回了我維護的一個開發站點的首頁(見清單8.2)。
清單 8.2 用 RequestWebPage 類返回一個簡單的網頁
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: } 成員
注意,我已經在一個try catch語句中包含了對 GetContent的調用。其中的一個原因是GetContent可能引發一個 ArgumentException異常。此外,我在組件內部調用的.NET框架類也可以引發異常。因為我不能在類的內部處理這些異常, 所以我必須在這里處理它們。 其余的代碼只不過是簡單的組件使用——調用標準的構造函數,存取一個屬性,并執行一個方法。但等一下:你需要 注意何時編譯應用程序。一定要告訴編譯器,讓它引用你的新組件庫DLL: csc /r:wrq.dll wrclient.cs 現在萬事俱備,你可以測試程序了。輸出結果會滾屏,但你可以看到應用程序工作。使用了常規的表達式,你也可以 增加代碼,以解析返回的HTML,并依據你個人的喜好,提取信息。我預想會使用到這個類新版本的SSL(安全套接字層), 用于ASP+網頁中的在線信用卡驗證。 你可能會注意到,沒有特殊的using 語句用于你所創建的庫。原因是你在組件的源文件中沒有定義名字空間。
8.2 使用名字空間工作 你經常使用到名字空間,例如System 和System.Net。C#利用名字空間來組織程序,而且分層的組織使一個程序的成員 傳到另一個程序變得更容易。 盡管不強制,但你總要創建名字空間,以清楚地識別應用程序的層次。.NET框架會給出構建這種分層的良好思想。 以下的代碼片段顯示了在C#原文件中簡單的名字空間 My.Test(點號表示一個分層等級)的聲明:
namespace My.Test { //這里的任何東西屬于名字空間 }
當你訪問名字空間中的一個成員時,也有必要使用名字空間標識符完全地驗證它,或者利用using標志把所有的成員引 入到你當前的名字空間。本書前面的例子演示了如何應用這些技術。 在開始使用名字空間之前,只有少數有關存取安全的詞。如果你不增加一個特定的存取修飾符,所有的類型將被默認 為internal 。當你想從外部訪問該類型時,使用 public 。不允許其它的修飾符。 這是關于名字空間充分的理論。讓我們繼續實現該理論——以下小節說明了當構建組件應用程序時,如何使用名字空 間 。在名字空間中包裝類 。在客戶應用程序中使用名字空間 。為名字空間增加多個類
8.2.1 在名字空間中包裝類 既然你知道了名字空間的理論含義,那么讓我們在現實生活中實現它吧。在這個和即將討論到的例子中,自然選擇到 的名字空間是Presenting.CSharp。為了不使你厭煩,僅僅是把RequestWebPage包裝到Presenting.CSharp中,我決定寫一 個類,用于 Whois查找(見清單8.3)。
清單 8.3 在名字空間中實現 WhoisLookup類
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 服務器 21: if (tcpc.Connect("whois.networksolutions.com", 43) != 0) 22: return false; 23: 24: // 獲取流 25: Stream s = tcpc.GetStream(); 26: 27: // 發送請求 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類。要聲明自己新的名字空間,實際要 做的就是這些。 在WhoisLookup類中當然具有一些有趣代碼,特別是由于它說明了使用C#進行socket編程是多么的容易。在static Query method中經過 not-so-stellar域名檢查之后,我實例化了TCPClient類型的一個對象,它用來完成具有 Whois服務 器的43端口上的所有通訊。在第21行建立了服務器連接: if (tcpc.Connect("whois.networksolutions.com", 43) != 0) 因為連接失敗是預料到的結果,所以這個方法不能引發一個異常。(你還記住異常處理的“要”和“不要”嗎?) 返 回值是一個錯誤代碼,而返回零則說明連接成功。 對于 Whois 查找,我必須首先發出一些信息給服務器——我要查找的域名。要完成此項工作,首先獲得一個引用給當 前TCP連接的雙向流(第25行)。接著附加上一個回車/換行對 給域名,以表示詢問結束。重新以字節數組打包,向Whois 服務器發送一個請求(第30行)。 余下的代碼和RequestWebPage類極其相似。在該類中,我再次利用一個緩沖區從遠程服務器讀入回應。當緩沖區完成 讀入后,連接被斷開。返回的回應被轉給了調用者。我明確地調用 Close 方法的原因是我不想等待垃圾收集器毀壞連接。 連接時間不要過長,以免占用TCP端口這種稀有資源。 在可以使用.NET 組件中的類之前,你必須把它作為一個庫來編譯。盡管現在有了一個已定義的名字空間,該編譯命令 仍然沒有變: csc /r:System.Net.dll /t:library /out:whois.dll whois.cs 注意,如果你想該庫按與C#源文件相同的方法命名,就沒有必要規定 /out:開關。規定該開關是一個良好的習慣,因 為很多項目不會只由單個源文件組成。如果你規定了多個源文件,該庫以名單中的第一個命名。
8.2.2 在客戶應用程序中使用名字空間 由于你使用了名字空間開發組件,所以客戶也要引入名字空間 using Presenting.CSharp; 或者給名字空間中的成員使用完全資格名(fully qualified name),例如 Presenting.CSharp.WhoisLookup.Query(...);
如果你不期望在名字空間中引入的成員之間出現沖突,using 標志( directive)是首選,特別是由于你具有很少的 類型時。使用組件的客戶程序樣本在清單8.4中給出。
清單 8.4 測試 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 標志引入了Presenting.CSharp名字空間。現在,我無論什么時候引用WhoisLookup ,都可以忽略名 字空間的完全資格名了。 該程序對 microsoft.com 域進行一次Whois 查找——你也可以用自己的域名代替microsoft.com 。允許命令行參數傳 遞域名,可使客戶的用途更廣。清單8.5 實現了該功能,但它不能實現適當的異常處理(為了使程序更短)。
清單 8.5 傳遞命令行參數給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: }
你所必須做的就是編譯這個應用程序: csc /r:whois.dll whoisclnt.cs 接著可以使用命令行參數執行該應用程序。例如,以 microsoft.com參數執行 whoisclnt microsoft.com 當查詢運行成功時,就會出現 microsoft.com的注冊信息。(清單8.6 顯示了輸出的簡略版本) 這是一個很方便的 小程序,通過組件化的途徑寫成的,花不到一個小時。如果用C++編寫,要花多長時間?很幸運,我再也想不起當第一次用 C++這樣做時,花了多長的時間。
清單 8.6 有關 microsoft.com (簡略) 的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 增加多個類到名字空間 使WhoisLookup和RequestWebPage 類共存于同一個名字空間是多么的美妙。既然WhoisLookup已是名字空間的一部分, 所以你只須使RequestWebPage 類也成為該名字空間的一部分。 必要的改變很容易被應用。你只需使用名字空間封裝RequestWebPage 類就可以了:
namespace Presenting.CSharp { public class RequestWebPage { ... } }
盡管兩個類包含于兩個不同的文件,但在編譯后,它們都是相同名字空間的一部分: csc /r:System.Net.dll /t:library /out:presenting.csharp.dll whois.cs webrequest.cs
你不必要按照名字空間的名字給DLL命名。然而,這樣做會有助你更容易你記住,當編譯一個客戶應用程序時要引用哪 一個庫。
8.3 小結 在這一章中,你學到了如何構建一個可以在客戶程序中使用的組件。最初,你不必關心名字空間,但后面第二個組件 中介紹了該特性。名字空間在內外部均是組織應用程序的好辦法。 C#中的組件很容易被構建,而且只要庫和應用程序共存于相同的目錄,你甚至不必進行特殊的安裝。當要創建必須被 多個客戶使用的類庫時,步驟就有所改變——而下一章將會告訴你為什么。
|