如何将字节数组转换为十六进制字符串,反之亦然?
当前回答
就速度而言,这似乎比这里的任何东西都要好:
public static string ToHexString(byte[] data) {
byte b;
int i, j, k;
int l = data.Length;
char[] r = new char[l * 2];
for (i = 0, j = 0; i < l; ++i) {
b = data[i];
k = b >> 4;
r[j++] = (char)(k > 9 ? k + 0x37 : k + 0x30);
k = b & 15;
r[j++] = (char)(k > 9 ? k + 0x37 : k + 0x30);
}
return new string(r);
}
其他回答
这是对托马拉克大受欢迎的答案(以及随后的编辑)第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
玩得高兴(但要适度优化。)
两个mashup,将两个半字节操作合并为一个。
可能非常有效的版本:
public static string ByteArrayToString2(byte[] ba)
{
char[] c = new char[ba.Length * 2];
for( int i = 0; i < ba.Length * 2; ++i)
{
byte b = (byte)((ba[i>>1] >> 4*((i&1)^1)) & 0xF);
c[i] = (char)(55 + b + (((b-10)>>31)&-7));
}
return new string( c );
}
Decadent linq与比特黑客版本:
public static string ByteArrayToString(byte[] ba)
{
return string.Concat( ba.SelectMany( b => new int[] { b >> 4, b & 0xF }).Select( b => (char)(55 + b + (((b-10)>>31)&-7))) );
}
并反转:
public static byte[] HexStringToByteArray( string s )
{
byte[] ab = new byte[s.Length>>1];
for( int i = 0; i < s.Length; i++ )
{
int b = s[i];
b = (b - '0') + ((('9' - b)>>31)&-7);
ab[i>>1] |= (byte)(b << 4*((i&1)^1));
}
return ab;
}
从微软的开发人员那里,一个很好的、简单的转换:
public static string ByteArrayToString(byte[] ba)
{
// Concatenate the bytes into one long string
return ba.Aggregate(new StringBuilder(32),
(sb, b) => sb.Append(b.ToString("X2"))
).ToString();
}
虽然上面的内容简洁紧凑,但性能狂热者会使用枚举器对此尖叫不已。通过Tomalak原始答案的改进版本,您可以获得最佳性能:
public static string ByteArrayToString(byte[] ba)
{
StringBuilder hex = new StringBuilder(ba.Length * 2);
for(int i=0; i < ba.Length; i++) // <-- Use for loop is faster than foreach
hex.Append(ba[i].ToString("X2")); // <-- ToString is faster than AppendFormat
return hex.ToString();
}
这是迄今为止我在这里看到的所有例程中速度最快的。不要只相信我的话…对每个例程进行性能测试并自行检查其CIL代码。
如果您希望比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")));
(后者来自对原帖子的评论。)
// a safe version of the lookup solution:
public static string ByteArrayToHexViaLookup32Safe(byte[] bytes, bool withZeroX)
{
if (bytes.Length == 0)
{
return withZeroX ? "0x" : "";
}
int length = bytes.Length * 2 + (withZeroX ? 2 : 0);
StateSmall stateToPass = new StateSmall(bytes, withZeroX);
return string.Create(length, stateToPass, (chars, state) =>
{
int offset0x = 0;
if (state.WithZeroX)
{
chars[0] = '0';
chars[1] = 'x';
offset0x += 2;
}
Span<uint> charsAsInts = MemoryMarshal.Cast<char, uint>(chars.Slice(offset0x));
int targetLength = state.Bytes.Length;
for (int i = 0; i < targetLength; i += 1)
{
uint val = Lookup32[state.Bytes[i]];
charsAsInts[i] = val;
}
});
}
private struct StateSmall
{
public StateSmall(byte[] bytes, bool withZeroX)
{
Bytes = bytes;
WithZeroX = withZeroX;
}
public byte[] Bytes;
public bool WithZeroX;
}