题 如何在不手动指定编码的情况下在C#中获得字符串的一致字节表示?


我如何转换 string 到了 byte[] 在.NET(C#)中没有手动指定特定的编码?

我要加密字符串。我可以在不转换的情况下加密它,但我仍然想知道为什么编码在这里发挥作用。

另外,为什么要考虑编码?我不能简单地得到字符串存储的字节数吗?为什么依赖于字符编码?


1912
2018-01-23 13:39


起源


每个字符串都存储为一个字节数组对吗?为什么我不能简单地拥有那些字节? - Agnel Kurian
编码 是 什么将字符映射到字节。例如,在ASCII中,字母“A”映射到数字65.在不同的编码中,它可能不相同。在.NET框架中采用的高级字符串方法使得这在很大程度上无关紧要(在这种情况下除外)。 - Lucas Jones
扮演魔鬼的拥护者:如果你想得到内存中字符串的字节(因为.NET使用它们)并以某种方式操纵它们(即CRC32),并且永远不想将它解码回原始字符串......它不是直截了当为什么你关心编码或者你如何选择使用哪种编码。 - Greg
惊讶没有人给出了这个链接: joelonsoftware.com/articles/Unicode.html - Bevan
char不是字节,字节不是char。 char既是字体表的关键,也是词汇传统的关键。字符串是一系列字符。 (单词,段落,句子和标题也有自己的词汇传统,证明了他们自己的类型定义 - 但我离题了)。与整数,浮点数和其他所有内容一样,字符编码为字节。曾经有一段时间编码很简单:ASCII。然而,为了适应所有人类符号系统,一个字节的256个排列是不够的,并且设计编码以选择性地使用更多字节。 - George


答案:


与此处的答案相反,您无需担心编码问题 如果 字节不需要解释!

就像你提到的那样,你的目标就是 “获取字符串存储在的字节数”
(当然,能够从字节中重新构造字符串。)

对于那些目标,我老实说  明白为什么人们一直告诉你,你需要编码。你当然不需要担心编码。

只需这样做:

static byte[] GetBytes(string str)
{
    byte[] bytes = new byte[str.Length * sizeof(char)];
    System.Buffer.BlockCopy(str.ToCharArray(), 0, bytes, 0, bytes.Length);
    return bytes;
}

static string GetString(byte[] bytes)
{
    char[] chars = new char[bytes.Length / sizeof(char)];
    System.Buffer.BlockCopy(bytes, 0, chars, 0, bytes.Length);
    return new string(chars);
}

只要您的程序(或其他程序)不尝试  不知怎的,这些字节,你显然没有提到你打算做的,那就有了 没有 这种方法有误!担心编码只会让你的生活更加复杂,没有真正的理由。

这种方法的其他好处:

字符串包含无效字符无关紧要,因为无论如何您仍然可以获取数据并重建原始字符串!

它将被编码和解码,因为你是 只看字节

但是,如果您使用了特定的编码,那么编码/解码无效字符会给您带来麻烦。


1719
2018-04-30 07:44



关于这一个的丑陋是什么 GetString 和 GetBytes 需要在具有相同字节序的系统上执行才能工作。所以你不能用它来获得你想在其他地方变成字符串的字节。所以我很难想出一个我想要使用它的情况。 - CodesInChaos
@CodeInChaos:就像我说的那样,这一点的重点在于你是否想在相同类型的系统上使用它,具有相同的功能集。如果没有,那么你不应该使用它。 - Mehrdad
-1我保证某人(不理解字节与字符的人)想要将他们的字符串转换为字节数组,他们会谷歌并阅读这个答案,他们会做错事,因为几乎所有案例,编码 IS 相关。 - artbristol
@artbristol:如果他们不愿意阅读答案(或其他答案......),那么我很抱歉,那么我没有更好的方式与他们沟通。我通常选择回答OP而不是试图猜测其他人可能会对我的答案做些什么 - OP有权知道,并且因为有人可能会滥用刀并不意味着我们需要隐藏世界上所有的刀为了我们自己虽然如果你不同意那也没关系。 - Mehrdad
这个答案在很多层面上都是错误的,但最重要的是因为它“你不需要担心编码!”。这两个方法,GetBytes和GetString是多余的,因为它们只是重新实现了Encoding.Unicode.GetBytes()和Encoding.Unicode.GetString()已经完成的任务。声明“只要你的程序(或其他程序)不试图解释字节”也从根本上是有缺陷的,因为它们意味着字节应该被解释为Unicode。 - David


这取决于你的字符串的编码(ASCIIUTF-8,...)。

例如:

byte[] b1 = System.Text.Encoding.UTF8.GetBytes (myString);
byte[] b2 = System.Text.Encoding.ASCII.GetBytes (myString);

编码重要的一个小例子:

string pi = "\u03a0";
byte[] ascii = System.Text.Encoding.ASCII.GetBytes (pi);
byte[] utf8 = System.Text.Encoding.UTF8.GetBytes (pi);

Console.WriteLine (ascii.Length); //Will print 1
Console.WriteLine (utf8.Length); //Will print 2
Console.WriteLine (System.Text.Encoding.ASCII.GetString (ascii)); //Will print '?'

ASCII根本没有配备处理特殊字符。

在.NET内部,.NET框架使用 UTF-16 表示字符串,所以如果你只想获得.NET使用的确切字节,请使用 System.Text.Encoding.Unicode.GetBytes (...)

看到 .NET Framework中的字符编码 (MSDN)了解更多信息。


1052
2018-01-23 13:43



但是,为什么要考虑编码?为什么我不能简单地获取字节而不必查看正在使用的编码?即使它是必需的,String对象本身也不应该知道正在使用什么编码并简单地转储内存中的内容? - Agnel Kurian
.NET字符串始终编码为Unicode。所以使用System.Text.Encoding.Unicode.GetBytes();获取.NET用来表示字符的字节集。但是你为什么要这样呢?我建议使用UTF-8,特别是当大多数角色都在拉丁西部时。 - AnthonyWJones
另外:字符串内部使用的确切字节数 没关系 如果检索它们的系统不处理该编码或将其处理为错误的编码。如果它都在.Net中,为什么要转换为字节数组。否则,最好明确编码 - Joel Coehoorn
@Joel,请注意System.Text.Encoding.Default,因为它在运行的每台机器上可能不同。这就是为什么建议始终指定编码,例如UTF-8。 - Ash
除非您(或其他人)真正想要,否则您不需要编码 译 数据,而不是将其视为通用的“字节块”。对于压缩,加密等问题,担心编码是没有意义的。看到 我的答案 为了这样做而不必担心编码。 (当你不这样做时,我可能会给你一个-1来说你需要担心编码,但我今天感觉不太特别。:P) - Mehrdad


接受的答案非常非常复杂。使用包含的.NET类:

const string data = "A string with international characters: Norwegian: ÆØÅæøå, Chinese: 喂 谢谢";
var bytes = System.Text.Encoding.UTF8.GetBytes(data);
var decoded = System.Text.Encoding.UTF8.GetString(bytes);

如果你不需要,不要重新发明轮子......


245
2018-04-30 07:26



接受的答案不仅非常复杂,而且还是灾难的一个方法。 - Konamiman
如果接受的答案发生变化,出于记录目的,在当前时间和日期是Mehrdad的答案。希望OP能够重新审视并接受更好的解决方案。 - Thomas Eding
原则上很好,但编码应该是 System.Text.Encoding.Unicode 相当于Mehrdad的答案。 - Jodrell
这个问题自原始答案以来已被编辑了很多次,所以,也许我的答案有点过时了。我从来没有打算给出与Mehrdad的答案相当的exace,但是给出一种合理的方式来做到这一点。但是,你可能是对的。但是,原始问题中的短语“获取字符串已存储的字节数”是非常不准确的。存储,在哪里?在记忆中?在磁盘上?如果在记忆中, System.Text.Encoding.Unicode.GetBytes 可能会更准确。 - Erik A. Brandstadmoen
@AMissico,你的建议是错误的,除非你确定你的字符串与你的系统默认编码兼容(在你的系统默认遗留字符集中只包含ASCII字符的字符串)。但OP没有说明这一点。 - Frédéric


BinaryFormatter bf = new BinaryFormatter();
byte[] bytes;
MemoryStream ms = new MemoryStream();

string orig = "喂 Hello 谢谢 Thank You";
bf.Serialize(ms, orig);
ms.Seek(0, 0);
bytes = ms.ToArray();

MessageBox.Show("Original bytes Length: " + bytes.Length.ToString());

MessageBox.Show("Original string Length: " + orig.Length.ToString());

for (int i = 0; i < bytes.Length; ++i) bytes[i] ^= 168; // pseudo encrypt
for (int i = 0; i < bytes.Length; ++i) bytes[i] ^= 168; // pseudo decrypt

BinaryFormatter bfx = new BinaryFormatter();
MemoryStream msx = new MemoryStream();            
msx.Write(bytes, 0, bytes.Length);
msx.Seek(0, 0);
string sx = (string)bfx.Deserialize(msx);

MessageBox.Show("Still intact :" + sx);

MessageBox.Show("Deserialize string Length(still intact): " 
    + sx.Length.ToString());

BinaryFormatter bfy = new BinaryFormatter();
MemoryStream msy = new MemoryStream();
bfy.Serialize(msy, sx);
msy.Seek(0, 0);
byte[] bytesy = msy.ToArray();

MessageBox.Show("Deserialize bytes Length(still intact): " 
   + bytesy.Length.ToString());

105
2018-01-23 16:36



您可以对所有这些操作使用相同的BinaryFormatter实例 - Joel Coehoorn
很有意思。显然它会丢弃任何高代理Unicode字符。请参阅文档 [BinaryFormatter的] - John Robertson
@ ErikA.Brandstadmoen在这里查看我的测试: stackoverflow.com/a/10384024 - Michael Buen


您需要考虑编码,因为1个字符可以用1表示 或者更多 字节(最多约6个),不同的编码将以不同的方式处理这些字节。

乔尔有一个帖子:

绝对最低每个软件开发人员绝对必须知道Unicode和字符集(没有借口!)


79
2018-01-23 14:03



“1个字符可以用1个或更多字节表示”我同意。我只想要那些字节而不管字符串的编码方式。字符串可以存储在内存中的唯一方法是以字节为单位。偶数字符存储为1个或更多字节。我只是想抓住他们的字节。 - Agnel Kurian
除非您(或其他人)真正想要,否则您不需要编码 译 数据,而不是将其视为通用的“字节块”。对于压缩,加密等问题,担心编码是没有意义的。看到 我的答案为了这样做而不必担心编码。 - Mehrdad
@Mehrdad - 总的来说,但是我最初回答时提出的原始问题没有告诫OP在转换它们后会发生什么样的字节,并且对于未来的搜索者来说,周围的信息是相关的 - 这是覆盖 乔尔的回答 非常好 - 当你在答案中陈述时:如果你坚持在.NET世界中,并使用你的方法转换成/从中,你很高兴。一旦你超越它,编码就会很重要。 - Zhaph - Ben Duguid


这是一个很受欢迎的问题。重要的是要理解作者提出的问题,并且它与最常见的需求不同。为了防止在不需要的地方滥用代码,我先回答了后面的问题。

共同需要

每个字符串都有一个字符集和编码。当你转换一个 System.String 对象到数组 System.Byte 你仍然有一个字符集和编码。 对于大多数用法,您可以知道所需的字符集和编码,而.NET使“转换时复制”变得简单。 只需选择合适的 Encoding 类。

// using System.Text;
Encoding.UTF8.GetBytes(".NET String to byte array")

转换可能需要处理目标字符集或编码不支持源中的字符的情况。你有一些选择:例外,替换或跳过。默认策略是替换“?”。

// using System.Text;
var text = Encoding.ASCII.GetString(Encoding.ASCII.GetBytes("You win €100")); 
                                                      // -> "You win ?100"

显然,转换不一定是无损的!

注意:对于 System.String 源字符集是Unicode。

唯一令人困惑的是,.NET使用字符集的名称作为该字符集的一个特定编码的名称。 Encoding.Unicode 应该叫 Encoding.UTF16

对于大多数用法来说都是如此。如果这就是你需要的,请在这里停止阅读。 看到有趣的 Joel Spolsky的文章 如果你不明白编码是什么。

具体需要

现在,问题作者问:“每个字符串都存储为一个字节数组,对吧?为什么我不能简单地拥有这些字节?”

他不想要任何转换。

来自 C#规范

C#中的字符和字符串处理使用Unicode编码。这个角色   type表示UTF-16代码单元,字符串类型表示a   UTF-16代码单元序列。

所以,我们知道如果我们要求空转换(即从UTF-16到UTF-16),我们将获得所需的结果:

Encoding.Unicode.GetBytes(".NET String to byte array")

但为了避免提及编码,我们必须采取另一种方式。如果中间数据类型是可接受的,则有一个概念性的快捷方式:

".NET String to byte array".ToCharArray()

这并没有让我们得到所需的数据类型 Mehrdad的回答 演示了如何使用这个Char数组转换为Byte数组 BlockCopy。但是,这会复制两次字符串!而且,它也明确使用特定于编码的代码:数据类型 System.Char

获取存储String的实际字节的唯一方法是使用指针。该 fixed 声明允许获取值的地址。来自C#规范:

[For]一个string类型的表达式,...初始化程序计算   字符串中第一个字符的地址。

为此,编译器将代码跳过写入字符串对象的其他部分 RuntimeHelpers.OffsetToStringData。因此,要获取原始字节,只需创建指向字符串的指针并复制所需的字节数。

// using System.Runtime.InteropServices
unsafe byte[] GetRawBytes(String s)
{
    if (s == null) return null;
    var codeunitCount = s.Length;
    /* We know that String is a sequence of UTF-16 codeunits 
       and such codeunits are 2 bytes */
    var byteCount = codeunitCount * 2; 
    var bytes = new byte[byteCount];
    fixed(void* pRaw = s)
    {
        Marshal.Copy((IntPtr)pRaw, bytes, 0, byteCount);
    }
    return bytes;
}

正如@CodesInChaos指出的那样,结果取决于机器的字节顺序。但问题的作者并不关心这一点。


76
2017-12-02 04:43



一般来说,设置不正确 byteCount 到字符串长度的两倍。对于基本多语言平面之外的Unicode代码点,每个字符将有两个16位代码单元。 - Jan Hettich
@Jan这是正确的,但字符串长度已经给出了代码单元的数量(不是代码点)。 - Tom Blodget
感谢您指出了这一点!来自MSDN:“The Length 财产[of String]返回的数量 Char 这个实例中的对象,而不是Unicode字符的数量。“因此,您的示例代码是正确的。 - Jan Hettich
@TomBlodget:有趣的是,如果需要实例 Globalization.SortKey,提取 KeyData,并将每个结果字节打包成一个 String [每个字符两个字节, MSB优先],打电话 String.CompareOrdinal 结果字符串将比调用快得多 SortKey.Compare 在实例上 SortKey,甚至打电话 memcmp 在那些情况下。鉴于此,我想知道为什么 KeyData 返回一个 Byte[] 而不是一个 String? - supercat
@TomBlodget:你不需要 fixed 要么 unsafe 代码,你也可以做 var gch = GCHandle.Alloc("foo", GCHandleType.Pinned); var arr = new byte[sizeof(char) * ((string)gch.Target).Length]; Marshal.Copy(gch.AddrOfPinnedObject(), arr, 0, arr.Length); gch.Free(); - Mehrdad


只是为了证明Mehrdrad的声音 回答 作品,他的方法甚至可以坚持下去 不成对的代理人物(其中许多人反对我的答案,但每个人都同样有罪,例如 System.Text.Encoding.UTF8.GetBytesSystem.Text.Encoding.Unicode.GetBytes;那些编码方法不能保留高代理字符 d800例如,那些只是用价值取代高代理字符 fffd ):

using System;

class Program
{     
    static void Main(string[] args)
    {
        string t = "爱虫";            
        string s = "Test\ud800Test"; 

        byte[] dumpToBytes = GetBytes(s);
        string getItBack = GetString(dumpToBytes);

        foreach (char item in getItBack)
        {
            Console.WriteLine("{0} {1}", item, ((ushort)item).ToString("x"));
        }    
    }

    static byte[] GetBytes(string str)
    {
        byte[] bytes = new byte[str.Length * sizeof(char)];
        System.Buffer.BlockCopy(str.ToCharArray(), 0, bytes, 0, bytes.Length);
        return bytes;
    }

    static string GetString(byte[] bytes)
    {
        char[] chars = new char[bytes.Length / sizeof(char)];
        System.Buffer.BlockCopy(bytes, 0, chars, 0, bytes.Length);
        return new string(chars);
    }        
}

输出:

T 54
e 65
s 73
t 74
? d800
T 54
e 65
s 73
t 74

尝试一下 System.Text.Encoding.UTF8.GetBytes 要么 System.Text.Encoding.Unicode.GetBytes,他们只会用价值取代高代理人物 FFFD

每当这个问题发生变化时,我仍然会想到一个序列化器(无论是来自微软还是来自第三方组件),它可以持久化字符串,即使它包含不成对的代理字符;我不时地谷歌这个: 序列化不成对的代理人物.NET。这并没有让我失去任何睡眠,但是当有人评论我的答案它有缺陷时,它会有点烦人,但是当涉及到不成对的代理人物时,他们的答案同样存在缺陷。

Darn,微软应该刚刚使用过 System.Buffer.BlockCopy 在它的 BinaryFormatter ツ

谢谢!


35
2017-07-25 22:52



代理人不必成对出现以形成有效的代码点吗?如果是这种情况,我可以理解为什么数据会被破坏。 - dtanders
@dtanders是的,这也是我的想法,他们必须成对出现,如果你故意将它们放在字符串上并使它们不成对,那么不成对的代理字符就会发生。我不知道的是为什么其他开发人员继续强调我们应该使用编码感知方法,因为他们认为是序列化方法(我的答案,这是一个超过3年的公认答案)并没有保持未成对的代理人物完好无损。但他们忘了检查他们的编码感知解决方案是否也没有保留不成对的代理人角色,具有讽刺意味的ツ - Michael Buen
如果有一个使用的序列化库 System.Buffer.BlockCopy 在内部,所有编码倡导者的论点都没有实际意义 - Michael Buen
您的测试的问题是您创建了一个无效的字符串。 “在UTF-16中,它们必须始终成对出现,作为高代理,然后是低代理,因此使用32位来表示一个代码点。”。如果您使用/ uDC00关注/ uD800,那么它在所有unicode格式中都能正常工作。重要的是要注意这是一个字符串,而不是char数组,因此某些限制是有意义的。此外,即使没有UTF7中的/ uDC00,它也能正常工作。 - Trisped
@dtanders:A System.String 是一个不可变的序列 Char; .NET一直允许使用 String 要从任何构造的对象 Char[] 并将其内容导出到 Char[] 包含相同的值,即使是原始值 Char[] 包含未成对的代理人。 - supercat


试试这个,少了很多代码:

System.Text.Encoding.UTF8.GetBytes("TEST String");

34
2018-01-23 15:54



然后尝试这个 System.Text.Encoding.UTF8.GetBytes("Árvíztűrő tükörfúrógép);,哭!它会起作用,但是 System.Text.Encoding.UTF8.GetBytes("Árvíztűrő tükörfúrógép").Length != System.Text.Encoding.UTF8.GetBytes("Arvizturo tukorfurogep").Length 而 "Árvíztűrő tükörfúrógép".Length == "Arvizturo tukorfurogep".Length - mg30rg
@ mg30rg:为什么你认为你的例子很奇怪?当然,在可变宽度编码中,并非所有字符都具有相同的字节长度。它出什么问题了? - Vlad


你的问题的第一部分(如何获得字节)已经被其他人回答了:看看 System.Text.Encoding 命名空间。

我将解决您的后续问题:为什么需要选择编码?为什么你不能从字符串类本身那里得到它?

答案分为两部分。

首先,字符串类在内部使用的字节 没关系,无论何时你认为他们你可能会引入一个错误。

如果您的程序完全在.Net世界中,那么即使您通过网络发送数据,也不必担心为字符串获取字节数组。相反,使用.Net Serialization来担心传输数据。您不再担心实际的字节:序列化格式化程序会为您执行此操作。

另一方面,如果您将这些字节发送到某个您无法保证的字节会从.Net序列化流中提取数据怎么办?在这种情况下,你肯定需要担心编码,因为显然这个外部系统在乎。同样,字符串使用的内部字节无关紧要:您需要选择一个编码,以便您可以在接收端明确表示此编码,即使它与.Net内部使用的编码相同。

我知道在这种情况下,您可能更愿意在可能的情况下使用字符串变量存储在字节变量中的实际字节,并认为它可能会节省一些创建字节流的工作。但是,我把它告诉你,与确保你的输出在另一端被理解相比并不重要,并保证你 必须 明确你的编码。另外,如果你真的想匹配你的内部字节,你可以选择 Unicode 编码,并节省性能。

这让我想到了第二部分......选择了 Unicode 编码  告诉.Net使用底层字节。你确实需要选择这种编码,因为当出现一些新奇的Unicode-Plus时,.Net运行时需要免费使用这种更新,更好的编码模型而不会破坏你的程序。但是,目前(以及可预见的未来),只需选择Unicode编码即可获得所需内容。

理解你的字符串必须重写为连线也很重要,这至少涉及一些位模式的转换 即使你使用匹配的编码。计算机需要考虑Big vs Little Endian,网络字节顺序,打包,会话信息等。


34
2018-03-10 08:57



在.NET中,您必须获得字符串的字节数组。许多.NET Cryptrography类包含接受字节数组或流的ComputeHash()等方法。您别无选择,只能先将字符串转换为字节数组(选择编码),然后将其包装在流中。但是,只要你选择一个编码(即UTF8),它就会有一个问题。 - Ash