Avatar ·

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

Всем привет! Я начинающий C# разработчик и столкнулся с проблемой при работе с байтовыми данными. Мне нужно сохранить хеш (SHA256) в текстовый файл, а потом загрузить его обратно для проверки. Хеш я получаю в виде массива байтов (byte[]), но для записи в файл его нужно преобразовать в строку, а потом при чтении — обратно в массив байтов. Я пробовал просто использовать `BitConverter.ToString(bytes)`, но он выдает строку с дефисами между байтами, например: "A1-B2-C3". Это не совсем то, что нужно — мне хотелось бы получить компактную строку без разделителей, типа "A1B2C3". А когда я пытаюсь прочитать такую строку обратно, я не знаю, как правильно преобразовать её в массив байтов. Вот пример моего кода, который сейчас не работает так, как хотелось бы: ```csharp byte[] hashBytes = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes("test")); string hexString = BitConverter.ToString(hashBytes); // Получаем "A1-B2-C3..." // Как убрать дефисы? // И как потом преобразовать hexString обратно в byte[]? ``` Подскажите, пожалуйста, как правильно и эффективно решить эту задачу? Есть ли встроенные методы в .NET для этого, или нужно писать свою реализацию? Заранее спасибо! <<>>

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

Отличный вопрос! Преобразование массива байтов в шестнадцатеричную строку — одна из самых частых задач при работе с криптографией, сетевыми протоколами или двоичными данными. Рассмотрим несколько надёжных способов, начиная с самых современных.

Способ 1: Convert.ToHexString и Convert.FromHexString (.NET 5+)

Начиная с .NET 5, в стандартной библиотеке появились встроенные методы, которые делают именно то, что вам нужно — без дефисов и лишних символов:

using System;

byte[] hashBytes = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes("test"));

// Преобразование byte[] -> hex-строка (без разделителей)
string hexString = Convert.ToHexString(hashBytes);
Console.WriteLine(hexString); // Вывод: "9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08"

// Преобразование hex-строка -> byte[]
byte[] restoredBytes = Convert.FromHexString(hexString);
// restoredBytes будет идентичен hashBytes

Важно: Эти методы работают только в .NET 5 и выше (включая .NET Core 3.1, но там они в пространстве имен System не доступны, нужно добавлять NuGet-пакет). Для старых версий используйте следующие способы.

Способ 2: BitConverter.ToString с удалением дефисов

Если вы используете старую версию .NET Framework, можно адаптировать ваш текущий код:

byte[] hashBytes = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes("test"));

// Убираем дефисы через Replace
string hexString = BitConverter.ToString(hashBytes).Replace("-", "");
// Или через ToLower, если нужны строчные буквы:
// string hexString = BitConverter.ToString(hashBytes).Replace("-", "").ToLower();

// Обратное преобразование — придется писать свой метод
byte[] restoredBytes = HexStringToByteArray(hexString);

// Вспомогательный метод для конвертации hex-строки в byte[]
static byte[] HexStringToByteArray(string hex)
{
    if (hex.Length % 2 != 0)
        throw new ArgumentException("Строка должна содержать четное количество символов");
    
    byte[] bytes = new byte[hex.Length / 2];
    for (int i = 0; i < bytes.Length; i++)
    {
        bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
    }
    return bytes;
}

Способ 3: Ручная реализация для максимальной производительности

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

// Преобразование byte[] -> hex-строка (быстро, без лишних аллокаций)
static string ByteArrayToHexString(byte[] bytes)
{
    char[] result = new char[bytes.Length * 2];
    for (int i = 0; i < bytes.Length; i++)
    {
        int value = bytes[i];
        result[i * 2] = GetHexChar(value >> 4);      // старший полубайт
        result[i * 2 + 1] = GetHexChar(value & 0x0F); // младший полубайт
    }
    return new string(result);
}

static char GetHexChar(int nibble)
{
    return (char)(nibble < 10 ? '0' + nibble : 'A' + nibble - 10);
}

// Обратное преобразование (hex-строка -> byte[])
static byte[] HexStringToByteArrayFast(string hex)
{
    if (hex.Length % 2 != 0)
        throw new ArgumentException("Строка должна содержать четное количество символов");
    
    byte[] bytes = new byte[hex.Length / 2];
    for (int i = 0; i < bytes.Length; i++)
    {
        bytes[i] = (byte)((GetHexValue(hex[i * 2]) << 4) | GetHexValue(hex[i * 2 + 1]));
    }
    return bytes;
}

static int GetHexValue(char c)
{
    if (c >= '0' && c <= '9') return c - '0';
    if (c >= 'A' && c <= 'F') return c - 'A' + 10;
    if (c >= 'a' && c <= 'f') return c - 'a' + 10;
    throw new ArgumentException($"Недопустимый символ: {c}");
}

Что выбрать?

  • .NET 5+ — используйте Convert.ToHexString / Convert.FromHexString. Это самый простой и быстрый вариант.
  • .NET Framework 4.x — используйте BitConverter.ToString с Replace("-", "") и ручной метод для обратного преобразования.
  • Большие данные — ручная реализация из способа 3 даст лучшую производительность.

Теперь ваш код для сохранения и загрузки хеша будет выглядеть так:

// Сохранение
byte[] hashBytes = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes("test"));
string hexString = Convert.ToHexString(hashBytes);
File.WriteAllText("hash.txt", hexString);

// Загрузка
string loadedHex = File.ReadAllText("hash.txt");
byte[] loadedHash = Convert.FromHexString(loadedHex);
<<>>

Альтернативный подход: использование StringBuilder и Span

Если вы предпочитаете более читаемый код, чем ручное манипулирование символами, но при этом хотите избежать лишних выделений памяти, можно использовать StringBuilder с фиксированной ёмкостью. Этот метод особенно удобен, когда нужно конвертировать массив байтов в строку с произвольным форматированием (например, с разделителями или разным регистром).

Пример с StringBuilder:

using System.Text;

static string ByteArrayToHexString(byte[] bytes)
{
    var sb = new StringBuilder(bytes.Length * 2);
    foreach (byte b in bytes)
    {
        sb.Append(b.ToString("X2")); // "X2" — шестнадцатеричный формат с ведущим нулём
    }
    return sb.ToString();
}

// Обратное преобразование можно оставить из первого ответа
static byte[] HexStringToByteArray(string hex)
{
    int length = hex.Length;
    byte[] bytes = new byte[length / 2];
    for (int i = 0; i < length; i += 2)
    {
        bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
    }
    return bytes;
}

Использование Span для высокой производительности (.NET Core 2.1+)

Для максимальной эффективности при работе с большими объёмами данных можно использовать Span и Span:

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

Недостаток: stackalloc может вызвать переполнение стека при очень больших массивах (больше ~256 КБ). Для таких случаев лучше использовать ArrayPool или массив в куче.

В целом, для 99% задач достаточно встроенного Convert.ToHexString — он оптимизирован и протестирован Microsoft.