C# Encoding.UTF8后0x80为什么变成了0xC2 0x80

前言

有网友在交流群中询问,为什么字符串转成 UTF-8 字节数组后,值变了。0x80变成了0xC2 0x80("\x80"是字符串的16进制表示方式):.

C# Encoding.UTF8后0x80为什么变成了0xC2 0x80

其实,这是因为 string 存储的是 UTF-16 编码的字符:

C# Encoding.UTF8后0x80为什么变成了0xC2 0x80

而使用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 具有相同的编码

C# Encoding.UTF8后0x80为什么变成了0xC2 0x80

  • 所有字符 > U+007F 都编码为几个字节的组合,每个字节都设置了最高有效位,这意味着原来一个字节会拆分成多个字节

C# Encoding.UTF8后0x80为什么变成了0xC2 0x80

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));
  1. 保留 6 位低有效位, 组合成10xxxxxx格式
  2. 保留 2 位高有效位, 组合成110xxxxx格式

C# Encoding.UTF8后0x80为什么变成了0xC2 0x80

结论

通过上图可以看出,\x80低有效位都是 0,组合成10xxxxxx格式后,刚好也等于10000000(0x80)。造成了误解,以为数据被修改,其实是改变了编码导致的。