Avatar ·

Как преобразовать массив байтов в шестнадцатеричную строку и обратно в C#?

Всем привет! Я начинающий C# разработчик, и у меня возникла проблема при работе с бинарными данными. Мне нужно сохранить массив байтов (например, хеш SHA256) в текстовый файл, а потом прочитать его обратно. Я понял, что просто записать байты как есть нельзя — получается кракозябра. Нужно преобразовать их в строку шестнадцатеричных символов, а потом при чтении снова превратить эту строку в массив байтов.

Я пробовал сделать это сам, но мой код работает неправильно. Вот что я написал:

byte[] data = { 0x0F, 0xBA, 0x01, 0xFF };
string hex = "";
foreach (byte b in data)
{
    hex += b.ToString("X");
}
// Получается "FBA1FF" — без ведущих нулей!
Console.WriteLine(hex);

Когда я пытаюсь прочитать эту строку обратно, я не могу понять, где заканчивается один байт и начинается другой. Например, строка "FBA1FF" может быть интерпретирована по-разному: {0xFB, 0xA1, 0xFF} или {0x0F, 0xBA, 0x1F, 0xF} — это неправильно.

Я слышал про Convert.ToHexString() и Convert.FromHexString() в .NET 5+, но мой проект на .NET Framework 4.7.2, и эти методы недоступны. Как правильно решить эту задачу?

Подскажите, пожалуйста, надёжный способ конвертации байтов в hex-строку и обратно, который работает во всех версиях .NET. Заранее спасибо!

<<>>

Лучший ответ: универсальное решение для .NET Framework и .NET Core

Проблема вашего кода в том, что метод ToString("X") не добавляет ведущий ноль для байтов меньше 0x10 (например, 0x0F превращается в "F", а не "0F"). Из-за этого строка становится неоднозначной при обратном парсинге.

Вот правильное и надёжное решение, которое работает во всех версиях .NET, начиная с .NET Framework 2.0:

1. Преобразование byte[] в hex-строку

public static string ByteArrayToHexString(byte[] bytes)
{
    if (bytes == null || bytes.Length == 0)
        return string.Empty;

    // Используем StringBuilder для эффективной конкатенации
    StringBuilder hex = new StringBuilder(bytes.Length * 2);
    
    foreach (byte b in bytes)
    {
        // "X2" — гарантирует два шестнадцатеричных символа с ведущим нулём
        hex.Append(b.ToString("X2"));
    }
    
    return hex.ToString();
}

2. Преобразование hex-строки обратно в byte[]

public static byte[] HexStringToByteArray(string hex)
{
    if (string.IsNullOrEmpty(hex))
        return Array.Empty<byte>();

    // Удаляем возможные дефисы или пробелы
    hex = hex.Replace("-", "").Replace(" ", "");
    
    // Проверка на чётную длину
    if (hex.Length % 2 != 0)
        throw new ArgumentException("Hex string must have an even number of characters");

    byte[] bytes = new byte[hex.Length / 2];
    
    for (int i = 0; i < hex.Length; i += 2)
    {
        // Convert.ToByte с NumberStyles.HexNumber — самый надёжный способ
        bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
    }
    
    return bytes;
}

3. Пример использования

byte[] originalData = { 0x0F, 0xBA, 0x01, 0xFF };
string hexString = ByteArrayToHexString(originalData);
Console.WriteLine(hexString); // "0FBA01FF" — правильно!

byte[] restoredData = HexStringToByteArray(hexString);
// restoredData == { 0x0F, 0xBA, 0x01, 0xFF } — полное совпадение
Console.WriteLine(BitConverter.ToString(restoredData)); // "0F-BA-01-FF"

4. Альтернативный метод (более производительный)

Если вам нужна максимальная производительность (например, для больших массивов), используйте прямое копирование символов:

public static string ByteArrayToHexStringFast(byte[] bytes)
{
    char[] result = new char[bytes.Length * 2];
    int hexCharIndex = 0;
    
    foreach (byte b in bytes)
    {
        // Старший полубайт
        result[hexCharIndex++] = GetHexChar(b >> 4);
        // Младший полубайт
        result[hexCharIndex++] = GetHexChar(b & 0x0F);
    }
    
    return new string(result);
}

private static char GetHexChar(int value)
{
    // value от 0 до 15
    return (char)(value < 10 ? '0' + value : 'A' + value - 10);
}

5. Важные замечания

  • Регистр символов: В методах выше используется верхний регистр (A-F). Если нужен нижний, замените "X2" на "x2" и 'A' на 'a'.
  • Обработка ошибок: Всегда проверяйте входные данные на null и корректность формата.
  • Совместимость: Если вы используете .NET 5+, просто вызывайте Convert.ToHexString(bytes) и Convert.FromHexString(hex) — это встроенные методы.
  • Альтернатива BitConverter: Встроенный BitConverter.ToString(bytes) возвращает строку с дефисами (например, "0F-BA-01-FF"). Их можно удалить, но это менее эффективно.

Используйте первый метод (со StringBuilder) для обычных задач — он достаточно быстр и легко читается. Для высоконагруженных систем применяйте второй вариант с массивом char.

<<>>

Альтернативный ответ: использование Span<T> и MemoryExtensions (для .NET Core 2.1+)

Если ваш проект использует .NET Core 2.1 или новее (включая .NET 5/6/7/8), вы можете применить более современный подход с Span<T> для минимизации аллокаций памяти:

Преобразование byte[] в hex-строку

public static string ByteArrayToHexStringSpan(byte[] bytes)
{
    if (bytes == null || bytes.Length == 0)
        return string.Empty;

    // Используем перегрузку с Span для избежания создания промежуточных строк
    return Convert.ToHexString(bytes.AsSpan());
}

Преобразование hex-строки в byte[]

public static byte[] HexStringToByteArraySpan(string hex)
{
    if (string.IsNullOrEmpty(hex))
        return Array.Empty<byte>();

    // Удаляем возможные дефисы (как от BitConverter)
    hex = hex.Replace("-", "");
    
    // Используем встроенный метод с проверкой
    return Convert.FromHexString(hex);
}

Преимущества этого подхода:

  • Минимальное выделение памяти (работает напрямую с Span).
  • Встроенная проверка на корректность входных данных.
  • Высокая скорость выполнения.

Недостаток: Методы Convert.ToHexString и Convert.FromHexString доступны только начиная с .NET 5.0. Для .NET Core 2.1–3.1 используйте Span<T> вручную:

public static string ByteArrayToHexStringSpanManual(byte[] bytes)
{
    Span<char> chars = stackalloc char[bytes.Length * 2];
    for (int i = 0; i < bytes.Length; i++)
    {
        byte b = bytes[i];
        chars[i * 2] = GetHexChar(b >> 4);
        chars[i * 2 + 1] = GetHexChar(b & 0x0F);
    }
    return new string(chars);
}

private static char GetHexChar(int value) => 
    (char)(value < 10 ? '0' + value : 'A' + value - 10);

Этот метод особенно эффективен для небольших массивов (до 256 байт), так как использует стек вместо кучи.