前面雙節講了關於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);
}
用法也比較簡單,下面代碼不詳細解釋了。
//Encoderstring 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();//DecoderConsole.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