首先请允许我重复造轮子
网络上很早就出现了给 Silverlight 使用的 GB2312 Encoding,但经过使用以后发现问题还挺多的:
- 不支持流操作
- 没有回退策略
- 只实现了解码没有实现编码
- 运行错误结果太多
- 支持的字符数目过少
对于上面出现的众多问题,特发布了一个专门针对 Silverlight 应用程序使用的 GB2312Encoding。顺便在这聊聊怎么编写一个 Encoding。
首先需要了解的知识:.NET Framework 中的字符编码,GB 2312,EUC(Extended Unix Code)
GB2312 是一种兼容 ASCII 编码的双字节字符编码,一个 Char 可以占用一个字节或两个字节。如果是占用两个字节,那么其第一个字节一定是 LEAD_BYTE。这样就可以区分一个 Char 是占用一个字节还是两个字节。在 GB2312 中,LEAD_BYTE 是 0x81~0xFE。
在实际中,Encoding 执行分为两种情况:直接调用和间接调用。
直接调用:即直接调用 Encoding 实体进行解码和编码操作。可以用如下的代码表示:
static string Decode(byte[] bytes)
{
Encoding encoding = Encoding.Default;
return encoding.GetString(bytes);
}
static byte[] Encode(string str)
{
Encoding encoding = Encoding.Default;
return encoding.GetBytes(str);
}
在 Encoding 内部最终调用的过程是,先调用 GetCharCount 和 GetByteCount 方法得知解码和编码后的数组长度,然后再创建一个相应大小和类型的数组传递给 GetChars 和 GetBytes 方法,最后得到最终结果。
这样,需要完成的重载是:
public override int GetCharCount(byte[] bytes, int index, int count)
public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
public override int GetByteCount(char[] chars, int index, int count)
public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex)
间接调用:即通过 StreamReader 和 StreamWriter 间接调用 Encoding。这个过程比直接调用的过程稍微复杂一点。以 StreamReader 读取 Stream 为例进行说明(StreamWriter 的过程类似)。
- 获取 encoding 的 Decoder
- 创建一个长度最小为 bufferSize = 1024 bytes 的 byteBuffer
- 获取到 _maxCharsPerBuffer = encoding.GetMaxCharCount(bufferSize)
- 创建一个长度为 _maxCharsPerBuffer 的 charBuffer
- 读取一个 byteBuffer 片段
- 把 byteBuffer 传递给 decoder.GetChars,并获取片段的解码结果
- 如果 stream 未读完,跳转到 5,否则进行下一步
- 把获取到的解码片段组合成最终结果
以上结论可以在如下地方找到证据:
private void System.IO.StreamReader.Init(Stream, Encoding, bool, int);
private int System.IO.StreamReader.ReadBuffer(char[], int, int, out bool);
public override string System.IO.StreamReader.ReadToEnd();
从这个过程可以看出两个问题:
- 对数据片段进行解码调用的是 decoder 而不是 encoding
- 在进行片段进行解码的过程中,如果出现了片段的最后一个字节为 LEAD_BYTE 的情况,如何保存这一位数据到下一次调用时使用
问题 1 的出现是为了能够解决问题 2
因为在构造 StreamReader 的时候会获取一个新的 Decoder 实例。注意,这个新的实例很重要。在执行完第 6 步发现最后一位是 LEAD_BYTE,这时可以把 LEAD_BYTE 保存在 decoder 中。因为 .net 中多个实例可以共用同一个 Encoding,所以在 Encoding 实例中不能够存储任何与编码解码有关的状态信息,同时每个 StreamReader 实例对应一个唯一的 Decoder 实例,所以 LEAD_BYTE 的状态是可以保证的。
这样,需要完成的重载是:
public override int GetMaxByteCount(int charCount)
public override int GetMaxCharCount(int byteCount)
public override Decoder GetDecoder()
回退策略:因为在编码和解码过程中并不是所有的字符在两个字符集中都是一一对应的,很可出现某个字符存在于这个字符集但不存在另外一个字符集的情况。所以必须有正确的回退机制处理此类问题。在 Fx 4.0 中支持“最佳回退”“替换回退”“异常回退”三种策略。但由于本文的目的是解决 Silverlight 中的 GB2312 支持问题,同时在 Silverlight 中并没有公开的回退策略。所以本文只选用了简化的“替换回退”策略:遇到不可处理的数据,一律替换成“?”(0x3F)。
最大化的支持字符
这里有一点需要澄清:本文所说的虽然是 GB2312 编码,但所使用的字符集已经远超出了 GB2312 的范围。为了达到最大化的支持,使用 Fx 4.0 的 gb2312 encoding 枚举了所有的结果保存在数据文件中。
本项目使用方法可以到 Codeplex 的 Wiki 页面找到。
结束语:
- 从零开始的做一个 Encoding 是非常复杂的,本文不可能面面俱到,还请谅解
- 此项目在发布前做了非常大量的测试,所用的测试样例覆盖了目前所能想到的方方面面,并且与 Fx 4.0 的 gb2312 encoding 进行了对比,所以大家可以放心的使用此项目
- 虽然说这是 GB2312 的 Encoding,但其中的实现方法是按照双字节字符集 (DBCS) 的编码方法实现的。所以,只需要替换掉相应的数据文件,就可以支持其他的 DBCS 编码
- 没有提到如何生成数据文件
- 此项目已经发布到 Codeplex: http://gb2312.codeplex.com/
- 最新源码下载地址: http://gb2312.codeplex.com/releases/view/75550#DownloadId=358387(2012/3/24)
- 此项目使用 Microsoft Reciprocal License (Ms-RL) 授权协议