前面双节讲了关于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