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


当前回答

这是对托马拉克大受欢迎的答案(以及随后的编辑)第4版的回答。

我会证明这个编辑是错误的,并解释为什么可以恢复。在这一过程中,您可能会学到一些关于内部的东西,并看到另一个关于过早优化到底是什么以及它如何影响您的例子。

tl;dr:如果你很着急,只需使用Convert.ToByte和String.Substring(下面的“原始代码”),如果你不想重新实现Convert.ToBByte,这是最好的组合。如果你需要性能,请使用不使用Convert.ToByte的更高级的(请参阅其他答案)。不要将String.Substring与Convert.ToByte组合使用,除非有人在这个答案的注释中对此有一些有趣的说法。

警告:如果在框架中实现Convert.ToByte(char[],Int32)重载,则此答案可能会过时。这不太可能很快发生。

一般来说,我不太喜欢说“不要过早优化”,因为没有人知道“过早”是什么时候。在决定是否优化时,你必须考虑的唯一一件事是:“我有时间和资源来适当地研究优化方法吗?”。如果你不这样做,那么现在就太早了,等到你的项目更加成熟或者你需要表现(如果有真正的需要,那么你会腾出时间)。同时,做最简单的事情,可能会奏效。

原始代码:

    public static byte[] HexadecimalStringToByteArray_Original(string input)
    {
        var outputLength = input.Length / 2;
        var output = new byte[outputLength];
        for (var i = 0; i < outputLength; i++)
            output[i] = Convert.ToByte(input.Substring(i * 2, 2), 16);
        return output;
    }

第4版:

    public static byte[] HexadecimalStringToByteArray_Rev4(string input)
    {
        var outputLength = input.Length / 2;
        var output = new byte[outputLength];
        using (var sr = new StringReader(input))
        {
            for (var i = 0; i < outputLength; i++)
                output[i] = Convert.ToByte(new string(new char[2] { (char)sr.Read(), (char)sr.Read() }), 16);
        }
        return output;
    }

修订版避免了String.Substring,而是使用StringReader。给出的原因是:

编辑:您可以通过使用单个传递解析器,如下所示:

好吧,看看String.Substring的参考代码,它显然已经是“单次传递”了;为什么不应该呢?它在字节级运行,而不是在代理对上运行。

然而,它确实分配了一个新字符串,但无论如何,您需要分配一个字符串传递给Convert.ToByte。此外,修订版中提供的解决方案在每次迭代中分配另一个对象(双字符数组);您可以安全地将该分配放在循环之外,并重用数组以避免这种情况。

    public static byte[] HexadecimalStringToByteArray(string input)
    {
        var outputLength = input.Length / 2;
        var output = new byte[outputLength];
        var numeral = new char[2];
        using (var sr = new StringReader(input))
        {
            for (var i = 0; i < outputLength; i++)
            {
                numeral[0] = (char)sr.Read();
                numeral[1] = (char)sr.Read();
                output[i] = Convert.ToByte(new string(numeral), 16);
            }
        }
        return output;
    }

每个十六进制数字表示使用两个数字(符号)的单个八位字节。

但是,为什么要调用StringReader。读两遍?只需调用它的第二个重载,并要求它一次读取两个字符数组中的两个字符;并将呼叫量减少两次。

    public static byte[] HexadecimalStringToByteArray(string input)
    {
        var outputLength = input.Length / 2;
        var output = new byte[outputLength];
        var numeral = new char[2];
        using (var sr = new StringReader(input))
        {
            for (var i = 0; i < outputLength; i++)
            {
                var read = sr.Read(numeral, 0, 2);
                Debug.Assert(read == 2);
                output[i] = Convert.ToByte(new string(numeral), 16);
            }
        }
        return output;
    }

剩下的是一个字符串读取器,其唯一添加的“值”是一个并行索引(internal_pos),您可以声明自己(例如j)、一个冗余长度变量(internal_length)和一个输入字符串的冗余引用(internal_s)。换句话说,这是无用的。

如果您想知道Read是如何“读取”的,只需看看代码,它所做的就是对输入字符串调用String.CopyTo。剩下的只是记账开销,以维持我们不需要的价值。

