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


当前回答

如果您希望比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")));

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

其他回答

具有扩展支持的基本解决方案

public static class Utils
{
    public static byte[] ToBin(this string hex)
    {
        int NumberChars = hex.Length;
        byte[] bytes = new byte[NumberChars / 2];
        for (int i = 0; i < NumberChars; i += 2)
            bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
        return bytes;
    }
    public static string ToHex(this byte[] ba)
    {
        return  BitConverter.ToString(ba).Replace("-", "");
    }
}

并像下面那样使用这个类

    byte[] arr1 = new byte[] { 1, 2, 3 };
    string hex1 = arr1.ToHex();
    byte[] arr2 = hex1.ToBin();

我将参加这个比特拨弄比赛,因为我有一个同样使用比特拨弄来解码十六进制的答案。请注意,使用字符数组可能会更快,因为调用StringBuilder方法也需要时间。

public static String ToHex (byte[] data)
{
    int dataLength = data.Length;
    // pre-create the stringbuilder using the length of the data * 2, precisely enough
    StringBuilder sb = new StringBuilder (dataLength * 2);
    for (int i = 0; i < dataLength; i++) {
        int b = data [i];

        // check using calculation over bits to see if first tuple is a letter
        // isLetter is zero if it is a digit, 1 if it is a letter
        int isLetter = (b >> 7) & ((b >> 6) | (b >> 5)) & 1;

        // calculate the code using a multiplication to make up the difference between
        // a digit character and an alphanumerical character
        int code = '0' + ((b >> 4) & 0xF) + isLetter * ('A' - '9' - 1);
        // now append the result, after casting the code point to a character
        sb.Append ((Char)code);

        // do the same with the lower (less significant) tuple
        isLetter = (b >> 3) & ((b >> 2) | (b >> 1)) & 1;
        code = '0' + (b & 0xF) + isLetter * ('A' - '9' - 1);
        sb.Append ((Char)code);
    }
    return sb.ToString ();
}

public static byte[] FromHex (String hex)
{

    // pre-create the array
    int resultLength = hex.Length / 2;
    byte[] result = new byte[resultLength];
    // set validity = 0 (0 = valid, anything else is not valid)
    int validity = 0;
    int c, isLetter, value, validDigitStruct, validDigit, validLetterStruct, validLetter;
    for (int i = 0, hexOffset = 0; i < resultLength; i++, hexOffset += 2) {
        c = hex [hexOffset];

        // check using calculation over bits to see if first char is a letter
        // isLetter is zero if it is a digit, 1 if it is a letter (upper & lowercase)
        isLetter = (c >> 6) & 1;

        // calculate the tuple value using a multiplication to make up the difference between
        // a digit character and an alphanumerical character
        // minus 1 for the fact that the letters are not zero based
        value = ((c & 0xF) + isLetter * (-1 + 10)) << 4;

        // check validity of all the other bits
        validity |= c >> 7; // changed to >>, maybe not OK, use UInt?

        validDigitStruct = (c & 0x30) ^ 0x30;
        validDigit = ((c & 0x8) >> 3) * (c & 0x6);
        validity |= (isLetter ^ 1) * (validDigitStruct | validDigit);

        validLetterStruct = c & 0x18;
        validLetter = (((c - 1) & 0x4) >> 2) * ((c - 1) & 0x2);
        validity |= isLetter * (validLetterStruct | validLetter);

        // do the same with the lower (less significant) tuple
        c = hex [hexOffset + 1];
        isLetter = (c >> 6) & 1;
        value ^= (c & 0xF) + isLetter * (-1 + 10);
        result [i] = (byte)value;

        // check validity of all the other bits
        validity |= c >> 7; // changed to >>, maybe not OK, use UInt?

        validDigitStruct = (c & 0x30) ^ 0x30;
        validDigit = ((c & 0x8) >> 3) * (c & 0x6);
        validity |= (isLetter ^ 1) * (validDigitStruct | validDigit);

        validLetterStruct = c & 0x18;
        validLetter = (((c - 1) & 0x4) >> 2) * ((c - 1) & 0x2);
        validity |= isLetter * (validLetterStruct | validLetter);
    }

    if (validity != 0) {
        throw new ArgumentException ("Hexadecimal encoding incorrect for input " + hex);
    }

    return result;
}

从Java代码转换而来。

将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("-", "");
}

为了方便以后复制和粘贴,将几个答案合并到一个类中:

/// <summary>
/// Extension methods to quickly convert byte array to string and back.
/// </summary>
public static class HexConverter
{
    /// <summary>
    /// Map values to hex digits
    /// </summary>
    private static readonly char[] HexDigits =
        {
            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
        };

    /// <summary>
    /// Map 56 characters between ['0', 'F'] to their hex equivalents, and set invalid characters
    /// such that they will overflow byte to fail conversion.
    /// </summary>
    private static readonly ushort[] HexValues =
        {
            0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x0009, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100,
            0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100,
            0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x0100, 0x000A, 0x000B,
            0x000C, 0x000D, 0x000E, 0x000F
        };

    /// <summary>
    /// Empty byte array 
    /// </summary>
    private static readonly byte[] Empty = new byte[0];

    /// <summary>
    /// Convert a byte array to a hexadecimal string.
    /// </summary>
    /// <param name="bytes">
    /// The input byte array.
    /// </param>
    /// <returns>
    /// A string of hexadecimal digits.
    /// </returns>
    public static string ToHexString(this byte[] bytes)
    {
        var c = new char[bytes.Length * 2];
        for (int i = 0, j = 0; i < bytes.Length; i++)
        {
            c[j++] = HexDigits[bytes[i] >> 4];
            c[j++] = HexDigits[bytes[i] & 0x0F];
        }

        return new string(c);
    }

    /// <summary>
    /// Parse a string of hexadecimal digits into a byte array.
    /// </summary>
    /// <param name="hexadecimalString">
    /// The hexadecimal string.
    /// </param>
    /// <returns>
    /// The parsed <see cref="byte[]"/> array.
    /// </returns>
    /// <exception cref="ArgumentException">
    /// The input string either contained invalid characters, or was of an odd length.
    /// </exception>
    public static byte[] ToByteArray(string hexadecimalString)
    {
        if (!TryParse(hexadecimalString, out var value))
        {
            throw new ArgumentException("Invalid hexadecimal string", nameof(hexadecimalString));
        }

        return value;
    }

    /// <summary>
    /// Parse a hexadecimal string to bytes
    /// </summary>
    /// <param name="hexadecimalString">
    /// The hexadecimal string, which must be an even number of characters.
    /// </param>
    /// <param name="value">
    /// The parsed value if successful.
    /// </param>
    /// <returns>
    /// True if successful.
    /// </returns>
    public static bool TryParse(string hexadecimalString, out byte[] value)
    {
        if (hexadecimalString.Length == 0)
        {
            value = Empty;
            return true;
        }

        if (hexadecimalString.Length % 2 != 0)
        {
            value = Empty;
            return false;
        }

        try
        {

            value = new byte[hexadecimalString.Length / 2];
            for (int i = 0, j = 0; j < hexadecimalString.Length; i++)
            {
                value[i] = (byte)((HexValues[hexadecimalString[j++] - '0'] << 4)
                                  | HexValues[hexadecimalString[j++] - '0']);
            }

            return true;
        }
        catch (OverflowException)
        {
            value = Empty;
            return false;
        }
    }
}