如何在.NET(C#)中将字符串转换为字节[]而不手动指定特定编码?

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

此外,为什么还要考虑编码?我不能简单地获取字符串存储的字节数吗?为什么依赖字符编码?


当前回答

这是一个流行的问题。重要的是要了解作者所问的问题,以及它与最常见的需求不同。为了防止在不需要的地方滥用代码,我首先回答了后者。

共同需求

每个字符串都有一个字符集和编码。将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使用字符集的名称作为该字符集的一个特定编码的名称。编码。Unicode应称为Encoding.UTF16。

这就是大多数用法。如果这正是你所需要的,请停止阅读这里。如果您不了解编码是什么,请参阅有趣的Joel Spolsky文章。

特定需求

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

他不想改变信仰。

根据C#规范:

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

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

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

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

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

这并不能为我们提供所需的数据类型,但Mehrad的答案显示了如何使用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 code units 
       and such code units 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;
}

正如@CodesInChaus所指出的,结果取决于机器的端序。但问题的作者并不关心这一点。

其他回答

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

我将回答您的后续问题:为什么需要选择编码?为什么不能从字符串类本身获得这些信息?

答案分为两部分。

首先,字符串类内部使用的字节并不重要,只要假设它们存在,就可能引入错误。

如果您的程序完全在.Net世界中,那么您根本不需要担心获取字符串的字节数组,即使您正在通过网络发送数据。相反,请使用.Net Serialization来担心数据传输问题。您不再担心实际的字节数:Serialization格式化程序会为您提供。

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

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

这让我进入第二部分。。。选择Unicode编码就是告诉.Net使用底层字节。您确实需要选择这种编码,因为当一些新的Unicode Plus问世时,.Net运行时需要自由使用这种更新、更好的编码模型,而不会破坏您的程序。但是,目前(以及可预见的未来),只要选择Unicode编码就可以满足您的需要。

理解字符串必须重写为线也是很重要的,即使使用匹配的编码,也至少需要对位模式进行一些转换。计算机需要考虑诸如Big vs Little Endian、网络字节顺序、分组化、会话信息等。

bytes[] buffer = UnicodeEncoding.UTF8.GetBytes(string something); //for converting to UTF then get its bytes

bytes[] buffer = ASCIIEncoding.ASCII.GetBytes(string something); //for converting to ascii then get its bytes

以下是我的String-to-Byte[]转换的不安全实现:

public static unsafe Byte[] GetBytes(String s)
{
    Int32 length = s.Length * sizeof(Char);
    Byte[] bytes = new Byte[length];

    fixed (Char* pInput = s)
    fixed (Byte* pBytes = bytes)
    {
        Byte* source = (Byte*)pInput;
        Byte* destination = pBytes;

        if (length >= 16)
        {
            do
            {
                *((Int64*)destination) = *((Int64*)source);
                *((Int64*)(destination + 8)) = *((Int64*)(source + 8));

                source += 16;
                destination += 16;
            }
            while ((length -= 16) >= 16);
        }

        if (length > 0)
        {
            if ((length & 8) != 0)
            {
                *((Int64*)destination) = *((Int64*)source);

                source += 8;
                destination += 8;
            }

            if ((length & 4) != 0)
            {
                *((Int32*)destination) = *((Int32*)source);

                source += 4;
                destination += 4;
            }

            if ((length & 2) != 0)
            {
                *((Int16*)destination) = *((Int16*)source);

                source += 2;
                destination += 2;
            }

            if ((length & 1) != 0)
            {
                ++source;
                ++destination;

                destination[0] = source[0];
            }
        }
    }

    return bytes;
}

它比公认的anwser要快得多,即使没有它那么优雅。以下是我在10000000次迭代中的秒表基准:

[Second String: Length 20]
Buffer.BlockCopy: 746ms
Unsafe: 557ms

[Second String: Length 50]
Buffer.BlockCopy: 861ms
Unsafe: 753ms

[Third String: Length 100]
Buffer.BlockCopy: 1250ms
Unsafe: 1063ms

为了使用它,您必须在项目构建财产中勾选“允许不安全代码”。根据.NET Framework 3.5,此方法也可以用作字符串扩展:

public static unsafe class StringExtensions
{
    public static Byte[] ToByteArray(this String s)
    {
        // Method Code
    }
}
byte[] strToByteArray(string str)
{
    System.Text.ASCIIEncoding enc = new System.Text.ASCIIEncoding();
    return enc.GetBytes(str);
}

如何在.NET(C#)中将字符串转换为字节[]而不手动指定特定编码?

NET中的字符串将文本表示为UTF-16代码单元的序列,因此字节已经在UTF-16中的内存中编码。

Mehrad的回答

您可以使用Mehrad的答案,但它实际上使用了编码,因为字符是UTF-16。它调用ToCharArray,通过查看源代码创建一个char[]并将内存直接复制到它。然后,它将数据复制到同样分配的字节数组中。因此,在后台,它复制了两次底层字节,并分配了一个在调用后不使用的字符数组。

Tom Blodget的回答

Tom Blodget的答案比Mehrad快20-30%,因为它跳过了分配一个字符数组并将字节复制到其中的中间步骤,但它需要使用/safe选项进行编译。如果你绝对不想使用编码,我认为这是正确的方法。如果将加密登录放在固定块中,甚至不需要分配单独的字节数组并将字节复制到其中。

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

因为这是正确的方法。字符串是一个抽象。

如果“字符串”包含无效字符,使用编码可能会给您带来麻烦,但这不应该发生。如果将数据输入到字符串中包含无效字符,则说明操作错误。您可能应该首先使用字节数组或Base64编码。

如果使用System.Text.Encoding.Unicode,代码将更具弹性。您不必担心运行代码的系统的端序。您不必担忧下一版本的CLR是否会使用不同的内部字符编码。

我认为问题不在于你为什么要担心编码,而是你为什么要忽略它而使用其他东西。编码旨在表示字节序列中字符串的抽象。System.Text.Encoding.Unicode将为您提供一个小端字节顺序编码,并将在现在和将来的每个系统上执行相同的编码。