前言
众所周知计算机只能识别二进制数字,如1010,1001。我们屏幕所看到的文字,字符都是和二进制转换后的结果。将我们的文字按照某种规则转换二进制存储在计算机上,这一个过程叫字符编码,反之就是解码。目前存在多种字符编码方式,一组二进制数字根据不同的解码方式,会得到不同的结果,有时甚至会得到乱码。这也就是为什么我们打开网页时有时会是乱码,打开一个文本文件有时也是乱码,而换了一种编码就恢复正常了。CLR中的所有字符都是16位Unicode来表示的。CLR中的Encoding就是用于字节和字符之间的转换的。更多字符编码知识请参考,字符集和字符编码(Charset & Encoding)
关于Encoding
CLR中的Encoding是在System.Text命名空间下的,它是一个抽象类(abstract class), 所以不能被直接实例化,它主要有如下的派生类:ASCIIEnding,UnicodeEncoding,UTF32Encoding,UTF7Encoding,UTF8Encoding,你可以根据需要选择一个合适的Encoding来进行编码和解码。你也可以调用Encoding的静态属性ASCII,Unicode,UTF32,UTF7,UTF8,来构造一个Encoding。其中Unicode是表示16位Encoding。调用静态属性和实例化一个子类的效果是一样的,如下代码。
1 Encoding encodingUTF8 = Encoding.UTF8; 2 Encoding encodingUTF8 = new UTF8Encoding(true);
以下是这些类型的一些简单描述:
ASCII编码 将16位字符编码成ASCII码,只能转换值小于Ox0080的16字符,并且被转换成单字节,就是说一个字符对应一个字节。当字符都在ASCII范围(0X00~0X7F)内时,可以用这种编码,它的速度非常快,适合于英美地区的字符。这种编码非常有限,汉字会被转换成乱码。在CLR对应ASCIIEndoing。
UTF-16 每个字符编码成2个字节,它不会对字符产生任何影响,也不会涉及到压缩处理,性能非常好,因为CLR中的字符也是16位的Unicode。在CLR中对应UnicodeEncoding。
UTF-32 使用4个字节编码成一个字符。从内存角度上讲,它并不是一种高效能的编码方案,因为第个字符都是4个字节,特别占内存,所以很少用来做文件和网络流的编码解码。在CLR中对应UTF32Encoding。
UTF-8 值在Ox0080之下的字符压缩成一个字符,也就是ASCII码;值在0X0080---0X07FF之间的字符都转换成2个字符,适合用于欧洲和中东地区。0X0800以上被转换成3个字符,适合于东亚地区的字符。代理项被转换成4个字节。因此,它是一种非常流行的编码,适用于互联网。它在处理0X0800以上的字符效率不好UTF-16。在CLR中对应UTF8Encoding。
UTF-7 这咱编码通常用于旧的系统,那时的系统是用7位值表示。目前已经被Unicode协淘汰。在CLR中对应UTF7Encoding。
从性能角度上来讲,如果你的代码需要在多处调用一个Encoding,微软建议你使用静态成员的方式构造一个Encoding对象,而不是构造实例。它的内部实现是一个单例模式。
public static Encoding UTF8 { get { if (utf8Encoding == null) { utf8Encoding = new UTF8Encoding(true); } return utf8Encoding; } }
如果你知道某种编码的代码页(code page)或名字,那么你可以调用Encoding的静态方法GetEncoding(int codepage),GetEncoding(string name)来构造一个Encoding,比如我们常用的用于显示简体中文的gb2312,它的代码页是936,我们就可以这样定义:
Encoding encodingGB2312=Encoding.GetEncoding("gb2312"); Encoding encodingGB2312=Encoding.GetEncoding(936);
目前有几十种文字代码页,分别对应于不同的国家,不同的语言,它们只是对应Unicode字符集里的相一部分,比如说936,它只是对应于Unicode字符集里简体中文的那一部分,如果你想正确的显示繁体字,那么就要用中文繁体对应的代码页950。具体的代码页有哪些可以参考MSDN或园子里这篇文章,C#文字代码页,文字编码的代码页名称速查表。
下面代码可以返回CLR中所有的Encoding。
foreach (EncodingInfo eInfo in Encoding.GetEncodings()) { Console.WriteLine("Encoding code page is {0}, encoding name is {1}", eInfo.CodePage, eInfo.Name); Console.WriteLine("Encoding dispaly name is {0}", eInfo.DisplayName); }
Encoding对象有一个静态属性Default,它返回的也是一个Encoding对象,至于返回哪个语言的Encoding取决于你电脑里-->控制面板->区域和语言 里面的设置,也就是ANSI。如下图,我电脑里设置是Chinses(Simplified, PRC)也就是简体中文,那么对应的就是gb2312,所以下面代码会打印gb2312。如果你的代码在不止一个国家里使用,那么你最好不要Encoding.Default,这样会造成乱码,你最好用Encoding.UTF8。
Encoding encoding1 = Encoding.Default;
Console.WriteLine(encoding1.WebName);
Encoding用法
Encoding用法比较简单,如果只是字节和字符的互相转换,GetBytes()和GetChars()这两个方法及它们的重载基本上会满足你所有要求。
GetByteCount()及其重载是得到一个字符串转换成字节时实际的字节个数。
GetCharCount()及其重载是得到一个字节数组转换成字符串的大小。
要注意这两个方法:int GetMaxByteCount(int charCount); int GetMaxCharCount(int byteCount);
它并不是你期望的那样,如果是单字节就返回charCount,如果是双字节就返回chartCount*2,而是chartCount+1,(chartCount+1)*2。
Console.WriteLine("The max byte count is {0}.", Encoding.Unicode.GetMaxByteCount(10)); Console.WriteLine("The max byte count is {0}.", Encoding.ASCII.GetMaxByteCount(10));
上面的结果分别是22和11,而不是20,10。我在一篇英文博客里找到了原因,我英语不好,没有弄明白什么是high surrogate和low surrogate:http://blogs.msdn.com/b/shawnste/archive/2005/03/02/383903.aspx
For example, Encoding.GetEncoding(1252).GetMaxByteCount(1) returns 2. 1252 is a single byte code page (encoding), so generally one would expect that GetMaxByteCount(n) would return n, but it doesn't, it usually returns n+1.
One reason for this oddity is that an Encoder could store a high surrogate on one call to GetBytes(), hoping that the next call is a low surrogate. This allows the fallback mechanism to provide a fallback for a complete surrogate pair, even if that pair is split between calls to GetBytes(). If the fallback returns a ? for each surrogate half, or if the next call doesn't have a surrogate, then 2 characters could be output for that surrogate pair. So in this case, calling Encoder.GetBytes() with a high surrogate would return 0 bytes and then following that with another call with only the low surrogate would return 2 bytes.
下面代码是Encoding的简单应用,大家可以打印一下结果,然后结合上篇讲的,会有所收获的。
static void Output(Encoding encoding,string t) { Console.WriteLine(encoding.ToString()); byte[] buffer = encoding.GetBytes(t); foreach (byte b in buffer) { Console.Write(b + "-"); } string s = encoding.GetString(buffer); Console.WriteLine(s); }
string strTest = "test我镕a有κ"; Console.WriteLine(strTest); Output(Encoding.GetEncoding("gb18030"), strTest); Output(Encoding.Default, strTest); Output(Encoding.UTF32, strTest); Output(Encoding.UTF8, strTest); Output(Encoding.Unicode, strTest); Output(Encoding.ASCII, strTest); Output(Encoding.UTF7, strTest);
关于BOM
BOM全称是Byte Order Mark,即字节顺序标记,是一段二进制,用于标识一个文本是用什么编码的,比如当用Notepad打开一个文本时,如果文本里包括这一段BOM,那么它就能判断是采用哪一种编码方式,并用相应的解码方式,就会正确打开文本不会有乱码。如果没有这一段BOM,Notepad会默认以ANSI打开,这种会有乱码的可能性。可以通过Encoding的方法GetPreamble()来判断这编码有没有BOM,目前CLR中只有下面5个Encoding有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
用Encoding的静态属性Unicode,UTF8,UTF32构造的Encoding都是默认带有BOM的,如果你想在写一个文本时(比如XML文件,如果有BOM,会有乱码的),不想带BOM,那么就必须用它们的实例,
Encoding encodingUTF16=new UnicodeEncoding(false, false);//第二个参数必须要为false Encoding encodingUTF8=new UTF8Encoding(false); Encoding encodingUTF32=new UTF32Encoding(false,false);//第二个参数必须要为false
读写文本和BOM的关系可以参考园子里这篇博客,讲的很详细我就不重复了,.NET(C#):字符编码(Encoding)和字节顺序标记(BOM)
判断一个文本的编码方式
如果给定一个文本,我们不知道它的编码格式,解码时我们如何选择Encoding呢?答案是根据BOM来判断到底是哪种Unicode,如果没有BOM,这个就很难说了,这个得根据文本文件的来源了,一般是用Encoding.Default,这个是根据你计算机里当前的设置而返回不同的值。如果你的文件是来自一位国际友人的话,你最好用UTF-8来解码了。下面的代码在指定文件没有BOM时,不能保证其正确性,如果你要用到你项目中,千万要注意这一点。
/// <summary> ///Return the Encoding of a text file. Return Encoding.Default if no Unicode // BOM (byte order mark) is found. /// </summary> /// <param name="FileName"></param> /// <returns></returns> public static Encoding GetFileEncoding(String FileName) { Encoding Result = null; FileInfo FI = new FileInfo(FileName); FileStream FS = null; try { FS = FI.OpenRead(); Encoding[] UnicodeEncodings = { Encoding.BigEndianUnicode, Encoding.Unicode, Encoding.UTF8, Encoding.UTF32, new UTF32Encoding(true,true) }; for (int i = 0; Result == null && i < UnicodeEncodings.Length; i++) { FS.Position = 0; byte[] Preamble = UnicodeEncodings[i].GetPreamble(); bool PreamblesAreEqual = true; for (int j = 0; PreamblesAreEqual && j < Preamble.Length; j++) { PreamblesAreEqual = Preamble[j] == FS.ReadByte(); } // or use Array.Equals to compare two arrays. // fs.Read(buf, 0, Preamble.Length); // PreamblesAreEqual = Array.Equals(Preamble, buf) if (PreamblesAreEqual) { Result = UnicodeEncodings[i]; } } } catch (System.IO.IOException ex) { throw ex; } finally { if (FS != null) { FS.Close(); } } if (Result == null) { Result = Encoding.Default; } return Result; }
关于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#全角和半角转换