漫談.NET開發中的字符串編碼

作者: 金旭亮  來源: 博客園  發布時間: 2010-11-28 21:24  閱讀: 1252 次  推薦: 2   原文鏈接   [收藏]  
摘要:本篇擴充閱讀將向讀者介紹將字符串對象的序列化,這里面的關鍵是字符串應該如何編碼和解碼為二進制數值,從而可以把它們保存到文件流(FileStream)中,或者通過網絡流(NetworkStream)將它們遠程發送到另一臺計算機上。

         在《 .NET 4.0面向對象編程漫談》基礎篇《13.2.1 序列化與流》中,向大家介紹了如何向流中序列化一個對象。

         本篇擴充閱讀將向讀者介紹將字符串對象的序列化,這里面的關鍵是字符串應該如何編碼和解碼為二進制數值,從而可以把它們保存到文件流(FileStream)中,或者通過網絡流(NetworkStream)將它們遠程發送到另一臺計算機上。

  1 引子

         在實際開發中,經常需要將一些字符串寫入到文本文件中,或者從文本文件中讀入字符串,在.NET應用程序中,通常使用StreamReaderStreamWriter兩個類完成這一工作,比如以下代碼將fileContent字串寫入到FileName文件中:

static void WriteFileUseStreamWriter(String fileContent, String FileName)
{

  using (StreamWriter writer = new StreamWriter(FileName))
  {
    writer.Write(fileContent);
  }
}

       如果你使用.NET基類庫中相關類(比如StreamReader或下面用到的File類)去讀取這個文件,你會發現一切如你所愿地正常運轉:

WriteFileUseStreamWriter("中國ab""test.txt");
Console.WriteLine(File.ReadAllText("test.txt"));     //輸出:“中國ab” 
       

        由于多數情況下我們都工作在中文Windows下,而且往往都是某個.NET程序寫,另一個.NET程序讀,所以,不少.NET程序員可能都沒注意到這其中其實存在著一個字符編碼的問題,在特定的場合下,這一問題會給我們帶來麻煩。

         請看圖1

1 記事本支持的編碼方式

         默認情況下,Windows記事本以ANSI編碼方式保存文件。如圖1所示,如果文本內容為“中國ab,記事本將其以ASNI方式保存為“test.txt”,則以下代碼將“罷工”了(參看圖2):

Console.WriteLine(File.ReadAllText("test.txt"));  

2 漢字將顯示為亂碼

         圖 2所示,File.ReadAllText方法打開“test.txt”文件時,會發現英文字符可以正常顯示,但中文將顯示為亂碼。

  2 了解字符的編碼

         我們可以做個試驗,使用記事本將“中國ab”這個中英混雜的字符串以不同編碼方式保存為多個“.txt”文件,然后直接查看其二進制內容:

比對字符編碼

          圖 3展示了“中國ab”按四種編碼方式(ANSIUTF8UnicodeUnicode Big Endian)得到的不同二進制數據。

         以英文字符“a”為例,ANSIUTF8得到的數值都是“61”,但Unicode將它擴充為2個字節16位的二進制(“61 00”和“00 61”),所以我們又將這種編碼方式稱為UTF-16

         UTF-16又可以細分為2種編碼方式:Big Endian方式與Little_Edian方式,這兩者的唯一區別在于字節排列順序剛好相反, Little_Edian方式將“a”編碼為“61 00”,而Big Endian方式則編碼為“00 61”。

         現在看看中文字符,“中國”兩個漢字,ANSI編碼為“D6 D0 B9 FA”,4個字節,一個漢字占兩個字節,而UTF8則編碼為“E4 B8 AD E5 9B BD”,6個字節,一個漢字占3個字節!這說明UTF8是一種“變長”的編碼,可能使用1~4個字節來表示某個字符。

         另外,我們看到UTF8Unicode編碼(不管是Big Endian還是Little Endian)前面都有幾個標記字符,這些字符放在文本文件的開頭,稱為“BOMByte Order Mark,字節順序標記)”指明了文本的編碼方式,以下是.NET程序中常見的字符編碼方式的BOM值:

 

編碼

BOM

UTF-8

EF BB BF

UTF-16  big endian

FE FF

UTF-16  little endian

FF FE

UTF-32  big endian

00 00 FE FF

UTF-32  little endian

FF FE 00 00

 

         了解了上述基礎知識,我們就可以依據BOM值自動檢測字符串的編碼方式,從而正確從二進制數據流中解碼,以下代碼檢測文本二進制數據是否采用UTF8編碼:

 
//打開文件讀取二進制數據
byte[] FileContents = File.ReadAllBytes(FilePath);
int filelength = FileContents.Length;
//檢測BOM
if (FileContents[0] == 0xef && FileContents[1] == 0xbb && FileContents[2] == 0xbf)
{

//按UTF8解碼字符串,注意要排除掉BOM占用的3個字節。
String content= Encoding.UTF8.GetString( FileContents, 3, filelength - 3);
Console.WriteLine(content);
}

         其他的編碼方式都可以“依樣畫葫蘆”。

  3 詳解.NET基類庫中與字符編碼相關的類

         前述代碼中的Encoding類是.NET實現字符編碼解碼的核心類型。圖4展示了它的屬性:

