Как получить согласованное байтовое представление строк в C # без указания кодировки вручную? - PullRequest
2058 голосов
/ 23 января 2009

Как преобразовать string в byte[] в .NET (C #) без указания конкретной кодировки вручную?

Я собираюсь зашифровать строку. Я могу зашифровать его без конвертации, но мне все равно хотелось бы знать, почему здесь играет роль кодировка.

Кроме того, почему следует учитывать кодировку? Разве я не могу просто получить, в каких байтах хранится строка? Почему существует зависимость от кодировки символов?

Ответы [ 38 ]

1795 голосов
/ 30 апреля 2012

Вопреки ответам здесь, вам не нужно беспокоиться о кодировке , если , байты не нужно интерпретировать!

Как вы упомянули, ваша цель состоит в том, чтобы просто "получить, какие байты строки были сохранены в" .
(И, конечно же, чтобы можно было восстановить строку из байтов.)

Для этих целей я, честно говоря, не понимаю, почему люди продолжают говорить вам, что вам нужны кодировки. Вам, конечно, не нужно беспокоиться о кодировках для этого.

Просто сделайте это вместо:

static byte[] GetBytes(string str)
{
    byte[] bytes = new byte[str.Length * sizeof(char)];
    System.Buffer.BlockCopy(str.ToCharArray(), 0, bytes, 0, bytes.Length);
    return bytes;
}

// Do NOT use on arbitrary bytes; only use on GetBytes's output on the SAME system
static string GetString(byte[] bytes)
{
    char[] chars = new char[bytes.Length / sizeof(char)];
    System.Buffer.BlockCopy(bytes, 0, chars, 0, bytes.Length);
    return new string(chars);
}

Пока ваша программа (или другие программы) не пытается интерпретировать байтов, что вы явно не упоминали, что намереваетесь это сделать, значит, ничего неправильно с этим подходом! Беспокойство по поводу кодировок просто усложняет вашу жизнь без всякой реальной причины.

Дополнительное преимущество для этого подхода:

Не имеет значения, содержит ли строка недопустимые символы, потому что вы все равно можете получить данные и восстановить исходную строку!

Он будет закодирован и декодирован точно так же, потому что вы просто смотрите на байты .

Однако, если вы используете определенную кодировку, это может привести к проблемам с кодированием / декодированием недопустимых символов.

1081 голосов
/ 23 января 2009

Это зависит от кодировки вашей строки ( ASCII , UTF-8 , ...).

Например:

byte[] b1 = System.Text.Encoding.UTF8.GetBytes (myString);
byte[] b2 = System.Text.Encoding.ASCII.GetBytes (myString);

Небольшой пример, почему кодирование имеет значение:

string pi = "\u03a0";
byte[] ascii = System.Text.Encoding.ASCII.GetBytes (pi);
byte[] utf8 = System.Text.Encoding.UTF8.GetBytes (pi);

Console.WriteLine (ascii.Length); //Will print 1
Console.WriteLine (utf8.Length); //Will print 2
Console.WriteLine (System.Text.Encoding.ASCII.GetString (ascii)); //Will print '?'

ASCII просто не приспособлен для работы со специальными символами.

Внутренне .NET Framework использует UTF-16 для представления строк, поэтому если вы просто хотите получить точные байты, которые использует .NET, используйте System.Text.Encoding.Unicode.GetBytes (...).

См. Кодировка символов в .NET Framework (MSDN) для получения дополнительной информации.

269 голосов
/ 30 апреля 2012

Принятый ответ очень и очень сложен. Используйте для этого включенные классы .NET:

const string data = "A string with international characters: Norwegian: ÆØÅæøå, Chinese: 喂 谢谢";
var bytes = System.Text.Encoding.UTF8.GetBytes(data);
var decoded = System.Text.Encoding.UTF8.GetString(bytes);

Не изобретай велосипед, если тебе не нужно ...

111 голосов
/ 23 января 2009
BinaryFormatter bf = new BinaryFormatter();
byte[] bytes;
MemoryStream ms = new MemoryStream();

string orig = "喂 Hello 谢谢 Thank You";
bf.Serialize(ms, orig);
ms.Seek(0, 0);
bytes = ms.ToArray();

MessageBox.Show("Original bytes Length: " + bytes.Length.ToString());

MessageBox.Show("Original string Length: " + orig.Length.ToString());

for (int i = 0; i < bytes.Length; ++i) bytes[i] ^= 168; // pseudo encrypt
for (int i = 0; i < bytes.Length; ++i) bytes[i] ^= 168; // pseudo decrypt

BinaryFormatter bfx = new BinaryFormatter();
MemoryStream msx = new MemoryStream();            
msx.Write(bytes, 0, bytes.Length);
msx.Seek(0, 0);
string sx = (string)bfx.Deserialize(msx);

MessageBox.Show("Still intact :" + sx);

MessageBox.Show("Deserialize string Length(still intact): " 
    + sx.Length.ToString());

BinaryFormatter bfy = new BinaryFormatter();
MemoryStream msy = new MemoryStream();
bfy.Serialize(msy, sx);
msy.Seek(0, 0);
byte[] bytesy = msy.ToArray();

MessageBox.Show("Deserialize bytes Length(still intact): " 
   + bytesy.Length.ToString());
89 голосов
/ 23 января 2009

Необходимо учитывать кодировку, поскольку 1 символ может быть представлен 1 или более байтами (примерно до 6), и разные кодировки будут обрабатывать эти байты по-разному.

У Джоэла есть сообщение на эту тему:

Абсолютный минимум каждый разработчик программного обеспечения Абсолютно, положительно должен знать о Unicode и наборах символов (без оправданий!)

85 голосов
/ 02 декабря 2013

Это популярный вопрос. Важно понимать, что задает автор вопроса, и что он отличается от того, что, вероятно, является наиболее распространенной потребностью. Чтобы воспрепятствовать неправильному использованию кода там, где он не нужен, я ответил первым позже.

Общая потребность

Каждая строка имеет набор символов и кодировку. Когда вы конвертируете объект System.String в массив System.Byte, у вас все еще есть набор символов и кодировка. В большинстве случаев вы знаете, какой набор символов и кодировку вам нужны, а .NET упрощает «копирование с преобразованием». Просто выберите соответствующий класс Encoding.

// using System.Text;
Encoding.UTF8.GetBytes(".NET String to byte array")

Преобразование может потребоваться для обработки случаев, когда целевой набор символов или кодировка не поддерживает символ, находящийся в источнике. У вас есть несколько вариантов: исключение, замена или пропуск. Политика по умолчанию заменяет «?».

// using System.Text;
var text = Encoding.ASCII.GetString(Encoding.ASCII.GetBytes("You win €100")); 
                                                      // -> "You win ?100"

Очевидно, что конверсии не обязательно без потерь!

Примечание. Для System.String исходным набором символов является Unicode.

Единственная запутанная вещь заключается в том, что .NET использует имя набора символов для имени одной конкретной кодировки этого набора символов. Encoding.Unicode должен называться Encoding.UTF16.

Вот и все для большинства случаев. Если это то, что вам нужно, прекратите читать здесь. См. Забавную статью Джоэла Спольски , если вы не понимаете, что такое кодировка.

Конкретная потребность

Теперь автор вопроса спрашивает: «Каждая строка хранится в виде массива байтов, верно? Почему я не могу просто иметь эти байты?»

Он не хочет никакого преобразования.

Из C # spec :

Обработка символов и строк в C # использует кодировку Unicode. Чарс тип представляет единицу кода UTF-16, а тип строки представляет последовательность кодовых единиц UTF-16.

Итак, мы знаем, что если мы запросим нулевое преобразование (т.е. из UTF-16 в UTF-16), мы получим желаемый результат:

Encoding.Unicode.GetBytes(".NET String to byte array")

Но чтобы избежать упоминания о кодировках, мы должны сделать это по-другому. Если промежуточный тип данных приемлем, для этого есть концептуальное сокращение:

".NET String to byte array".ToCharArray()

Это не дает нам желаемый тип данных, но Ответ Мехрдада показывает, как преобразовать этот массив Char в байтовый массив, используя BlockCopy . Тем не менее, это копирует строку дважды! И он слишком явно использует специфический для кодирования код: тип данных System.Char.

Единственный способ получить действительные байты, в которых хранится строка, - это использовать указатель. Оператор fixed позволяет получить адрес значений. Из спецификации C #:

[Для] выражения типа string ... инициализатор вычисляет адрес первого символа в строке.

Для этого компилятор пишет код, пропускающий другие части строкового объекта с помощью RuntimeHelpers.OffsetToStringData. Итак, чтобы получить необработанные байты, просто создайте указатель на строку и скопируйте необходимое количество байтов.

// using System.Runtime.InteropServices
unsafe byte[] GetRawBytes(String s)
{
    if (s == null) return null;
    var codeunitCount = s.Length;
    /* We know that String is a sequence of UTF-16 codeunits 
       and such codeunits are 2 bytes */
    var byteCount = codeunitCount * 2; 
    var bytes = new byte[byteCount];
    fixed(void* pRaw = s)
    {
        Marshal.Copy((IntPtr)pRaw, bytes, 0, byteCount);
    }
    return bytes;
}

Как указал @CodesInChaos, результат зависит от порядкового номера машины. Но автора вопроса это не касается.

44 голосов
/ 30 апреля 2012

Просто чтобы продемонстрировать, что звук Мехрадрада ответ работает, его подход может даже сохранить непарных суррогатных символов (из которых многие выровнялись против моего ответа, но в которых все одинаково виновны из, например, System.Text.Encoding.UTF8.GetBytes, System.Text.Encoding.Unicode.GetBytes, эти методы кодирования не могут сохранять, например, старшие суррогатные символы d800, а просто заменяют старшие суррогатные символы значением fffd):

using System;

class Program
{     
    static void Main(string[] args)
    {
        string t = "爱虫";            
        string s = "Test\ud800Test"; 

        byte[] dumpToBytes = GetBytes(s);
        string getItBack = GetString(dumpToBytes);

        foreach (char item in getItBack)
        {
            Console.WriteLine("{0} {1}", item, ((ushort)item).ToString("x"));
        }    
    }

    static byte[] GetBytes(string str)
    {
        byte[] bytes = new byte[str.Length * sizeof(char)];
        System.Buffer.BlockCopy(str.ToCharArray(), 0, bytes, 0, bytes.Length);
        return bytes;
    }

    static string GetString(byte[] bytes)
    {
        char[] chars = new char[bytes.Length / sizeof(char)];
        System.Buffer.BlockCopy(bytes, 0, chars, 0, bytes.Length);
        return new string(chars);
    }        
}

Выход:

T 54
e 65
s 73
t 74
? d800
T 54
e 65
s 73
t 74

Попробуйте это с System.Text.Encoding.UTF8.GetBytes или System.Text.Encoding.Unicode.GetBytes , они просто заменят старшие суррогатные символы значением FFFD

Каждый раз, когда в этом вопросе есть движение, я все еще думаю о сериализаторе (будь то от Microsoft или от стороннего компонента), который может сохранять строки, даже если он содержит непарные суррогатные символы; Я гуглю это время от времени: сериализация непарного суррогатного символа .NET . Это не заставляет меня терять сон, но это немного раздражает, когда время от времени кто-то комментирует мой ответ, что он некорректен, но его ответы одинаково несовершенны, когда речь идет о непарных суррогатных персонажах.

Черт, Microsoft должна была использовать System.Buffer.BlockCopy в своем BinaryFormatter

谢谢!

43 голосов
/ 23 января 2009

На первую часть вашего вопроса (как получить байты) уже отвечали другие: посмотрите в пространство имен System.Text.Encoding.

Я отвечу на ваш следующий вопрос: зачем вам выбирать кодировку? Почему вы не можете получить это из самого строкового класса?

Ответ состоит из двух частей.

Прежде всего, байты, используемые внутри класса строк , не имеют значения , и всякий раз, когда вы предполагаете, что они это делают, вы, вероятно, вносите ошибку.

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

С другой стороны, что если вы отправляете эти байты куда-то, что, как вы не можете гарантировать, извлечет данные из сериализованного потока .Net? В этом случае вам определенно нужно беспокоиться о кодировании, потому что, очевидно, эта внешняя система заботится. Итак, опять же, внутренние байты, используемые строкой, не имеют значения: вам нужно выбрать кодировку, чтобы вы могли четко указать эту кодировку на принимающей стороне, даже если это та же кодировка, которая используется внутри .Net.

Я понимаю, что в этом случае вы можете предпочесть использовать фактические байты, хранящиеся в переменной строки в памяти, где это возможно, с мыслью, что это может сэкономить некоторую работу, создавая ваш поток байтов. Тем не менее, я говорю вам, что это просто не важно по сравнению с тем, чтобы убедиться, что ваш вывод понят на другом конце, и чтобы гарантировать, что вы должны быть явным с вашей кодировкой. Кроме того, если вы действительно хотите сопоставить свои внутренние байты, вы уже можете просто выбрать кодировку Unicode и получить это снижение производительности.

Что подводит меня ко второй части ... выбор кодировки Unicode означает , говорящую .Net использовать базовые байты. Вам нужно выбрать эту кодировку, потому что, когда выходит какой-то новый Unicode-Plus, среда выполнения .Net должна быть свободна, чтобы использовать эту более новую, лучшую модель кодирования, не ломая вашу программу. Но на данный момент (и в обозримом будущем), просто выбрав кодировку Unicode, вы получите то, что вам нужно.

Также важно понимать, что ваша строка должна быть перезаписана на провод, и это включает в себя по крайней мере некоторый перевод битового шаблона , даже если вы используете соответствующую кодировку . Компьютер должен учитывать такие вещи, как Big против Little Endian, порядок байтов в сети, пакетирование, информация о сеансе и т. Д.

39 голосов
/ 26 июля 2011

Попробуйте, намного меньше кода:

System.Text.Encoding.UTF8.GetBytes("TEST String");
25 голосов
/ 10 марта 2011

Хорошо, я прочитал все ответы, и они были об использовании кодировки или о сериализации, которая отбрасывает непарные суррогаты.

Плохо, когда строка, например, взята из SQL Server , где она была построена из байтового массива, хранящего, например, хэш пароля. Если мы отбросим что-либо из него, он сохранит недопустимый хеш, а если мы хотим сохранить его в XML, мы хотим оставить его нетронутым (поскольку средство записи XML удаляет исключение для любого найденного непарного суррогата).

Поэтому я использую Base64 кодирование байтовых массивов в таких случаях, но, эй, в Интернете есть только одно решение этой проблемы в C #, и в нем есть ошибка, и это только один способ, поэтому Я исправил ошибку и написал процедуру возврата. Вот вы, гуглеры будущего:

public static byte[] StringToBytes(string str)
{
    byte[] data = new byte[str.Length * 2];
    for (int i = 0; i < str.Length; ++i)
    {
        char ch = str[i];
        data[i * 2] = (byte)(ch & 0xFF);
        data[i * 2 + 1] = (byte)((ch & 0xFF00) >> 8);
    }

    return data;
}

public static string StringFromBytes(byte[] arr)
{
    char[] ch = new char[arr.Length / 2];
    for (int i = 0; i < ch.Length; ++i)
    {
        ch[i] = (char)((int)arr[i * 2] + (((int)arr[i * 2 + 1]) << 8));
    }
    return new String(ch);
}
...