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

在不手动指定特定编码的情况下,如何在. NET(C#)中将string转换为byte[]

我将对字符串进行加密。我可以加密而不进行转换,但是我仍然想知道为什么编码在这里起作用。

另外,为什么还要考虑编码?我不能简单地获取字符串存储在哪个字节中?为什么要依赖字符编码?

答案

与这里的答案相反, 如果不需要解释字节,则无需担心编码!

就像您提到的那样,您的目标很简单,就是“获取字符串存储在哪个字节中”
(并且,当然,能够从字节中重建字符串。)

对于这些目标,老实说,我明白为什么人们总是告诉您您需要编码。您当然不必为此担心编码。

只需这样做:

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;
}

// Do NOT use on arbitrary bytes; only use on GetBytes's output on the SAME system
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);
}

只要您的程序(或其他程序)不尝试以某种方式解释字节(您显然没有提到您打算这样做),那么这种方法就没有错!无须担心编码,只会使您的生活变得更加复杂。

这种方法的其他好处:

字符串是否包含无效字符并不重要,因为您仍然可以获取数据并重建原始字符串!

因为您只是在看 bytes ,所以它的编码和解码都一样

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

它取决于字符串的编码( 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 框架使用UTF-16表示字符串,因此,如果您只想获取. NET 使用的确切字节,请使用System.Text.Encoding.Unicode.GetBytes (...)

有关更多信息,请参见.NET Framework (MSDN) 中的字符编码

公认的答案非常非常复杂。为此使用包含的. 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);

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

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());

您需要考虑编码,因为 1 个字符可以由 1 个或多个字节(最多约 6 个)表示,并且不同的编码将对这些字节进行不同的处理。

乔尔对此发表了文章:

每个软件开发人员绝对,肯定必须绝对了解 Unicode 和字符集(无借口!)

这是一个受欢迎的问题。重要的是要了解问题作者的要求,并且该要求与最常见的要求有所不同。为了防止在不需要的地方滥用该代码,我已经在后面的第一个回答。

共同需求

每个字符串都有一个字符集和编码。当您将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 编码。 char 类型表示 UTF-16 代码单元,而字符串类型表示 UTF-16 代码单元的序列。

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

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

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

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

这并不能为我们提供所需的数据类型,但是Mehrdad 的答案显示了如何使用BlockCopy将此 Char 数组转换为 Byte 数组。但是,这会将字符串复制两次!而且,它也明确使用编码特定的代码:数据类型System.Char

获取字符串存储的实际字节的唯一方法是使用指针。 fixed语句允许获取值的地址。根据 C#规范:

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

为此,编译器使用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 所指出的,结果取决于计算机的字节序。但是问题作者对此并不关心。

其他人已经回答了问题的第一部分(如何获取字节):在System.Text.Encoding命名空间中查找。

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

答案分为两部分。

首先,字符串类内部使用的字节无关紧要 ,并且每当您假设它们出现时,就很可能引入错误。

如果您的程序完全位于. Net 世界之内,那么即使您正在通过网络发送数据,也不必担心完全为字符串获取字节数组。而是使用. Net 序列化来担心传输数据。您不必再担心实际的字节数了:序列化格式化程序会为您完成此操作。

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

我了解在这种情况下,您可能更愿意在可能的情况下使用字符串变量存储在内存中的实际字节,这样可能会节省创建字节流的工作。但是,我告诉您,与确保另一端可以理解您的输出,并确保您的编码必须是明确的相比,这并不重要。此外,如果您确实想匹配内部字节,则只需选择Unicode编码即可,从而节省了性能。

这使我进入第二部分... 选择Unicode编码告诉. Net 使用基础字节。您确实需要选择这种编码,因为当某些新型的 Unicode-Plus 发行时,.Net 运行时需要自由使用此更新的,更好的编码模型而不会破坏程序。但是,就目前而言(以及可预见的未来),仅选择 Unicode 编码即可满足您的需求。

理解必须将字符串重写为电线也很重要, 即使使用匹配的编码 ,这也至少涉及位模式的一些翻译。计算机需要考虑诸如 Big vs Little Endian,网络字节顺序,数据包化,会话信息等问题。

只是为了证明 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.GetBytesSystem.Text.Encoding.Unicode.GetBytes ,它们只会将高替代字符替换为值fffd

每当这个问题出现动静时,我仍在考虑一个序列化程序(无论是来自 Microsoft 还是来自第三方的组件),即使其中包含不成对的替代字符,该序列化程序也可以保留字符串;我不时在 Google 上搜索: 序列化未配对的代理字符. NET 。这不会让我失去任何睡眠,但是每时每刻都有一种烦人的感觉,有人在评论我的回答有缺陷,但是当涉及到不成对的代理角色时,他们的回答同样有缺陷。

哎呀,微软应该在BinaryFormatter使用System.Buffer.BlockCopy

谢谢!

试试看,更少的代码:

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

好吧,我已经阅读了所有答案,它们都是关于使用编码或关于删除未配对代理的序列化的答案。

例如,当字符串来自SQL Server 时 ,这是很糟糕的, SQL Server是从存储例如密码哈希的字节数组构建的。如果我们从其中删除任何内容,它将存储一个无效的哈希,并且如果我们要将其存储在 XML 中,我们希望将其保持不变(因为 XML 编写器会在发现的任何未配对代理中删除异常)。

因此,在这种情况下,我使用字节数组的Base64编码,但是,在 Internet 上,C#仅对此提供一种解决方案,并且其中包含错误,并且只是一种方法,因此,我已修复了该错误并写回程序。未来的 Google 员工,您好!

public static byte[] StringToBytes(string str)
{
    byte[] data = new byte[str.Length * 2];
    for (int i = 0; i < str.Length; ++i)
    {
        char ch = str[i];
        data[i * 2] = (byte)(ch & 0xFF);
        data[i * 2 + 1] = (byte)((ch & 0xFF00) >> 8);
    }

    return data;
}

public static string StringFromBytes(byte[] arr)
{
    char[] ch = new char[arr.Length / 2];
    for (int i = 0; i < ch.Length; ++i)
    {
        ch[i] = (char)((int)arr[i * 2] + (((int)arr[i * 2 + 1]) << 8));
    }
    return new String(ch);
}