因此,已经删除字符串读取器,并自己调用CopyTo;它更简单、更清晰、更高效。

    public static byte[] HexadecimalStringToByteArray(string input)
    {
        var outputLength = input.Length / 2;
        var output = new byte[outputLength];
        var numeral = new char[2];
        for (int i = 0, j = 0; i < outputLength; i++, j += 2)
        {
            input.CopyTo(j, numeral, 0, 2);
            output[i] = Convert.ToByte(new string(numeral), 16);
        }
        return output;
    }

你真的需要一个j索引,它以两个平行于i的步长递增吗?当然不是,只需将i乘以2(编译器应该能够将其优化为加法)。

    public static byte[] HexadecimalStringToByteArray_BestEffort(string input)
    {
        var outputLength = input.Length / 2;
        var output = new byte[outputLength];
        var numeral = new char[2];
        for (int i = 0; i < outputLength; i++)
        {
            input.CopyTo(i * 2, numeral, 0, 2);
            output[i] = Convert.ToByte(new string(numeral), 16);
        }
        return output;
    }

现在的解决方案是什么样子的?与一开始的情况完全一样,只是没有使用String.Substring来分配字符串并将数据复制到其中,而是使用了一个中间数组,将十六进制数字复制到该数组中,然后自己分配字符串并再次将数据从数组复制到字符串中(当您在字符串构造函数中传递它时)。如果字符串已经在实习池中,则第二个副本可能会被优化,但在这些情况下,string.Substring也可以避免。

事实上,如果您再次查看String.Substring,您会发现它使用了一些关于如何构造字符串的低级内部知识,以比通常更快地分配字符串,并且它直接在其中内联CopyTo使用的相同代码,以避免调用开销。

字符串.子字符串

最坏的情况:一次快速分配,一次快速复制。最佳情况:无分配,无复制。

手动方法

最坏情况:两个正常分配,一个正常复制,一个快速复制。最佳情况:一个正常分配,一个正常复制。

结论如果您想使用Convert.ToByte(String,Int32)(因为您不想自己重新实现该功能),似乎没有办法击败String.Substring;你所做的就是绕圈子,重新发明轮子(只使用次优材料)。

注意,如果您不需要极端的性能,那么使用Convert.ToByte和String.Substring是一个非常有效的选择。记住:只有在你有时间和资源调查它是如何正常工作的情况下,才选择一个替代方案。

如果有Convert.ToByte(char[],Int32),情况当然会有所不同(可以执行上面描述的操作,完全避免使用String)。

我怀疑那些通过“避免String.Substring”来报告更好性能的人也会避免Convert.ToByte(String,Int32),如果你需要性能的话,你真的应该这样做。看看其他无数的答案,找出实现这一目标的所有不同方法。

免责声明:我没有反编译框架的最新版本,以验证参考源是否是最新的,我想是的。

现在,这一切听起来都很好,也很合乎逻辑,如果你已经做到了这一点,希望甚至是显而易见的。但这是真的吗?

Intel(R) Core(TM) i7-3720QM CPU @ 2.60GHz
    Cores: 8
    Current Clock Speed: 2600
    Max Clock Speed: 2600
--------------------
Parsing hexadecimal string into an array of bytes
--------------------
HexadecimalStringToByteArray_Original: 7,777.09 average ticks (over 10000 runs), 1.2X
HexadecimalStringToByteArray_BestEffort: 8,550.82 average ticks (over 10000 runs), 1.1X
HexadecimalStringToByteArray_Rev4: 9,218.03 average ticks (over 10000 runs), 1.0X

Yes!

支撑Partridge的长凳框架,很容易破解。使用的输入是以下SHA-1哈希,重复5000次以生成100000字节长的字符串。

209113288F93A9AB8E474EA78D899AFDBB874355

玩得高兴(但要适度优化。)

其他回答

Dotnet 5更新

要从byte[](字节数组)转换为十六进制字符串,请使用:

System.Convert.ToHexString

var myBytes = new byte[100];
var myString = System.Convert.ToHexString(myBytes);

要将十六进制字符串转换为字节[],请使用:

System.Convert.FromHexString

var myString  = "E10B116E8530A340BCC7B3EAC208487B";
var myBytes = System.Convert.FromHexString(myString);

可以使用从.NET 5开始的Convert.ToHexString。还有一个用于反向操作的方法:Convert.FromHexString。


对于较旧版本的.NET,您可以使用:

