> 第八章 用C#寫組件(rainbow 翻譯) (來自重粒子空間)
來源:互聯網
上載者:User
第八章 用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#中的組件很容易被構建,而且只要庫和應用程式共存於相同的目錄,你甚至不必進行特殊的安裝。當要建立必須被多個客戶使用的類庫時,步驟就有所改變——而下一章將會告訴你為什麼。