如何将字节数组转换为十六进制字符串,反之亦然?


当前回答

对老年人来说最快的方法。。。想念你的指针

    static public byte[] HexStrToByteArray(string str)
    {
        byte[] res = new byte[(str.Length % 2 != 0 ? 0 : str.Length / 2)]; //check and allocate memory
        for (int i = 0, j = 0; j < res.Length; i += 2, j++) //convert loop
            res[j] = (byte)((str[i] % 32 + 9) % 25 * 16 + (str[i + 1] % 32 + 9) % 25);
        return res;
    }

其他回答

如果您希望比BitConverter更灵活,但不希望使用那些笨重的90年代风格的显式循环,那么您可以这样做:

String.Join(String.Empty, Array.ConvertAll(bytes, x => x.ToString("X2")));

或者,如果您使用的是.NET 4.0:

String.Concat(Array.ConvertAll(bytes, x => x.ToString("X2")));

(后者来自对原帖子的评论。)

这是一篇很棒的帖子。我喜欢瓦利德的解决方案。我还没有通过帕特里奇的测试,但似乎很快。我还需要反向过程,将十六进制字符串转换为字节数组,因此我将其作为Waleed解决方案的反向来编写。不确定它是否比托马拉克的原始解决方案更快。同样,我也没有通过帕特里奇的测试运行相反的过程。

private byte[] HexStringToByteArray(string hexString)
{
    int hexStringLength = hexString.Length;
    byte[] b = new byte[hexStringLength / 2];
    for (int i = 0; i < hexStringLength; i += 2)
    {
        int topChar = (hexString[i] > 0x40 ? hexString[i] - 0x37 : hexString[i] - 0x30) << 4;
        int bottomChar = hexString[i + 1] > 0x40 ? hexString[i + 1] - 0x37 : hexString[i + 1] - 0x30;
        b[i / 2] = Convert.ToByte(topChar + bottomChar);
    }
    return b;
}

有一个名为SoapHexBinary的类,它完全符合您的需要。

using System.Runtime.Remoting.Metadata.W3cXsd2001;

public static byte[] GetStringToBytes(string value)
{
    SoapHexBinary shb = SoapHexBinary.Parse(value);
    return shb.Value;
}

public static string GetBytesToString(byte[] value)
{
    SoapHexBinary shb = new SoapHexBinary(value);
    return shb.ToString();
}

在编写加密代码时,通常避免依赖数据的分支和表查找,以确保运行时不依赖于数据,因为依赖数据的计时可能会导致侧通道攻击。

它也很快。

static string ByteToHexBitFiddle(byte[] bytes)
{
    char[] c = new char[bytes.Length * 2];
    int b;
    for (int i = 0; i < bytes.Length; i++) {
        b = bytes[i] >> 4;
        c[i * 2] = (char)(55 + b + (((b-10)>>31)&-7));
        b = bytes[i] & 0xF;
        c[i * 2 + 1] = (char)(55 + b + (((b-10)>>31)&-7));
    }
    return new string(c);
}

Ph'nglui mglw'naph Cthulhu R'lieh wgah'nagl fhtagn公司


你们这些进入这里的人,放弃一切希望吧

一个奇怪的拨弄解释:

bytes[i]>>4提取字节的高位半字节bytes[i]&0xF提取字节的低位半字节b-10对于值b<10,为<0,将变为十进制数字对于值b>10,为>=0,这将成为从a到F的字母。在有符号32位整数上使用i>>31可以提取符号,这得益于符号扩展。当i<0时为-1,当i>=0时为0。结合2)和3),表明(b-10)>>31将是字母0,数字-1。看看字母的大小写,最后一个被加数变为0,b在10到15的范围内。我们希望将其映射到A(65)到F(70),这意味着添加55('A'-10)。看看数字的情况,我们希望调整最后一个被加数,使其将b从范围0到9映射到范围0(48)到9(57)。这意味着它需要变为-7('0'-55)。现在我们可以乘以7。但由于-1由所有位表示为1,因此我们可以改用&-7,因为(0&-7)==0和(-1&-7)==-7。

进一步考虑:

我没有使用第二个循环变量来索引c,因为测量表明从I计算它更便宜。正好使用i<bytes.Length作为循环的上限允许JITter消除对bytes[i]的边界检查,所以我选择了这个变量。将b设为int允许不必要的从和到字节的转换。

将byte[]转换为十六进制字符串-基准测试/性能分析

更新日期:2022-04-17

从.NET 5开始,您应该使用Convert.ToHexString(bytes[])!

using System;
string result = Convert.ToHexString(bytesToConvert);

关于此排行榜和基准

