«Строки» и «байты»: почему это вечная головная боль программиста?
Представьте себе ситуацию: вы пишете на C# сетевое приложение, отправляете данные на сервер, а на выходе — каша из символов. Или шифруете пароль, а на другом конце его не могут расшифровать. Знакомо? Корень зла часто кроется в неправильном преобразовании строки в массив байтов (и обратно).
Вопрос «Converting string to byte array in C#» на Stack Overflow собрал тысячи просмотров не случайно. Это база, на которой держатся работа с файлами, криптография, сериализация и сетевое взаимодействие. Без понимания этой темы вы будете наступать на одни и те же грабли. Давайте раз и навсегда разберемся, как делать это правильно.
Почему нельзя просто взять и преобразовать строку в байты?
Наивный подход — использовать BitConverter.GetBytes() для символа? Нет. Проблема в том, что строка — это абстракция, состоящая из символов (char, 2 байта в C#). А массив байтов — это сырые данные, которые зависят от кодировки.
Один и тот же символ «A» в кодировке UTF-8 займет 1 байт, в UTF-16 — 2 байта, а в UTF-32 — 4 байта. Если вы выберете не ту кодировку, данные будут потеряны. Запомните главное правило: всегда указывайте кодировку явно.
Самый популярный метод: Encoding.UTF8.GetBytes()
Это то, что вы будете использовать в 90% случаев. UTF-8 — стандарт де-факто для веба, JSON и большинства API.
using System;
using System.Text;
class Program
{
static void Main()
{
string original = "Привет, мир! 🌍";
// Преобразуем строку в массив байтов (UTF-8)
byte[] utf8Bytes = Encoding.UTF8.GetBytes(original);
// Обратное преобразование (для проверки)
string restored = Encoding.UTF8.GetString(utf8Bytes);
Console.WriteLine($"Исходная строка: {original}");
Console.WriteLine($"Массив байтов (длина {utf8Bytes.Length}): {BitConverter.ToString(utf8Bytes)}");
Console.WriteLine($"Восстановленная строка: {restored}");
}
}
Результат работы:
Исходная строка: Привет, мир! 🌍
Массив байтов (длина 18): D0-9F-D1-80-D0-B8-D0-B2-D0-B5-D1-82-2C-20-D0-BC-D0-B8-D1-80-21-20-F0-9F-8C-8D
Восстановленная строка: Привет, мир! 🌍
Обратите внимание: эмодзи «🌍» занимает 4 байта (F0-9F-8C-8D). UTF-8 умеет работать с любыми символами Юникода, в отличие от устаревших кодировок.
Другие популярные кодировки
- Encoding.ASCII — только 7-битные символы (0-127). Для кириллицы или спецсимволов не подходит — все, что выше 127, превратится в '?'
- Encoding.Unicode (он же UTF-16LE) — используется внутри C# для хранения строк. Каждый символ занимает 2 байта (или 4 для суррогатных пар).
- Encoding.UTF32 — каждый символ строго 4 байта. Ресурсоёмко, но предсказуемо.
- Encoding.Default — системная кодировка (ANSI). Избегайте! Она разная на разных компьютерах.
Преобразование с помощью BitConverter и MemoryStream
Иногда нужно получить «сырое» представление строки как массива символов (char). Это не то же самое, что байты в кодировке, но бывает полезно для низкоуровневой работы.
string text = "C#";
// Получаем массив char
char[] chars = text.ToCharArray();
// Конвертируем каждый char в 2 байта (Little-Endian)
byte[] rawBytes = new byte[chars.Length * sizeof(char)];
Buffer.BlockCopy(chars, 0, rawBytes, 0, rawBytes.Length);
Console.WriteLine(BitConverter.ToString(rawBytes)); // 43-00-23-00
Этот метод даёт массив в кодировке UTF-16LE (как хранится в памяти). Используйте его только если точно знаете, что делаете.
Специальный случай: Base64
Если вам нужно передать массив байтов в виде текста (например, в JSON или URL), используйте Base64. Это не кодировка символов, а способ представления двоичных данных.
string input = "Hello, C#!";
byte[] bytes = Encoding.UTF8.GetBytes(input);
// В Base64
string base64 = Convert.ToBase64String(bytes);
Console.WriteLine(base64); // SGVsbG8sIEMh
// Обратно
byte[] decodedBytes = Convert.FromBase64String(base64);
string output = Encoding.UTF8.GetString(decodedBytes);
Console.WriteLine(output); // Hello, C#!
Base64 увеличивает размер данных на ~33%, но гарантирует, что они не будут повреждены при передаче через текстовые протоколы.
Типичные ошибки (и как их избежать)
Ошибка #1: Забыли про кодировку
// ПЛОХО: результат зависит от культуры системы
byte[] badBytes = Encoding.Default.GetBytes("строка");
Решение: Всегда указывайте Encoding.UTF8 или другую явную кодировку.
Ошибка #2: Путаница между char и byte
// ПЛОХО: char это 2 байта, а не 1
byte singleByte = (byte)'Я'; // Потеря данных!
Решение: Используйте Encoding для корректного преобразования.
Ошибка #3: Неправильная кодировка при обратном преобразовании
byte[] bytes = Encoding.UTF8.GetBytes("Привет");
// ОШИБКА: пытаемся декодировать как ASCII
string broken = Encoding.ASCII.GetString(bytes); // "??????"
Решение: Кодировка при декодировании должна совпадать с той, что использовалась при кодировании.
Продвинутая техника: свой кастомный конвертер
Если вы часто работаете с разными кодировками, создайте утилитарный класс:
public static class StringByteConverter
{
public static byte[] ToBytes(this string str, Encoding encoding = null)
{
encoding ??= Encoding.UTF8;
return encoding.GetBytes(str ?? string.Empty);
}
public static string FromBytes(this byte[] bytes, Encoding encoding = null)
{
encoding ??= Encoding.UTF8;
return encoding.GetString(bytes ?? Array.Empty<byte>());
}
}
// Использование
string message = "Кодируем с запасом";
byte[] data = message.ToBytes(); // UTF-8 по умолчанию
string decoded = data.FromBytes(Encoding.Unicode); // О