前言
有网友在交流群中询问,为什么字符串转成 UTF-8 字节数组后,值变了。0x80
变成了0xC2 0x80
("\x80"是字符串的16进制表示方式):.
其实,这是因为 string 存储的是 UTF-16 编码的字符:
而使用Encoding.UTF8
相当于将 UTF-16 编码转换成了 UTF-8 编码。
那它们到底有什么区别呢?
Unicode
Unicode 是一种国际编码标准,可用于各种平台以及各种语言和脚本。Unicode 标准定义了超过 110 万个码位。码位是一个整数值,通常使用语法U+xxxx
来表示码位,其中 xxxx 是十六进制编码的整数值。
常用的表示 Unicode 码位的编码方式有:
-
UTF-8,将每个 Unicode 码位表示为一至四个字节的序列。 -
UTF-16,将每个 Unicode 码位表示为一至两个 16 位整数的序列。
因此,上例的\x80
实际上表示的 Unicode 码位U+0080
。
UTF-8
UTF-8可以使用一个字节对应 Unicode 码位字符串,而通常 Unicode 码位是用U+xxxx
两个字节表示的,为了和Unicode 码位进行对应,它进行了如下设定:
-
字符 U+0000 到 U+007F 使用一个字节对应,这意味着 ASCII 字符 和 UTF-8 具有相同的编码
-
所有字符 > U+007F 都编码为几个字节的组合,每个字节都设置了最高有效位,这意味着原来一个字节会拆分成多个字节
xxx
代表二进制数值。最右边的 x 位是最低有效位。
因此,上例的\x80
转化成 UTF-8 编码,实际值位于第 2 行110xxxxx 10xxxxxx
。
Demo
根据上面的规则,我们可以自己验证上例的\x80
如何转化成 UTF-8 编码的:
var utf16 = Convert.ToByte("80", 16);
Console.WriteLine("UTF-16:"+Convert.ToString(utf16, 2));
byte low = (byte)(Convert.ToByte("10000000", 2) + (utf16 & Convert.ToByte("00111111", 2)));
byte high = (byte)(Convert.ToByte("11000000", 2) + (utf16 >> 6));
Console.WriteLine("UTF-8:" + Convert.ToString(high, 16) + " " + Convert.ToString(low, 16));
Console.WriteLine("UTF-8:" + Convert.ToString(high, 2) + " " + Convert.ToString(low, 2));
-
保留 6 位低有效位, 组合成 10xxxxxx
格式 -
保留 2 位高有效位, 组合成 110xxxxx
格式
结论
通过上图可以看出,\x80低有效位都是 0,组合成10xxxxxx
格式后,刚好也等于10000000
(0x80)。造成了误解,以为数据被修改,其实是改变了编码导致的。