目录

C# 字节数组和十六进制字符串的相互转化

C# 如何高效地实现字节数组 (byte array) 和十六进制字符串 (hex string) 的相互转化呢?

public static string ByteArrayToHexString(byte[] bytes)
{
    StringBuilder sb = new StringBuilder();
    foreach (byte b in bytes)
    {
        sb.Append(b.ToString("x2"));
    }
    return sb.ToString();
}
public static string ByteArrayToHexString(byte[] bytes)
{
    char[] c = new char[bytes.Length * 2];
    byte b;
    for (int i = 0; i < bytes.Length; i++)
    {
        b = ((byte)(bytes[i] >> 4));
        c[i * 2] = (char)(b > 9 ? b + 0x37 : b + 0x30);
        b = ((byte)(bytes[i] & 0xF));
        c[i * 2 + 1] = (char)(b > 9 ? b + 0x37 : b + 0x30);
    }
    return new string(c);
}
public static string ByteArrayToHexString(byte[] bytes)
{
    return BitConverter.ToString(bytes).Replace("-", "");
}
string hex = Convert.ToHexString(bytes);
string hex = new SoapHexBinary(bytes).ToString();

我们使用 BenchmarkDotNet.NET 9.0.NET Framework 4.8 上对上述方法进行性能测试。

测试代码:

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Text;

namespace BenchMarkerNet9
{
    public class Program
    {
        private static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<Benchmarker>();
        }
    };

    [MemoryDiagnoser]
    public class Benchmarker
    {
        public byte[] bytes = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 };

        [Benchmark]
        public string ByteArrayToHexString1()
        {
            StringBuilder sb = new StringBuilder();
            foreach (byte b in bytes)
            {
                sb.Append(b.ToString("x2"));
            }
            return sb.ToString();
        }

        [Benchmark]
        public string ByteArrayToHexString2()
        {
            char[] c = new char[bytes.Length * 2];
            byte b;
            for (int i = 0; i < bytes.Length; i++)
            {
                b = ((byte)(bytes[i] >> 4));
                c[i * 2] = (char)(b > 9 ? b + 0x37 : b + 0x30);
                b = ((byte)(bytes[i] & 0xF));
                c[i * 2 + 1] = (char)(b > 9 ? b + 0x37 : b + 0x30);
            }
            return new string(c);
        }

        [Benchmark]
        public string ByteArrayToHexString3()
        {
            return BitConverter.ToString(bytes).Replace("-", string.Empty);
        }

        [Benchmark]
        public string ByteArrayToHexString4()
        {
            return Convert.ToHexString(bytes);
        }
    }
}

测试结果如下:

| Method                | Mean      | Error    | StdDev   | Gen0   | Allocated |
|---------------------- |----------:|---------:|---------:|-------:|----------:|
| ByteArrayToHexString1 | 156.29 ns | 2.677 ns | 4.006 ns | 0.0429 |     808 B |
| ByteArrayToHexString2 |  22.63 ns | 0.475 ns | 0.650 ns | 0.0093 |     176 B |
| ByteArrayToHexString3 | 156.63 ns | 0.864 ns | 0.808 ns | 0.0110 |     208 B |
| ByteArrayToHexString4 |  11.45 ns | 0.262 ns | 0.408 ns | 0.0047 |      88 B |
  • Mean : Arithmetic mean of all measurements 平均值
  • Error : Half of 99.9% confidence interval 误差
  • StdDev : Standard deviation of all measurements 方差
  • Gen0 : GC Generation 0 collects per 1000 operations 第 0 代 GC 次数
  • Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B) 分配的内存
  • 1 ns : 1 Nanosecond (0.000000001 sec) 纳秒

可见,Convert.ToHexString 是最快的,手动实现优化次之,BitConverter手动实现性能很差。

测试代码:

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Runtime.Remoting.Metadata.W3cXsd2001;
using System.Text;

namespace BenchMarker480
{
    public class Program
    {
        private static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<Benchmarker>();
        }
    };

    [MemoryDiagnoser]
    public class Benchmarker
    {
        public byte[] bytes = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 };

        [Benchmark]
        public string ByteArrayToHexString1()
        {
            StringBuilder sb = new StringBuilder();
            foreach (byte b in bytes)
            {
                sb.Append(b.ToString("x2"));
            }
            return sb.ToString();
        }

        [Benchmark]
        public string ByteArrayToHexString2()
        {
            char[] c = new char[bytes.Length * 2];
            byte b;
            for (int i = 0; i < bytes.Length; i++)
            {
                b = ((byte)(bytes[i] >> 4));
                c[i * 2] = (char)(b > 9 ? b + 0x37 : b + 0x30);
                b = ((byte)(bytes[i] & 0xF));
                c[i * 2 + 1] = (char)(b > 9 ? b + 0x37 : b + 0x30);
            }
            return new string(c);
        }

        [Benchmark]
        public string ByteArrayToHexString3()
        {
            return BitConverter.ToString(bytes).Replace("-", string.Empty);
        }

        [Benchmark]
        public string ByteArrayToHexString4()
        {
            return new SoapHexBinary(bytes).ToString();
        }
    }
}

测试结果如下:

