前面雙節講了關於Encoding的一些概念及簡單應用,需要回顧的朋友們可以點下面的鏈接。今天這一節主要講一下Encoder和Decoder。
C# 小敘 Encoding (一)
C# 小敘 Encoding (二)
關於Encoder和Decoder
從字面意思上理解就是編碼和解碼,CLR有類似的,像UrlDecode()和UrlEncode()是對URL中的參數解碼編碼一樣。Encoder,Decoder這兩個是用來字符和字節之間的編碼和解碼的,是兩個類型,而且還是抽象的,所以我們不能直接實例化它,但是目前CLR中給我們使用的類型中沒有它們的派生類,不過CLR內部實現里肯定有它們的派生類。比如說下面的DecoderNLS就被定義成了internal,做為調用者的我們是看不到的。Encoder和Decoder是在Encoding里以兩個虛方法出現的,GetEncoder()和GetDecoder(),派生類里有不同的實現。比較UTF-8里就返回UTF8Decoder。
[Serializable]
internal class UTF8Decoder : DecoderNLS, ISerializable
[Serializable]
internal class DecoderNLS : Decoder, ISerializable
public override Decoder GetDecoder()
{
return new UTF8Decoder(this);
}
用法也比較簡單,下面代碼不詳細解釋了。
//Encoder string test = "ABCDE1234測試"; Console.WriteLine("The test of string is {0}", test); Encoding encoding = Encoding.UTF8; char[] source = test.ToCharArray(); int strLength = test.Length; int len = encoding.GetEncoder().GetByteCount(source, 0, strLength, false); byte[] result = new byte[len]; encoding.GetEncoder().GetBytes(source, 0, strLength, result, 0, false); Console.WriteLine("After Encoder,the byte of test is output below。"); foreach (byte b in result) { Console.Write("{0:X}-", b); } Console.WriteLine(); //Decoder Console.Write("After Decoder,the string is "); int deslen = encoding.GetDecoder().GetCharCount(result, 0, result.Length); char[] des = new char[deslen]; encoding.GetDecoder().GetChars(result, 0, result.Length, des, 0); foreach (char c in des) { Console.Write("{0}", c); }
也許有人看出來了,這和Encoding的編碼和解碼沒什么區別啊,Encoding還會更簡單,選擇更多些,為何我還要多創建兩個對象?是的,沒錯,如果對一塊完整的數據流,完全沒必要去創建這兩個對象,Encoding的功能已經可以實現了,但是如果我們要操作的是文件流或網絡流,需要跨塊處理,比如每次我都從一個流中讀取5個字節進行處理?看一下代碼就知道了
public static void Main() { //臨時文件 string path = Path.GetTempFileName(); File.WriteAllText(path, "ABCDE1234測試∑我", new UTF8Encoding(false)); //創建Decoder對象 //Decoder dec = Encoding.UTF8.GetDecoder(); using (FileStream fs = File.OpenRead(path)) { byte[] buffer; int size; //每次都讀取5個字節 buffer = new byte[5]; while ((size = fs.Read(buffer, 0, 5)) > 0) { //char[] chars = new char[dec.GetCharCount(buffer, 0, size)]; //dec.GetChars(buffer, 0, size, chars, 0); char[] chars1 = Encoding.UTF8.GetChars(buffer, 0, size); if (chars1.Length != 0) { //Console.Write("{0,-10}", new string(chars)); Console.Write("{0,-10}", new string(chars1)); Console.Write("字節:"); PrintBytes(buffer, size); } Thread.Sleep(500); } } Console.Read(); } static void PrintBytes(byte[] bytes, int len) { for (int i = 0; i < len; i++) Console.Write("{0:X2} ", bytes[i]); Console.WriteLine(); }
我們先將字符串"ABCDE1234測試∑我”用UTF-8編碼寫到一個臨時文件里,然后放到一個stream里,再對這個stream每次讀取5個字節的操作。我們可以看出來這個字符串轉化成字節的長度為1+1+1+1+1+1+1+1+1+3+3+3+3,讀取前5個是沒任何問題的,都是單字節字符。再讀接下來五個時就有問題了,第10個字符是一個多字節字符,其中的兩個字節要放下一次的讀取了,Encoding.GetChars()就不能正確識別了,第10個字符將被識別為亂碼,將會以為?顯示。
下面是打印的結果:
我們把注釋的代碼取消注釋后,再重新運行看一下結果,
Decoder dec = Encoding.UTF8.GetDecoder();
char[] chars = new char[dec.GetCharCount(buffer, 0, size)];
dec.GetChars(buffer, 0, size, chars, 0);
Console.Write("{0,-10}", new string(chars));
最左邊的是用Decoder解碼的,中間的是用Encoding解碼的
亂碼消失了,Decoder可以正確的得到我們想要的結果,而且Encoding卻有亂碼。為什么會這樣?
Encoder和Decoder 維護對 GetBytes() 和GetChars()的連續調用間的狀態信息,因此它可以正確地對跨塊的字符序列進行編碼。Encoder 還保留數據塊結尾的尾部字符並將這些尾部字符用在下一次編碼操作中。例如,一個數據塊的末尾可能是一個不匹配的高代理項,而與其匹配的低代理項則可能位於下一個數據塊中。因此,Decoder 和 Encoder 對網絡傳輸和文件操作很有用,這是因為這些操作通常處理數據塊而不是完整的數據流。StreamReader和SteamWriter關於讀和書的就是用Decoder和Encoder。
//StreamWriter
int count = this.encoder.GetBytes(this.charBuffer, 0, this.charPos, this.byteBuffer, 0, flushEncoder)
//StreamReader
charIndex = this.decoder.GetChars(this.byteBuffer, 0, this.byteLen, this.charBuffer, charIndex);
中文的全角和半角問題
這個問題有人問過我,我查了一些資料。因為所有的字符在CLR中都是以Unicode-16編碼的,這個問題就比較好處理了,全角和半角的值它們相差65248,除了空格相差12256。所以全角的字符若是想轉換成半角除空格減12256外,其他相減65248便是相應的半角。具體可以參考園子里的這篇博客:C#全角和半角轉換
小結
關於Encoding到這里已經講的差不多了,總結一下:
1. CLR中字符串都是Unicode 16 編碼
2. 盡量調用Encoding的靜態屬性UTF8,Unicode等,而不是去實例它們
3. 盡量避免用Encoding.Defalut
4. BOM是用來識別哪一種編碼的,默認是帶有的,如果不需要,那么調用它們的帶有參數的構造器,找到相應參數傳false
5. 在對文件流和網絡流操作時,應該用Encoder和Decoder