如何将字节数组转换为十六进制字符串,反之亦然?
当前回答
安全版本:
public static class HexHelper
{
[System.Diagnostics.Contracts.Pure]
public static string ToHex(this byte[] value)
{
if (value == null)
throw new ArgumentNullException("value");
const string hexAlphabet = @"0123456789ABCDEF";
var chars = new char[checked(value.Length * 2)];
unchecked
{
for (int i = 0; i < value.Length; i++)
{
chars[i * 2] = hexAlphabet[value[i] >> 4];
chars[i * 2 + 1] = hexAlphabet[value[i] & 0xF];
}
}
return new string(chars);
}
[System.Diagnostics.Contracts.Pure]
public static byte[] FromHex(this string value)
{
if (value == null)
throw new ArgumentNullException("value");
if (value.Length % 2 != 0)
throw new ArgumentException("Hexadecimal value length must be even.", "value");
unchecked
{
byte[] result = new byte[value.Length / 2];
for (int i = 0; i < result.Length; i++)
{
// 0(48) - 9(57) -> 0 - 9
// A(65) - F(70) -> 10 - 15
int b = value[i * 2]; // High 4 bits.
int val = ((b - '0') + ((('9' - b) >> 31) & -7)) << 4;
b = value[i * 2 + 1]; // Low 4 bits.
val += (b - '0') + ((('9' - b) >> 31) & -7);
result[i] = checked((byte)val);
}
return result;
}
}
}
不安全版本适用于那些喜欢性能且不怕不安全的人。ToHex快35%,FromHex快10%。
public static class HexUnsafeHelper
{
[System.Diagnostics.Contracts.Pure]
public static unsafe string ToHex(this byte[] value)
{
if (value == null)
throw new ArgumentNullException("value");
const string alphabet = @"0123456789ABCDEF";
string result = new string(' ', checked(value.Length * 2));
fixed (char* alphabetPtr = alphabet)
fixed (char* resultPtr = result)
{
char* ptr = resultPtr;
unchecked
{
for (int i = 0; i < value.Length; i++)
{
*ptr++ = *(alphabetPtr + (value[i] >> 4));
*ptr++ = *(alphabetPtr + (value[i] & 0xF));
}
}
}
return result;
}
[System.Diagnostics.Contracts.Pure]
public static unsafe byte[] FromHex(this string value)
{
if (value == null)
throw new ArgumentNullException("value");
if (value.Length % 2 != 0)
throw new ArgumentException("Hexadecimal value length must be even.", "value");
unchecked
{
byte[] result = new byte[value.Length / 2];
fixed (char* valuePtr = value)
{
char* valPtr = valuePtr;
for (int i = 0; i < result.Length; i++)
{
// 0(48) - 9(57) -> 0 - 9
// A(65) - F(70) -> 10 - 15
int b = *valPtr++; // High 4 bits.
int val = ((b - '0') + ((('9' - b) >> 31) & -7)) << 4;
b = *valPtr++; // Low 4 bits.
val += (b - '0') + ((('9' - b) >> 31) & -7);
result[i] = checked((byte)val);
}
}
return result;
}
}
}
顺便提一下对于每次调用的转换函数错误时初始化字母表的基准测试,字母表必须是常量(对于字符串)或静态只读(对于字符[])。然后,基于字母表的字节[]到字符串的转换变得和字节操作版本一样快。
当然,测试必须在Release中编译(带有优化),并关闭调试选项“抑制JIT优化”(如果代码必须可调试,则“仅启用我的代码”也是如此)。
其他回答
对于插入SQL字符串(如果不使用命令参数):
public static String ByteArrayToSQLHexString(byte[] Source)
{
return = "0x" + BitConverter.ToString(Source).Replace("-", "");
}
将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中找到。
为了提高性能,我会选择drphrozens解决方案。解码器的一个微小的优化可能是为任一字符使用一个表,以消除“<<4”。
显然,这两个方法调用代价高昂。如果对输入或输出数据进行某种检查(可以是CRC、校验和或其他),则If(b==255)。。。可以跳过,从而也可以完全调用方法。
使用offset++和offset代替offset和offset+1可能会带来一些理论上的好处,但我怀疑编译器比我更好地处理这一点。
private static readonly byte[] LookupTableLow = new byte[] {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
private static readonly byte[] LookupTableHigh = new byte[] {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
private static byte LookupLow(char c)
{
var b = LookupTableLow[c];
if (b == 255)
throw new IOException("Expected a hex character, got " + c);
return b;
}
private static byte LookupHigh(char c)
{
var b = LookupTableHigh[c];
if (b == 255)
throw new IOException("Expected a hex character, got " + c);
return b;
}
public static byte ToByte(char[] chars, int offset)
{
return (byte)(LookupHigh(chars[offset++]) | LookupLow(chars[offset]));
}
这只是我的头顶,没有经过测试或基准测试。
这里不想赘述很多答案,但我发现了一个十六进制字符串解析器的相当优化(比公认的好4.5倍)、简单的实现。首先,我的测试输出(第一批是我的实现):
Give me that string:
04c63f7842740c77e545bb0b2ade90b384f119f6ab57b680b7aa575a2f40939f
Time to parse 100,000 times: 50.4192 ms
Result as base64: BMY/eEJ0DHflRbsLKt6Qs4TxGfarV7aAt6pXWi9Ak58=
BitConverter'd: 04-C6-3F-78-42-74-0C-77-E5-45-BB-0B-2A-DE-90-B3-84-F1-19-F6-AB-5
7-B6-80-B7-AA-57-5A-2F-40-93-9F
Accepted answer: (StringToByteArray)
Time to parse 100000 times: 233.1264ms
Result as base64: BMY/eEJ0DHflRbsLKt6Qs4TxGfarV7aAt6pXWi9Ak58=
BitConverter'd: 04-C6-3F-78-42-74-0C-77-E5-45-BB-0B-2A-DE-90-B3-84-F1-19-F6-AB-5
7-B6-80-B7-AA-57-5A-2F-40-93-9F
With Mono's implementation:
Time to parse 100000 times: 777.2544ms
Result as base64: BMY/eEJ0DHflRbsLKt6Qs4TxGfarV7aAt6pXWi9Ak58=
BitConverter'd: 04-C6-3F-78-42-74-0C-77-E5-45-BB-0B-2A-DE-90-B3-84-F1-19-F6-AB-5
7-B6-80-B7-AA-57-5A-2F-40-93-9F
With SoapHexBinary:
Time to parse 100000 times: 845.1456ms
Result as base64: BMY/eEJ0DHflRbsLKt6Qs4TxGfarV7aAt6pXWi9Ak58=
BitConverter'd: 04-C6-3F-78-42-74-0C-77-E5-45-BB-0B-2A-DE-90-B3-84-F1-19-F6-AB-5
7-B6-80-B7-AA-57-5A-2F-40-93-9F
base64和“BitConverter'd”行用于测试正确性。请注意,它们是相等的。
实施:
public static byte[] ToByteArrayFromHex(string hexString)
{
if (hexString.Length % 2 != 0) throw new ArgumentException("String must have an even length");
var array = new byte[hexString.Length / 2];
for (int i = 0; i < hexString.Length; i += 2)
{
array[i/2] = ByteFromTwoChars(hexString[i], hexString[i + 1]);
}
return array;
}
private static byte ByteFromTwoChars(char p, char p_2)
{
byte ret;
if (p <= '9' && p >= '0')
{
ret = (byte) ((p - '0') << 4);
}
else if (p <= 'f' && p >= 'a')
{
ret = (byte) ((p - 'a' + 10) << 4);
}
else if (p <= 'F' && p >= 'A')
{
ret = (byte) ((p - 'A' + 10) << 4);
} else throw new ArgumentException("Char is not a hex digit: " + p,"p");
if (p_2 <= '9' && p_2 >= '0')
{
ret |= (byte) ((p_2 - '0'));
}
else if (p_2 <= 'f' && p_2 >= 'a')
{
ret |= (byte) ((p_2 - 'a' + 10));
}
else if (p_2 <= 'F' && p_2 >= 'A')
{
ret |= (byte) ((p_2 - 'A' + 10));
} else throw new ArgumentException("Char is not a hex digit: " + p_2, "p_2");
return ret;
}
我尝试了一些不安全的东西,并将(显然是冗余的)字符移动到另一个方法来蚕食if序列,但这是最快的。
(我承认这回答了一半的问题。我觉得字符串->字节[]转换不足,而字节[]->字符串角度似乎被很好地覆盖了。因此,这个答案。)
您可以使用BitConverter.ToString方法:
byte[] bytes = {0, 1, 2, 4, 8, 16, 32, 64, 128, 256}
Console.WriteLine( BitConverter.ToString(bytes));
输出:
00-01-02-04-08-10-20-40-80-FF
更多信息:BitConverter.ToString方法(Byte[])
推荐文章
- 实体框架核心:在上一个操作完成之前,在此上下文中开始的第二个操作
- 如何为构造函数定制Visual Studio的私有字段生成快捷方式?
- 如何使用JSON确保字符串是有效的JSON。网
- 使用C返回一个数组
- AppSettings从.config文件中获取值
- 通过HttpClient向REST API发布一个空体
- 如何检查IEnumerable是否为空或空?
- 自动化invokerrequired代码模式
- 在c#代码中设置WPF文本框的背景颜色
- 在c#中,什么是单子?
- c#和Java中的泛型有什么不同?和模板在c++ ?
- 向对象数组添加属性
- c#线程安全快速(est)计数器
- 如何将此foreach代码转换为Parallel.ForEach?
- 如何分裂()一个分隔字符串到一个列表<字符串>