| Method                | Mean      | Error     | StdDev    | Median    | Gen0   | Allocated |
|---------------------- |----------:|----------:|----------:|----------:|-------:|----------:|
| ByteArrayToHexString1 | 539.00 ns | 22.907 ns | 67.543 ns | 513.00 ns | 0.1035 |     545 B |
| ByteArrayToHexString2 |  37.79 ns |  0.656 ns |  0.613 ns |  37.91 ns | 0.0297 |     156 B |
| ByteArrayToHexString3 | 194.82 ns |  2.988 ns |  3.557 ns | 194.53 ns | 0.0565 |     296 B |
| ByteArrayToHexString4 | 339.67 ns |  5.352 ns |  4.745 ns | 340.74 ns | 0.1135 |     597 B |

可见,手动实现优化是最快的,BitConverter 次之,SoapHexBinary手动实现性能很差。

  • Convert.ToHexString 是最快的,假设你的项目是.NET 5+,推荐使用。
  • 如果你的项目是.NET 5 以下,推荐使用手动实现优化
  • 纵向对比会发现,.NET 9StringBuilderBitConverter 性能比.NET framework 4.8 有较大提升。
  • byte 数组有一个扩展方法 ToString(),但是它返回的是 "System.Byte[]"。这是因为 System.Byte 继承自 System.Object,而 System.Object 有一个 ToString() 方法,且 System.Byte 没有重写这个方法。同样,Console.WriteLine(bytes) 也会返回 System.Byte[]
  • 不可以使用 Encoding.xxx.GetString(bytes),因为这个方法会将字节数组按照指定的编码转换成字符串,而不是转换为十六进制字符串。
  • 将 byte 数组写入 json 或其他文本文件时,建议使用 Convert.ToBase64String(bytes),不建议使用 hex string。
public static byte[] HexStringToByteArray(string hex)
{
    byte[] bytes = new byte[hex.Length / 2];
    for (int i = 0; i < hex.Length; i += 2)
    {
        bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
    }
    return bytes;
}
public static byte[] HexStringToByteArray(string hex)
{
    return Enumerable.Range(0, hex.Length)
                     .Where(x => x % 2 == 0)
                     .Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
                     .ToArray();
}
public static byte[] HexStringToByteArray(string hex)
{
    return Convert.FromHexString(hex);
}
public static byte[] HexStringToByteArray(string hex)
{
    return new SoapHexBinary(hex).Value;
}

环境同上。

测试代码:

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace BenchMarkerNet9
{
    public class Program
    {
        private static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<Benchmarker>();
        }
    };

    [MemoryDiagnoser]
    public class Benchmarker
    {
        public string hex = "0102030405060708090A0B0C0D0E0F10";

        [Benchmark]
        public byte[] HexToBytes1()
        {
            byte[] bytes = new byte[hex.Length / 2];
            for (int i = 0; i < hex.Length; i += 2)
            {
                bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
            }
            return bytes;
        }

        [Benchmark]
        public byte[] HexToBytes2()
        {
            return Enumerable.Range(0, hex.Length)
                .Where(x => x % 2 == 0)
                .Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
                .ToArray();
        }

        [Benchmark]
        public byte[] HexToBytes3()
        {
            return Convert.FromHexString(hex);
        }
    }
}

测试结果如下:

| Method      | Mean       | Error     | StdDev     | Gen0   | Allocated |
|------------ |-----------:|----------:|-----------:|-------:|----------:|
| HexToBytes1 | 172.904 ns | 8.5000 ns | 25.0625 ns | 0.0293 |     552 B |
| HexToBytes2 | 200.896 ns | 3.9337 ns |  5.7660 ns | 0.0410 |     776 B |
| HexToBytes3 |   7.851 ns | 0.1848 ns |  0.2270 ns | 0.0021 |      40 B |

可见,Convert.FromHexString 是最快的,手动实现基于Linq的实现性能很差。

测试代码:

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Linq;
using System.Runtime.Remoting.Metadata.W3cXsd2001;

namespace BenchMarker480
{
    public class Program
    {
        private static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<Benchmarker>();
        }
    };

    [MemoryDiagnoser]
    public class Benchmarker
    {
        public string hex = "0102030405060708090A0B0C0D0E0F10";

        [Benchmark]
        public byte[] HexToBytes1()
        {
            byte[] bytes = new byte[hex.Length / 2];
            for (int i = 0; i < hex.Length; i += 2)
            {
                bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
            }
            return bytes;
        }

        [Benchmark]
        public byte[] HexToBytes2()
        {
            return Enumerable.Range(0, hex.Length)
                .Where(x => x % 2 == 0)
                .Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
                .ToArray();
        }

        [Benchmark]
        public byte[] HexToBytes3()
        {
            return SoapHexBinary.Parse(hex).Value;
        }
    }
}

测试结果如下:

| Method      | Mean       | Error    | StdDev   | Gen0   | Allocated |
|------------ |-----------:|---------:|---------:|-------:|----------:|
| HexToBytes1 |   414.5 ns | 13.73 ns | 40.48 ns | 0.0663 |     349 B |
| HexToBytes2 |   586.6 ns |  3.08 ns |  2.57 ns | 0.0992 |     525 B |
| HexToBytes3 | 1,285.0 ns | 18.47 ns | 17.27 ns | 0.3071 |    1610 B |

可见,手动实现是最快的,基于Linq的实现次之,SoapHexBinary 性能很差。

  • Convert.FromHexString 是最快的,假设你的项目是.NET 5+,推荐使用。
  • 如果你的项目是.NET 5 以下,推荐使用手动实现
  • 纵向对比会发现,.NET 9手动实现性能和 Linq 性能比.NET framework 4.8 有较大提升。