public static string ByteArrayToString(byte[] ba)
{
  StringBuilder hex = new StringBuilder(ba.Length * 2);
  foreach (byte b in ba)
    hex.AppendFormat("{0:x2}", b);
  return hex.ToString();
}

or:

public static string ByteArrayToString(byte[] ba)
{
  return BitConverter.ToString(ba).Replace("-","");
}

举个例子,这里有更多的方法。

反向转换如下:

public static byte[] StringToByteArray(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;
}

使用Substring是与Convert.ToByte结合使用的最佳选项。有关详细信息,请参阅此答案。如果需要更好的性能,必须避免Convert.ToByte,然后才能删除SubString。

测试:十六进制字符串到字节数组

我注意到,大多数测试都是在将Bytes数组转换为十六进制字符串的函数上执行的。因此,在这篇文章中,我将关注另一方面:将十六进制字符串转换为字节数组的函数。若您只对结果感兴趣,可以跳到“摘要”部分。测试代码文件在文章末尾提供。

标签

我想根据接受的答案(Tomalak)将函数命名为StringToByteArrayV1,或将其快捷到V1。其余函数将以相同的方式命名:V2、V3、V4、…、。。。,等

参与功能索引

Tomalak的StringToByteArrayV1(公认答案)Mykroft的StringToByteArrayV2(使用SoapHexBinary)drphrozen的StringToByteArrayV3(查找表)CoperNick的StringToByteArrayV4(字节操作)Chris F编写的StringToByteArrayV5_1(字节操作)Chris F的StringToByteArrayV5_2(V5_1+根据Amir Rezaei的评论修改)Chris F的StringToByteArrayV5_3(V5_2+根据Ben Voigt的评论对其进行了修改)(您可以在发布后的测试代码中看到它的最终形状)Ben Mosher编写的StringToByteArrayV6(字节操作)Maratius的StringToByteArrayV7(字节操作-安全版本)Maratius的StringToByteArrayV8(字节操作-不安全版本)StringToByteArrayV9(按Geograph)AlejandroAlis编写的StringToByteArrayV10Fredrik Hu编写的StringToByteArrayV11Maarten Bodewes编写的StringToByteArrayV12ClausAndersen编写的StringToByteArrayV13Stas Makutin编写的StringToByteArrayV14JJJ的StringToByteArrayV15JamieSee的StringToByteArrayV16spacepille的StringToByteArrayV17Gregory Morse编写的StringToByteArrayV18Rick编写的StringToByteArrayV19SandRock的StringToByteArrayV20Paul编写的StringToByteArrayV21

正确性测试

我通过传递1字节的所有256个可能值来测试正确性,然后检查输出是否正确。结果:

V18中以“00”开头的字符串有问题(请参阅Roger Stewart对此的评论)。除了通过所有测试。如果十六进制字符串字母是大写的:所有函数都成功传递如果十六进制字符串字母是小写的,则以下函数失败:V5_1、V5_2、v7、V8、V15、V19

注:V5_3解决了这个问题(V5_1和V5_2)

性能测试

我已经使用Stopwatch类进行了性能测试。

长字符串的性能

input length: 10,000,000 bytes
runs: 100
average elapsed time per run:
V1 = 136.4ms
V2 = 104.5ms
V3 = 22.0ms
V4 = 9.9ms
V5_1 = 10.2ms
V5_2 = 9.0ms
V5_3 = 9.3ms
V6 = 18.3ms
V7 = 9.8ms
V8 = 8.8ms
V9 = 10.2ms
V10 = 19.0ms
V11 = 12.2ms
V12 = 27.4ms
V13 = 21.8ms
V14 = 12.0ms
V15 = 14.9ms
V16 = 15.3ms
V17 = 9.5ms
V18 got excluded from this test, because it was very slow when using very long string
V19 = 222.8ms
V20 = 66.0ms
V21 = 15.4ms