4 Encoding類型

         如圖4所示,Encoding類型提供了UTF8Unicode等編碼和解碼器,調用它的Get系列方法完成編碼和解碼工作,以下為示例代碼:

 
//編碼
byte[] bytes = Encoding.UTF8.GetBytes("中國ab");
foreach (byte value in bytes)
Console.Write(
" {0}", value.ToString("x")); //轉化為16進制
Console.WriteLine();
//解碼
char[] chars = Encoding.UTF8.GetChars(bytes);
foreach (char ch in chars)
Console.Write(
" {0}", ch);

 

  運行結果如下:

圖5 編碼和解碼

         需要注意的是上述二進制值不包括BOM

         事實上,.NET中的StreamWriter默認采用UTF8編碼格式編碼字符串,但并不將UTF8所對應的BOM值(“EF BB BF”)寫入到二進制流中。以下是StreamWriter的一個構造函數聲明:

public StreamWriter(string path) : this(path, falseUTF8NoBOM0x400)
{    }

         類似地,File.ReadAllText()方法在內部使用UTF8來讀取指定文件中的字符串

public static string ReadAllText(string path)
{
    
//……
    return InternalReadAllText(path, Encoding.UTF8);
}

     由于默認編碼方式一致,所以配套使用StreamWriterFile.ReadAllText()方法可以正確地從流中存取字符串。

    出于提升代碼可維護性考慮,正確的用法應該是明確地指明編碼方式:

static void WriteFileUseStreamWriterUseUTF8(String fileContent, String FileName)
{
    
using (StreamWriter writer = new StreamWriter(FileName, false, Encoding.UTF8))
    {
                   writer.Write(fileContent);
    }
}

          這時,StreamWriter會在文件開頭寫入UTF8BOM標記,從而讓其他的應用程序可以很明確地知道本文件中字符串的編碼方式。

  4 談談有趣的Encoding.Default屬性        

         Encoding類中有一個有趣的Default屬性,它的類型很奇怪,叫作“DBCSCodePageEncoding”,這個類型在MSDN中是查不到的。

         DBCS”代表“double-byte character set(雙字節字符集)”,它是與“SBCSsingle-byte character set,單字節字符集)”相對應的,SBCS中,所有字符都只占一個字節,所以能表示的字符數有限,但在DBCS中,英文字母占一個字節,漢字等特殊字符占有兩個字節,從而擴充了Windows能顯示的字符數量。

         DBCSCodePageEncoding中的“Code Page”被稱為“代碼頁”,每個代碼頁定義了特定的編碼將如何對應于特定的字符(比如簡體和繁體中文就分別定義在不同的代碼頁中),因此,同樣的二進制數值,在不同的代碼頁中,會代表不同的字符。中文Windows通過使用基于代碼頁的DBCS編碼方式,可以方便地以多種編碼方式顯示和處理字符串。

         我們在MSDN中可以查到所有代碼頁的編號,下面列出了可能比較常用的代碼頁標識:

代碼頁標識值

.NET中的名字

936

gb2312

950

big5

1200

utf-16

52936

hz-gb-2312

54936

GB18030

65000

utf-7

65001

utf-8

         .NET應用程序可以通過以下方式獲取指定代碼頁的編碼對象:        

Encoding encode=Encoding.GetEncoding(CodePage);

         以下代碼將按照指定代碼頁編碼字符串,并將其寫入到文件中:

static void WriteFileUseStreamWriterUseCodePage(String fileContent,String FileName,
   int CodePage)
{
  
using (StreamWriter writer = new StreamWriter(FileName, false,
        Encoding.GetEncoding(CodePage)))

   {
        writer.Write(fileContent);
   }
}

    現在,使用以下代碼將按照UTF8編碼字符串:

WriteFileUseStreamWriterUseCodePage("中國ab""test.txt"65001);

  5 結束語

         除了本文所介紹的將字符串保存到文本文件的這種場景,字符串的編碼方式在基于套接字的TCP/UDP網絡編程也非常重要,比如.NET提供了一個NetworkStream封裝Socket實現網絡通訊,如果希望將一個命令字符串從客戶端送到服務端,服務端通過讀取這個字符串完成特定的工作,則編碼方式就很重要了,客戶端與服務端必須采用一致的編碼方式傳送命令,否則,網絡服務就有可能因為無法解析客戶端發送過來的數據而Down掉。

         有關網絡編程的內容很有趣,我的下一篇文章會介紹.NET套接字編程。

         好了,這篇介紹字符串編碼的短文寫完了,希望本文能對讀者有所幫助,如有錯誤,敬請指正。

2
0
 
標簽:.NET 字符串
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()