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


当前回答

.NET 5已添加Convert.ToHexString方法。

对于使用旧版本.NET的用户

internal static class ByteArrayExtensions
{
    
    public static string ToHexString(this byte[] bytes, Casing casing = Casing.Upper)
    {
        Span<char> result = stackalloc char[0];
        if (bytes.Length > 16)
        {
            var array = new char[bytes.Length * 2];
            result = array.AsSpan();
        }
        else
        {
            result = stackalloc char[bytes.Length * 2];
        }

        int pos = 0;
        foreach (byte b in bytes)
        {
            ToCharsBuffer(b, result, pos, casing);
            pos += 2;
        }

        return result.ToString();
    }

    private static void ToCharsBuffer(byte value, Span<char> buffer, int startingIndex = 0, Casing casing = Casing.Upper)
    {
        uint difference = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU) - 0x8989U;
        uint packedResult = ((((uint)(-(int)difference) & 0x7070U) >> 4) + difference + 0xB9B9U) | (uint)casing;

        buffer[startingIndex + 1] = (char)(packedResult & 0xFF);
        buffer[startingIndex] = (char)(packedResult >> 8);
    }
}

public enum Casing : uint
{
    // Output [ '0' .. '9' ] and [ 'A' .. 'F' ].
    Upper = 0,

    // Output [ '0' .. '9' ] and [ 'a' .. 'f' ].
    Lower = 0x2020U,
}

改编自.NET存储库https://github.com/dotnet/runtime/blob/v5.0.3/src/libraries/System.Private.CoreLib/src/System/Convert.cshttps://github.com/dotnet/runtime/blob/v5.0.3/src/libraries/Common/src/System/HexConverter.cs

其他回答

将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中找到。

对于插入SQL字符串(如果不使用命令参数):

public static String ByteArrayToSQLHexString(byte[] Source)
{
    return = "0x" + BitConverter.ToString(Source).Replace("-", "");
}

如果性能很重要,这里有一个优化的解决方案:

    static readonly char[] _hexDigits = "0123456789abcdef".ToCharArray();
    public static string ToHexString(this byte[] bytes)
    {
        char[] digits = new char[bytes.Length * 2];
        for (int i = 0; i < bytes.Length; i++)
        {
            int d1, d2;
            d1 = Math.DivRem(bytes[i], 16, out d2);
            digits[2 * i] = _hexDigits[d1];
            digits[2 * i + 1] = _hexDigits[d2];
        }
        return new string(digits);
    }

它比BitConverter.ToString快2.5倍,比BitConverter.ToString+删除“-”字符快7倍。

这是一篇很棒的帖子。我喜欢瓦利德的解决方案。我还没有通过帕特里奇的测试,但似乎很快。我还需要反向过程,将十六进制字符串转换为字节数组,因此我将其作为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;
}
static string ByteArrayToHexViaLookupPerByte2(byte[] bytes)
{                
        var result3 = new uint[bytes.Length];
        for (int i = 0; i < bytes.Length; i++)
                result3[i] = _Lookup32[bytes[i]];
        var handle = GCHandle.Alloc(result3, GCHandleType.Pinned);
        try
        {
                var result = Marshal.PtrToStringUni(handle.AddrOfPinnedObject(), bytes.Length * 2);
                return result;
        }
        finally
        {
                handle.Free();
        }
}

在我的测试中,这个函数总是不安全实现之后的第二个条目。

不幸的是,测试台不太可靠。。。如果你多次运行它,列表会被打乱,以至于谁知道在不安全之后哪个才是最快的!它没有考虑预热、jit编译时间和GC性能影响。我很想重写它以获得更多信息,但我真的没有时间。