V1 average ticks per run: 1363529.4
V2 is more fast than V1 by: 1.3 times (ticks ratio)
V3 is more fast than V1 by: 6.2 times (ticks ratio)
V4 is more fast than V1 by: 13.8 times (ticks ratio)
V5_1 is more fast than V1 by: 13.3 times (ticks ratio)
V5_2 is more fast than V1 by: 15.2 times (ticks ratio)
V5_3 is more fast than V1 by: 14.8 times (ticks ratio)
V6 is more fast than V1 by: 7.4 times (ticks ratio)
V7 is more fast than V1 by: 13.9 times (ticks ratio)
V8 is more fast than V1 by: 15.4 times (ticks ratio)
V9 is more fast than V1 by: 13.4 times (ticks ratio)
V10 is more fast than V1 by: 7.2 times (ticks ratio)
V11 is more fast than V1 by: 11.1 times (ticks ratio)
V12 is more fast than V1 by: 5.0 times (ticks ratio)
V13 is more fast than V1 by: 6.3 times (ticks ratio)
V14 is more fast than V1 by: 11.4 times (ticks ratio)
V15 is more fast than V1 by: 9.2 times (ticks ratio)
V16 is more fast than V1 by: 8.9 times (ticks ratio)
V17 is more fast than V1 by: 14.4 times (ticks ratio)
V19 is more SLOW than V1 by: 1.6 times (ticks ratio)
V20 is more fast than V1 by: 2.1 times (ticks ratio)
V21 is more fast than V1 by: 8.9 times (ticks ratio)

V18的长串性能

V18 took long time at the previous test, 
so let's decrease length for it:  
input length: 1,000,000 bytes
runs: 100
average elapsed time per run: V1 = 14.1ms , V18 = 146.7ms
V1 average ticks per run: 140630.3
V18 is more SLOW than V1 by: 10.4 times (ticks ratio)

短字符串的性能

input length: 100 byte
runs: 1,000,000
V1 average ticks per run: 14.6
V2 is more fast than V1 by: 1.4 times (ticks ratio)
V3 is more fast than V1 by: 5.9 times (ticks ratio)
V4 is more fast than V1 by: 15.7 times (ticks ratio)
V5_1 is more fast than V1 by: 15.1 times (ticks ratio)
V5_2 is more fast than V1 by: 18.4 times (ticks ratio)
V5_3 is more fast than V1 by: 16.3 times (ticks ratio)
V6 is more fast than V1 by: 5.3 times (ticks ratio)
V7 is more fast than V1 by: 15.7 times (ticks ratio)
V8 is more fast than V1 by: 18.0 times (ticks ratio)
V9 is more fast than V1 by: 15.5 times (ticks ratio)
V10 is more fast than V1 by: 7.8 times (ticks ratio)
V11 is more fast than V1 by: 12.4 times (ticks ratio)
V12 is more fast than V1 by: 5.3 times (ticks ratio)
V13 is more fast than V1 by: 5.2 times (ticks ratio)
V14 is more fast than V1 by: 13.4 times (ticks ratio)
V15 is more fast than V1 by: 9.9 times (ticks ratio)
V16 is more fast than V1 by: 9.2 times (ticks ratio)
V17 is more fast than V1 by: 16.2 times (ticks ratio)
V18 is more fast than V1 by: 1.1 times (ticks ratio)
V19 is more SLOW than V1 by: 1.6 times (ticks ratio)
V20 is more fast than V1 by: 1.9 times (ticks ratio)
V21 is more fast than V1 by: 11.4 times (ticks ratio)

测试代码

在使用以下代码之前,最好先阅读本文下面的免责声明部分https://github.com/Ghosticollis/performance-tests/blob/main/MTestPerformance.cs

总结

由于性能良好,我建议使用以下函数之一,并支持大写和小写:

CoperNick的StringToByteArrayV4StringToByteArrayV9(按Geograph)spacepille的StringToByteArrayV17StringToByteArrayV5_3基本上由Chris F开发(它基于V5_1,但我根据Amir Rezaei和Ben Voigt的评论对其进行了增强)。

以下是V5_3的最终形状:

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

免责声明

警告:我没有适当的测试知识。这些原始测试的主要目的是快速概述所有发布的函数的优点。如果您需要准确的结果,请使用适当的测试工具。

最后,我想说,我是新来的,在斯塔科弗洛活跃,如果我的职位空缺,我很抱歉。如果您能发表评论,我们将不胜感激。

.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

这是一篇很棒的帖子。我喜欢瓦利德的解决方案。我还没有通过帕特里奇的测试,但似乎很快。我还需要反向过程,将十六进制字符串转换为字节数组,因此我将其作为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;
}