为什么`Encoding.UTF8.GetBytes(Encoding.UTF8.GetString(x))==x`不成立?

7
在.NET中,为什么不是真的:
Encoding.UTF8.GetBytes(Encoding.UTF8.GetString(x))

返回任意字节数组x的原始字节数组?

在回答另一个问题时提到,但回答者没有解释为什么。


你链接的答案讨论的是ASCII,而不是UTF-8。 - svick
1
你能使用 == 比较字节数组吗?那可能只会比较它们的引用,你可能需要循环比较数组中每个元素是否相等。 - Matthew
@Matthew,这个答案的要点似乎是编码可能会有所不同。而且是的,示例代码是有缺陷/反向的。 - sehe
解释很简单:并不是每个任意的字节序列都是有效的UTF-8编码。将非UTF-8编码的内容解释为UTF-8,会产生意想不到的结果。因此,将UTF-8编码的字符串转换回字节缓冲区,并不一定会产生原始序列。真正的解决方案是使用可以编码任意字节序列的编码(如Base64)。这条评论中关于UTF-8的所有说法也适用于ASCII(链接问题正在使用),核心问题是相同的。 - IInspectable
4个回答

3
首先,如 watbywbarif 所提到的,你不应该使用 == 来比较序列,这是不起作用的。
但是,即使你正确比较了数组(例如通过使用 SequenceEquals() 或仅仅查看它们),它们并不总是相同的。这种情况可能发生在x 是一个无效的 UTF-8 编码字符串时。
例如,0xFF 的 1 字节序列不是有效的 UTF-8。那么 Encoding.UTF8.GetString(new byte[] { 0xFF }) 返回什么?它是 �,U+FFFD,替换字符。当然,如果你对其调用 Encoding.UTF8.GetBytes(),它也不会返回 0xFF

1
我之前不知道 SequenceEqual 扩展方法,非常有用。 - PyreneesJim

2
另一个角度来看,Encoding 类被设计成能够往返转换数据,但是它们被设计成往返转换的数据是 char 数据,编码为 byte,而不是相反的方向。这意味着,在所涉及的 Encoding 的能力范围内,每个 char 值都有一个对应的编码值(1个或多个)在 byte 值中,可以完全还原成相同的 char 值。(值得注意的是,并非所有的 Encoding 都能够支持所有可能的 char 值进行此操作 - 例如,Encoding.ASCII 只能支持 [0, 128) 范围内的 char 值。)
因此,如果您从字符数据开始,并且需要一种以字节为单位(例如磁盘上的文件或网络流)存储或发送数据的方式,则 Encoding 是将 char 数据转换为 byte 数据,然后在另一端再次转换回来的绝佳方式。 (如果您想支持所有可能的字符串,则需要使用基于Unicode的其中一种 Encoding ,例如 Encoding.Unicode Encoding.UTF8 。)
那么,如果您从一堆 byte 开始怎么办?好吧,根据所涉及的编码,您正在使用的 byte 可能实际上不是 Encoding 曾经输出的序列。 您需要将 Encoding.GetBytes 视为编码操作,将 Encoding.GetChars / Encoding.GetString 视为解码操作,因此您从任意字节数组开始并尝试解码它们。
作为类比,考虑图像的JPEG文件格式。它具有类似的编码解码方式,但在这种情况下,解码后的数据不是字符串,而是图像。因此,如果您拿一串任意的字节,将其解码为JPEG图像的可能性有多大呢?显然,答案非常非常渺茫。更有可能的是,您的字节将沿着解码器中的某个路径走下去,它会说,“哇,我没想到那个字节会出现在那个字节后面”,并且会尽力处理数据,假设它是一个损坏的有效JPEG文件。

当您将任意字节数组转换为字符串时,会发生完全相同的事情。 UTF-8编码有关于如何对128及以上的char值进行编码的特定规则,其中之一规定,您只会在匹配类似110xxxxx1110xxxx11110xxx的模式之后看到与位模式10xxxxxx匹配的字节,这些模式“介绍”了多字节序列(多个表示单个charbyte)。因此,如果您的数据包含与预期的“介绍者”之一不匹配的模式10xxxxxx的字节,则编码器只能假定数据已被某种方式损坏。它会插入一个字符,表明:“编码数据出现了严重问题。我已尽力。这就是出错的地方。” 设计Unicode的人预见了这种情况,并创建了一个具有这种精确含义的字符:替换字符

如果您尝试在char字符串中往返传输byte,并且遇到这种情况,则有问题的byte的实际值会丢失,而是插入替换字符。当您尝试将string转换回byte数组时,它会对替换字符进行编码,而不是原始数据。原始数据会丢失。
您需要寻找的是一种可以反向工作的编码和解码关系。编码是将char数据转换为byte数据的方法。如果您想将byte数据转换为char数据,则需要针对该特定目的设计的编码。幸运的是,这些编码已经存在。维基百科上有一个相当全面的列表供您选择。 :-)
在.NET Framework中,最简单和最易于访问的选项是MIME Base-64编码,通过Convert.ToBase64StringConvert.FromBase64String公开。

1

字符编码(特别是UTF8)可能对于相同的代码点有不同的形式。

因此,当您转换为字符串并返回时,实际字节可能表示一个不同(规范化)的形式

另请参见String.Normalize(NormalizationForm.System.Text.NormalizationForm.FormD)

另请参见:

Some Unicode sequences are considered equivalent because they represent the same character. For example, the following are considered equivalent because any of these can be used to represent "ắ":

"\u1EAF" 
"\u0103\u0301" 
"\u0061\u0306\u0301" 

However, ordinal, that is, binary, comparisons consider these sequences different because they contain different Unicode code values. Before performing ordinal comparisons, applications must normalize these strings to decompose them into their basic components.

该页面附带了一个漂亮的示例,展示了哪些编码始终是规范化的。


为什么这两种方法中的任何一种会改变字符串的形式? - svick
@svick不要问我。我没有检查文档以确保它不会发生,但是。 - sehe
我认为这不会发生。那是因为这些不同的形式不是各种编码的属性,而是Unicode本身的属性。因此,一个字符可以被表示为不同的代码点序列。但是,当使用特定的编码时,一个代码点序列只能以一种方式表示为字节序列。 - svick
1
@svick 老实说,我不知道为什么我的答案被接受了;我认为你的例子更有说服力(那将是一个问题;我的建议只是可能但很可能不会)。我猜我得到了标记,因为提供了背景信息的链接... - sehe

1
这是因为“==”不会比较数组的每个元素。它与Encoding.UTF8没有任何关系。 请检查:
var a = new byte[] { 1 };
var b = new byte[] { 1 };
bool res = a == b;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接