Thymine的比较似乎过时且不完整,尤其是在.NET 5及其Convert.ToHexString之后,所以我决定~~从字节到十六进制字符串的兔子洞~~创建一个新的、更新的比较,其中包含这两个问题的答案中的更多方法。

我使用的是BencharkDotNet,而不是定制的基准测试脚本,这有望使结果更准确。请记住,微观基准测试永远不能代表实际情况,您应该进行测试。

我在AMD Ryzen 5800H的Linux上运行了这些基准测试,内核为5.15.32,内存为2x8 GB DDR4@2133 MHz。请注意,完成整个基准测试可能需要很多时间——在我的机器上大约需要40分钟。

大写输出与小写输出

所有提到的方法(除非另有说明)都只关注UPPERCASE输出。这意味着输出将看起来像B33F69,而不是B33F69。

Convert.ToHexString的输出始终为大写。不过,值得庆幸的是,与ToLower()配合使用时,性能并没有显著下降,尽管这两种不安全的方法都会更快。

在某些方法中(尤其是具有位运算符魔力的方法),有效地将字符串小写可能是一个挑战,但在大多数情况下,将参数X2更改为X2或将映射中的字母从大写更改为小写就足够了。

排行榜

按平均值N=100排序。参考点是StringBuilderForEachByte方法。

Method (means are in nanoseconds) Mean N=10 Ratio N=10 Mean N=100 Ratio N=100 Mean N=500 Ratio N=500 Mean N=1k Ratio N=1k Mean N=10k Ratio N=10k Mean N=100k Ratio N=100k
StringBuilderAggregateBytesAppendFormat 364.92 1.48 3,680.00 1.74 18,928.33 1.86 38,362.94 1.87 380,994.74 1.72 42,618,861.57 1.62
StringBuilderForEachAppendFormat 309.59 1.26 3,203.11 1.52 20,775.07 2.04 41,398.07 2.02 426,839.96 1.93 37,220,750.15 1.41
StringJoinSelect 310.84 1.26 2,765.91 1.31 13,549.12 1.33 28,691.16 1.40 304,163.97 1.38 63,541,601.12 2.41
StringConcatSelect 301.34 1.22 2,733.64 1.29 14,449.53 1.42 29,174.83 1.42 307,196.94 1.39 32,877,994.95 1.25
StringJoinArrayConvertAll 279.21 1.13 2,608.71 1.23 13,305.96 1.30 27,207.12 1.32 295,589.61 1.34 62,950,871.38 2.39
StringBuilderAggregateBytesAppend 276.18 1.12 2,599.62 1.23 12,788.11 1.25 26,043.54 1.27 255,389.06 1.16 27,664,344.41 1.05
StringConcatArrayConvertAll 244.81 0.99 2,361.08 1.12 11,881.18 1.16 23,709.21 1.15 265,197.33 1.20 56,044,744.44 2.12
StringBuilderForEachByte 246.09 1.00 2,112.77 1.00 10,200.36 1.00 20,540.77 1.00 220,993.95 1.00 26,387,941.13 1.00
StringBuilderForEachBytePreAllocated 213.85 0.87 1,897.19 0.90 9,340.66 0.92 19,142.27 0.93 204,968.88 0.93 24,902,075.81 0.94
BitConverterReplace 140.09 0.57 1,207.74 0.57 6,170.46 0.60 12,438.23 0.61 145,022.35 0.66 17,719,082.72 0.67
LookupPerNibble 63.78 0.26 421.75 0.20 1,978.22 0.19 3,957.58 0.19 35,358.21 0.16 4,993,649.91 0.19
LookupAndShift 53.22 0.22 311.56 0.15 1,461.15 0.14 2,924.11 0.14 26,180.11 0.12 3,771,827.62 0.14
WhilePropertyLookup 41.83 0.17 308.59 0.15 1,473.10 0.14 2,925.66 0.14 28,440.28 0.13 5,060,341.10 0.19
LookupAndShiftAlphabetArray 37.06 0.15 290.96 0.14 1,387.01 0.14 3,087.86 0.15 29,883.54 0.14 5,136,607.61 0.19
ByteManipulationDecimal 35.29 0.14 251.69 0.12 1,180.38 0.12 2,347.56 0.11 22,731.55 0.10 4,645,593.05 0.18
ByteManipulationHexMultiply 35.45 0.14 235.22 0.11 1,342.50 0.13 2,661.25 0.13 25,810.54 0.12 7,833,116.68 0.30
ByteManipulationHexIncrement 36.43 0.15 234.31 0.11 1,345.38 0.13 2,737.89 0.13 26,413.92 0.12 7,820,224.57 0.30
WhileLocalLookup 42.03 0.17 223.59 0.11 1,016.93 0.10 1,979.24 0.10 19,360.07 0.09 4,150,234.71 0.16
LookupAndShiftAlphabetSpan 30.00 0.12 216.51 0.10 1,020.65 0.10 2,316.99 0.11 22,357.13 0.10 4,580,277.95 0.17
LookupAndShiftAlphabetSpanMultiply 29.04 0.12 207.38 0.10 985.94 0.10 2,259.29 0.11 22,287.12 0.10 4,563,518.13 0.17
LookupPerByte 32.45 0.13 205.84 0.10 951.30 0.09 1,906.27 0.09 18,311.03 0.08 3,908,692.66 0.15
LookupSpanPerByteSpan 25.69 0.10 184.29 0.09 863.79 0.08 2,035.55 0.10 19,448.30 0.09 4,086,961.29 0.15
LookupPerByteSpan 27.03 0.11 184.26 0.09 866.03 0.08 2,005.34 0.10 19,760.55 0.09 4,192,457.14 0.16
Lookup32SpanUnsafeDirect 16.90 0.07 99.20 0.05 436.66 0.04 895.23 0.04 8,266.69 0.04 1,506,058.05 0.06
Lookup32UnsafeDirect 16.51 0.07 98.64 0.05 436.49 0.04 878.28 0.04 8,278.18 0.04 1,753,655.67 0.07
ConvertToHexString 19.27 0.08 64.83 0.03 295.15 0.03 585.86 0.03 5,445.73 0.02 1,478,363.32 0.06
ConvertToHexString.ToLower() 45.66 - 175.16 - 787.86 - 1,516.65 - 13,939.71 - 2,620,046.76 -

结论

ConvertToHexString方法无疑是目前最快的方法,在我看来,如果您有选择的话,应该始终使用它-它既快速又干净。

using System;

string result = Convert.ToHexString(bytesToConvert);

如果没有,我决定在下面强调另外两种我认为值得使用的方法。我决定不强调不安全的方法,因为这样的代码可能不仅是不安全的,而且我合作过的大多数项目都不允许这样的代码。

值得一提

第一个是LookupPerByteSpan。从这个答案中可以看出,该代码与LookupPerBytebyCodesInChaos中的代码几乎相同。这是最快且不安全的基准方法。原始版本和本版本之间的区别在于,对更短的输入使用堆栈分配(最多512字节)。这使得该方法在这些输入上快10%左右,但在较大的输入上慢5%左右。由于我使用的大多数数据都比大数据短,所以我选择了这个。LookupSpanPerByteSpan也非常快,但与所有其他方法相比,其ReadOnlySpan<byte>映射的代码大小太大。

private static readonly uint[] Lookup32 = Enumerable.Range(0, 256).Select(i =>
{
    string s = i.ToString("X2");
    return s[0] + ((uint)s[1] << 16);
}).ToArray();

public string ToHexString(byte[] bytes)
{
    var result = bytes.Length * 2 <= 1024
        ? stackalloc char[bytes.Length * 2]
        : new char[bytes.Length * 2];

    for (int i = 0; i < bytes.Length; i++)
    {
        var val = Lookup32[bytes[i]];
        result[2 * i] = (char)val;
        result[2 * i + 1] = (char)(val >> 16);
    }

    return new string(result);
}

第二个是LookupAndShiftAlphabetSpanMultiply。首先,我想提一下,这是我的创作。然而,我相信这种方法不仅速度很快,而且很容易理解。速度来自于C#7.3中发生的变化,其中声明的ReadOnlyPan<byte>方法返回常量数组初始化-新字节{1,2,3,…}-被编译为程序的静态数据,因此省略了冗余内存。[来源]

private static ReadOnlySpan<byte> HexAlphabetSpan => new[]
{
    (byte)'0', (byte)'1', (byte)'2', (byte)'3',
    (byte)'4', (byte)'5', (byte)'6', (byte)'7',
    (byte)'8', (byte)'9', (byte)'A', (byte)'B',
    (byte)'C', (byte)'D', (byte)'E', (byte)'F'
};

public static string ToHexString(byte[] bytes)
{
    var res = bytes.Length * 2 <= 1024 ? stackalloc char[bytes.Length * 2] : new char[bytes.Length * 2];

    for (var i = 0; i < bytes.Length; ++i)
    {
        var j = i * 2;
        res[j] = (char)HexAlphabetSpan[bytes[i] >> 4];
        res[j + 1] = (char)HexAlphabetSpan[bytes[i] & 0xF];
    }

    return new string(res);
}

源代码

所有方法的源代码、基准和这个答案都可以在GitHub上的Gist中找到。