Кодировка символов в .NET

В этой статье содержатся общие сведения о системах кодирования символов, которые используются в .NET. В этой статье описывается, как типы String, Char, Rune и StringInfo работают с кодировками Юникод, UTF-16 и UTF-8.

Термин символ используется здесь в общем смысле того, что читатель воспринимает как отдельный отображаемый элемент. Распространенными примерами являются буква "а", символ "@" и эмодзи "🐂". Иногда то, что выглядит как один символ, на самом деле состоит из нескольких независимых отображаемых элементов, как описано в разделе о кластерах графем.

Типы string и char

Экземпляр класса string представляет некоторый текст. Экземпляр string логически является последовательностью 16-разрядных значений, каждое из которых представляет собой экземпляр структуры char. Свойство string.Length возвращает количество экземпляров char в экземпляре string.

Следующий пример функции выводит значения в шестнадцатеричной нотации всех экземпляров char в string:

void PrintChars(string s)
{
    Console.WriteLine($"\"{s}\".Length = {s.Length}");
    for (int i = 0; i < s.Length; i++)
    {
        Console.WriteLine($"s[{i}] = '{s[i]}' ('\\u{(int)s[i]:x4}')");
    }
    Console.WriteLine();
}

Передайте string "Hello" в эту функцию, и вы получите следующие выходные данные:

PrintChars("Hello");
"Hello".Length = 5
s[0] = 'H' ('\u0048')
s[1] = 'e' ('\u0065')
s[2] = 'l' ('\u006c')
s[3] = 'l' ('\u006c')
s[4] = 'o' ('\u006f')

Каждый символ представлен одним значением char. Этот шаблон применяется для большинства языков мира. Например, вот выходные данные для двух китайских символов, которые звучат как nǐ hǎo и означают привет :

PrintChars("你好");
"你好".Length = 2
s[0] = '你' ('\u4f60')
s[1] = '好' ('\u597d')

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

PrintChars("𐓏𐓘𐓻𐓘𐓻𐓟 𐒻𐓟");
"𐓏𐓘𐓻𐓘𐓻𐓟 𐒻𐓟".Length = 17
s[0] = '�' ('\ud801')
s[1] = '�' ('\udccf')
s[2] = '�' ('\ud801')
s[3] = '�' ('\udcd8')
s[4] = '�' ('\ud801')
s[5] = '�' ('\udcfb')
s[6] = '�' ('\ud801')
s[7] = '�' ('\udcd8')
s[8] = '�' ('\ud801')
s[9] = '�' ('\udcfb')
s[10] = '�' ('\ud801')
s[11] = '�' ('\udcdf')
s[12] = ' ' ('\u0020')
s[13] = '�' ('\ud801')
s[14] = '�' ('\udcbb')
s[15] = '�' ('\ud801')
s[16] = '�' ('\udcdf')

В приведенном выше примере каждый символ, кроме пробела, представлен двумя экземплярами char.

Один эмодзи в Юникоде также представлен двумя экземплярами char, как показано в следующем примере с эмодзи вола:

"🐂".Length = 2
s[0] = '�' ('\ud83d')
s[1] = '�' ('\udc02')

В этих примерах показано, что значение string.Length, которое указывает количество экземпляров char, не обязательно указывает количество отображаемых символов. Один экземпляр char сам по себе не обязательно представляет один символ.

Пары char, которые сопоставляются с одним символом, называются суррогатными парами. Чтобы понять принцип их работы, вам нужно ознакомиться с кодировкой Юникод и UTF-16.

Кодовые точки Юникода

Юникод — это международный стандарт кодирования, используемый на различных платформах и с различными языками и скриптами.

Стандарт Юникода определяет более 1,1 миллиона кодовых точек. Кодовая точка — это целочисленное значение, которое может быть в диапазоне от 0 до U+10FFFF (десятичное число 1 114 111). Некоторые кодовые точки назначаются буквам, символам или эмодзи. Другие назначаются действиям, которые определяют способ отображения текста или символов, например переход на новую строку. Многие кодовые точки еще не назначены.

Вот несколько примеров назначения кодовых точек со ссылками на диаграммы Юникода, в которых они появляются:

Decimal Hex Пример Описание
10 U+000A Н/Д Перевод строки
97 U+0061 а Латинская строчная буква A
562 U+0232 Ȳ Латинская заглавная буква Y со знаком долготы
68 675 U+10C43 𐱃 Древнетюркская буква (язык орхоно-енисейских надписей)
127 801 U+1F339 🌹 Эмодзи "Роза"

Кодовые точки обычно определяются с использованием синтаксиса U+xxxx, где xxxx — это шестнадцатеричное целочисленное значение.

В пределах всего диапазона кодовых точек существует два поддиапазона:

  • Основная многоязыковая плоскость (BMP) в диапазоне U+0000..U+FFFF. Этот 16-разрядный диапазон предоставляет 65 536 кодовых точек. Их достаточно для охвата большинства мировых систем письма.
  • Дополнительные кодовые точки в диапазоне U+10000..U+10FFFF. Этот 21-разрядный диапазон предоставляет более миллиона дополнительных кодовых точек, которые можно использовать для менее известных языков и для других целей, таких как эмодзи.

На следующей схеме показана взаимосвязь между BMP и дополнительными кодовыми точками.

BMP and supplementary code points

Единицы кода UTF-16

16-разрядный формат преобразования Юникода ( UTF-16) — это система кодирования символов, которая использует 16-разрядные единицы кода для представления кодовых точек Юникода. .NET использует UTF-16 для кодирования текста в string. Экземпляр char представляет собой 16-разрядную единицу кода.

Одна 16-разрядная единица кода может представлять любую кодовую точку в 16-разрядном диапазоне основной многоязыковой плоскости. Однако для кодовой точки в дополнительном диапазоне необходимы два экземпляра char.

Суррогатные пары

Преобразование двух 16-разрядных значений в одно 21-разрядное значение обеспечивается специальным диапазоном, который называется суррогатными кодовыми точками , от U+D800 до U+DFFF (десятичное число от 55 296 до 57 343) включительно.

На следующей схеме показана взаимосвязь между BMP и суррогатными кодовыми точками.

BMP and surrogate code points

Когда за старшей заменяющей кодовой точкой (U+D800..U+DBFF) сразу же следует младшая заменяющая кодовая точка (U+DC00..U+DFFF), пара интерпретируется как дополнительная кодовая точка с помощью следующей формулы:

code point = 0x10000 +
  ((high surrogate code point - 0xD800) * 0x0400) +
  (low surrogate code point - 0xDC00)

Вот та же формула, но с использованием десятичной нотации:

code point = 65,536 +
  ((high surrogate code point - 55,296) * 1,024) +
  (low surrogate code point - 56,320)

Старшая заменяющая кодовая точка не имеет значения числа выше, чем младшая заменяющая кодовая точка. Старшая заменяющая кодовая точка называется "старшей", потому что она используется для вычисления 11 разрядов высшего порядка полного 21-разрядного диапазона кодовых точек. Младшая заменяющая кодовая точка используется для вычисления 10 разрядов низшего порядка.

Например, фактическая кодовая точка, которая соответствует суррогатной паре 0xD83C и 0xDF39, вычисляется следующим образом:

actual = 0x10000 + ((0xD83C - 0xD800) * 0x0400) + (0xDF39 - 0xDC00)
       = 0x10000 + (          0x003C  * 0x0400) +           0x0339
       = 0x10000 +                      0xF000  +           0x0339
       = 0x1F339

Вот тот же расчет, но с использованием десятичной нотации:

actual =  65,536 + ((55,356 - 55,296) * 1,024) + (57,145 - 56320)
       =  65,536 + (              60  * 1,024) +             825
       =  65,536 +                     61,440  +             825
       = 127,801

В предыдущем примере показано, что "\ud83c\udf39" является кодировкой UTF-16 кодовой точки U+1F339 ROSE ('🌹'), упомянутой ранее.

Скалярные значения Юникода

Термин скалярное значение Юникода относится ко всем кодовым точкам, кроме суррогатных. Другими словами, скалярное значение — это любая кодовая точка, которой присвоен символ или которой может быть присвоен символ в будущем. Слово "символ" здесь относится ко всему, что может быть назначено кодовой точке, включая действия, которые определяют способ отображения текста или символов.

На приведенной ниже схеме показаны точки кода скалярного значения.

Scalar values

Тип Rune как скалярное значение

Начиная с версии .NET Core 3.0, тип System.Text.Rune представляет скалярное значение Юникода. Тип Rune недоступен в .NET Core 2.x или .NET Framework 4.x.

Конструкторы Rune проверяют, является ли полученный экземпляр допустимым скалярным значением Юникода. В противном случае они создают исключение. В следующем примере показан код, который создает экземпляры Rune, так как входные данные представляют допустимые скалярные значения:

Rune a = new Rune('a');
Rune b = new Rune(0x0061);
Rune c = new Rune('\u0061');
Rune d = new Rune(0x10421);
Rune e = new Rune('\ud801', '\udc21');

В следующем примере создается исключение, так как кодовая точка находится в суррогатном диапазоне и не является частью суррогатной пары:

Rune f = new Rune('\ud801');

В следующем примере создается исключение, так как кодовая точка находится за пределами дополнительного диапазона:

Rune g = new Rune(0x12345678);

Пример использования Rune: изменение регистра букв

API, который принимает char и предполагает, что работает с кодовой точкой, которая является скалярным значением, работает неправильно, если char принадлежит суррогатной паре. Например, рассмотрим следующий метод, который вызывает Char.ToUpperInvariant для каждого экземпляра char в string:

// THE FOLLOWING METHOD SHOWS INCORRECT CODE.
// DO NOT DO THIS IN A PRODUCTION APPLICATION.
static string ConvertToUpperBadExample(string input)
{
    StringBuilder builder = new StringBuilder(input.Length);
    for (int i = 0; i < input.Length; i++) /* or 'foreach' */
    {
        builder.Append(char.ToUpperInvariant(input[i]));
    }
    return builder.ToString();
}

Если input string содержит строчную букву дезерет er (𐑉), этот код не преобразует ее в прописную букву (𐐡). Код вызывает char.ToUpperInvariant отдельно для каждой суррогатной кодовой точки U+D801 и U+DC49. Однако в самой кодовой точке U+D801 информации недостаточно, чтобы идентифицировать ее как строчную букву. Таким образом char.ToUpperInvariant оставляет ее как есть. И таким же образом обрабатывает U+DC49. В результате буква "𐑉" нижнего регистра в input string не преобразуется в букву "𐐡" верхнего регистра.

Вот два варианта правильного преобразования string в верхний регистр:

  • Вызовите String.ToUpperInvariant для входного экземпляра string, а не в итерации char-by-char. Метод string.ToUpperInvariant имеет доступ к обеим частям каждой суррогатной пары, поэтому он может правильно обрабатывать все кодовые точки Юникода.

  • Выполните итерацию скалярных значений Юникода в качестве экземпляров Rune, а не экземпляров char, как показано в следующем примере. Так как экземпляр Rune является допустимым скалярным значением Юникода, его можно передать в API-интерфейсы, которые должны работать со скалярным значением. Например, вызвав Rune.ToUpperInvariant, как показано в следующем примере, вы получите правильные результаты:

    static string ConvertToUpper(string input)
    {
        StringBuilder builder = new StringBuilder(input.Length);
        foreach (Rune rune in input.EnumerateRunes())
        {
            builder.Append(Rune.ToUpperInvariant(rune));
        }
        return builder.ToString();
    }
    

Другие API-интерфейсы Rune

Тип Rune предоставляет аналоги многих API-интерфейсов char. Например, приведенные ниже методы отражают статические API-интерфейсы для типа char:

Чтобы получить необработанное скалярное значение из экземпляра Rune, используйте свойство Rune.Value.

Чтобы преобразовать экземпляр Rune обратно в последовательность типов char, используйте метод Rune.ToString или Rune.EncodeToUtf16.

Так как любое скалярное значение Юникода может быть представлено одним экземпляром char или суррогатной парой, любой экземпляр Rune может быть представлен не более чем двумя экземплярами char. Используйте Rune.Utf16SequenceLength, чтобы узнать количество экземпляров char, требуемых для представления экземпляра Rune.

Дополнительные сведения о типе Rune .NET см. в справочнике по API для Rune.

Кластеры графем

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

Рассмотрим экземпляры string "a", "á", "á" и "👩🏽‍🚒". Если операционная система обрабатывает их в соответствии со стандартом Юникода, каждый из этих экземпляров string отображается в виде одного текстового элемента или кластера графем. Однако последние два представлены более чем одной кодовой точкой скалярного значения.

  • Экземпляр string "a" представлен одним скалярным значением и содержит один экземпляр char.

    • U+0061 LATIN SMALL LETTER A
  • Экземпляр string "á" представлен одним скалярным значением и содержит один экземпляр char.

    • U+00E1 LATIN SMALL LETTER A WITH ACUTE
  • Экземпляр string "á" выглядит так же, как "á", но представлен двумя скалярными значениями и содержит два экземпляра char.

    • U+0061 LATIN SMALL LETTER A
    • U+0301 COMBINING ACUTE ACCENT
  • Наконец, экземпляр string "👩🏽‍🚒" представлен четырьмя скалярными значениями и содержит семь экземпляров char.

    • U+1F469 WOMAN (дополнительный диапазон, требуется суррогатная пара);
    • U+1F3FD EMOJI MODIFIER FITZPATRICK TYPE-4 (дополнительный диапазон, требуется суррогатная пара);
    • U+200D ZERO WIDTH JOINER
    • U+1F692 FIRE ENGINE (дополнительный диапазон, требуется суррогатная пара).

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

Чтобы перечислить кластеры графем для string, используйте класс StringInfo, как показано в приведенном ниже примере. Если вы знакомы с Swift, тип StringInfo .NET концептуально похож на тип character Swift.

Пример: количество char, Rune и экземпляров текстовых элементов

В интерфейсах API .NET кластер графем называется текстовым элементом. Приведенный ниже метод демонстрирует различия между char, Rune и экземплярами текстового элемента в string:

static void PrintTextElementCount(string s)
{
    Console.WriteLine(s);
    Console.WriteLine($"Number of chars: {s.Length}");
    Console.WriteLine($"Number of runes: {s.EnumerateRunes().Count()}");

    TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(s);

    int textElementCount = 0;
    while (enumerator.MoveNext())
    {
        textElementCount++;
    }

    Console.WriteLine($"Number of text elements: {textElementCount}");
PrintTextElementCount("á");
// Number of chars: 1
// Number of runes: 1
// Number of text elements: 1

PrintTextElementCount("á");
// Number of chars: 2
// Number of runes: 2
// Number of text elements: 1

PrintTextElementCount("👩🏽‍🚒");
// Number of chars: 7
// Number of runes: 4
// Number of text elements: 1

Если вы запустите этот код в .NET Framework или .NET Core 3.1 или более ранней версии, для эмодзи отобразится 4 текстовых элемента. Это связано с ошибкой в классе StringInfo, которая исправлена в .NET 5.

Пример: разделение экземпляров string

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

// THE FOLLOWING METHOD SHOWS INCORRECT CODE.
// DO NOT DO THIS IN A PRODUCTION APPLICATION.
static string InsertNewlinesEveryTencharsBadExample(string input)
{
    StringBuilder builder = new StringBuilder();

    // First, append chunks in multiples of 10 chars
    // followed by a newline.
    int i = 0;
    for (; i < input.Length - 10; i += 10)
    {
        builder.Append(input, i, 10);
        builder.AppendLine(); // newline
    }

    // Then append any leftover data followed by
    // a final newline.
    builder.Append(input, i, input.Length - i);
    builder.AppendLine(); // newline

    return builder.ToString();
}

Так как этот код перечисляет экземпляры char, суррогатная пара, которая пересекает границу 10-char, будет разделена и между ними будет введена новая строка. Эта вставка представляет собой повреждение данных, так как суррогатные кодовые точки имеют смысл только как пары.

При перечислении экземпляров Rune (скалярные значения) вместо экземпляров char возможность повреждения данных не исключается. Набор экземпляров Rune может составлять кластер графем, который выходит за границу 10-char. Если набор кластеров графем разделен, он не может быть правильно интерпретирован.

Лучшим подходом является разделение string путем подсчета кластеров графем или текстовых элементов, как показано в приведенном ниже примере.

static string InsertNewlinesEveryTenTextElements(string input)
{
    StringBuilder builder = new StringBuilder();

    // Append chunks in multiples of 10 chars

    TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(input);

    int textElementCount = 0;
    while (enumerator.MoveNext())
    {
        builder.Append(enumerator.Current);
        if (textElementCount % 10 == 0 && textElementCount > 0)
        {
            builder.AppendLine(); // newline
        }
        textElementCount++;
    }

    // Add a final newline.
    builder.AppendLine(); // newline
    return builder.ToString();

}

Однако, как упоминалось ранее, в реализациях .NET, отличных от .NET 5, класс StringInfo может некорректно обрабатывать некоторые кластеры графем.

UTF-8 и UTF-32

В предыдущих разделах основное внимание уделялось UTF-16, потому что именно эту кодировку .NET использует для кодирования экземпляров string. Существуют и другие системы кодирования для Юникода: UTF-8 и UTF-32. Эти кодировки используют 8-разрядные и 32-разрядные единицы кода соответственно.

Как и в системе UTF-16, для UTF-8 требуется несколько единиц кода, чтобы предоставить некоторые скалярные значения Юникода. UTF-32 может представлять любое скалярное значение в одной 32-разрядной единице кода.

Ниже приведено несколько примеров, показывающих, как одна и та же кодовая точка Юникода представлена в каждой из этих трех систем кодирования Юникода.

Scalar: U+0061 LATIN SMALL LETTER A ('a')
UTF-8 : [ 61 ]           (1x  8-bit code unit  = 8 bits total)
UTF-16: [ 0061 ]         (1x 16-bit code unit  = 16 bits total)
UTF-32: [ 00000061 ]     (1x 32-bit code unit  = 32 bits total)

Scalar: U+0429 CYRILLIC CAPITAL LETTER SHCHA ('Щ')
UTF-8 : [ D0 A9 ]        (2x  8-bit code units = 16 bits total)
UTF-16: [ 0429 ]         (1x 16-bit code unit  = 16 bits total)
UTF-32: [ 00000429 ]     (1x 32-bit code unit  = 32 bits total)

Scalar: U+A992 JAVANESE LETTER GA ('ꦒ')
UTF-8 : [ EA A6 92 ]     (3x  8-bit code units = 24 bits total)
UTF-16: [ A992 ]         (1x 16-bit code unit  = 16 bits total)
UTF-32: [ 0000A992 ]     (1x 32-bit code unit  = 32 bits total)

Scalar: U+104CC OSAGE CAPITAL LETTER TSHA ('𐓌')
UTF-8 : [ F0 90 93 8C ]  (4x  8-bit code units = 32 bits total)
UTF-16: [ D801 DCCC ]    (2x 16-bit code units = 32 bits total)
UTF-32: [ 000104CC ]     (1x 32-bit code unit  = 32 bits total)

Как упоминалось ранее, одна единица кода UTF-16 из суррогатной пары сама по себе не имеет смысла. Таким же образом одна единица кода UTF-8 сама по себе не имеет смысла, если она находится в последовательности из двух, трех или четырех единиц, используемых для вычисления скалярных значений.

Порядок байтов

В .NET единицы кода UTF-16 string хранятся в непрерывной памяти в виде последовательности 16-разрядных целых чисел (экземпляров char). Разряды отдельных единиц кода размещаются в соответствии с порядком байтов текущей архитектуры.

В архитектуре с прямым порядком байтов экземпляр string, состоящий из кодовых точек UTF-16 [ D801 DCCC ], будет размещен в памяти в виде байтов [ 0x01, 0xD8, 0xCC, 0xDC ]. В архитектуре с обратным порядком байтов тот же экземпляр string будет размещен в памяти в виде байтов [ 0xD8, 0x01, 0xDC, 0xCC ].

Компьютерные системы, которые взаимодействуют друг с другом, должны согласовать представление передаваемых данных. Большинство сетевых протоколов используют систему UTF-8 в качестве стандарта при передаче текста, частично во избежание проблем, которые могут возникнуть из-за того, что компьютер с обратным порядком байтов взаимодействует с компьютером с прямым порядком байтов. Экземпляр string, состоящий из кодовых точек UTF-8 [ F0 90 93 8C ], всегда будет представлен в виде байтов [ 0xF0, 0x90, 0x93, 0x8C ], независимо от порядка байтов.

Чтобы использовать систему UTF-8 для передачи текста, приложения .NET часто применяют код, как в следующем примере:

string stringToWrite = GetString();
byte[] stringAsUtf8Bytes = Encoding.UTF8.GetBytes(stringToWrite);
await outputStream.WriteAsync(stringAsUtf8Bytes, 0, stringAsUtf8Bytes.Length);

В предыдущем примере метод Encoding.UTF8.GetBytes декодирует экземпляр stringUTF-16 обратно в ряд скалярных значений Юникода, затем он повторно кодирует эти скалярные значения в UTF-8 и помещает полученную последовательность в массив byte. Метод Encoding.UTF8.GetString выполняет обратное преобразование, преобразовывая массив byte UTF-8 в string UTF-16.

Предупреждение

Так как система UTF-8 очень часто используется в Интернете, очевидным решением может показаться считывать необработанные байты из сети и обрабатывать данные, как если бы это была система кодировки UTF-8. Однако вы должны проверить, что она действительно имеет правильный формат. Вредоносный клиент может отправить в службу неверно сформированную кодировку UTF-8. Если вы выполняете операции с этими данными так, как если бы они были правильно сформированы, это может вызвать ошибки или бреши в системе безопасности приложения. Чтобы проверить данные UTF-8, вы можете использовать метод, например Encoding.UTF8.GetString, который при преобразовании входящих данных в string будет выполнять проверку.

Кодирование с правильным форматом

Кодировка Юникод с правильным форматом — это экземпляр string кодовых единиц, который может быть однозначно и без ошибок декодирован в последовательность скалярных значений Юникода. Данные с правильным форматом могут быть свободно перекодированы между UTF-8, UTF-16 и UTF-32.

Вопрос в том, имеет ли последовательность кодирования правильный формат или нет, независимо от порядка байтов в архитектуре компьютера. Неверно сформированная последовательность UTF-8 имеет неправильный формат как на компьютерах с обратным порядком байтов, так и на компьютерах с прямым порядком байтов.

Вот несколько примеров кодировок с неправильным форматом:

  • В UTF-8 последовательность [ 6C C2 61 ] имеет неправильный формат, потому что за C2 не может следовать 61.

  • В UTF-16 последовательность [ DC00 DD00 ] (или, в C#, string "\udc00\udd00") имеет неправильный формат, потому что за младшей заменяющей кодовой точкой DC00 не может следовать другая младшая заменяющая точка DD00.

  • В UTF-32 последовательность [ 0011ABCD ] имеет неправильный формат, так как 0011ABCD находится вне диапазона скалярных значений Юникода.

В .NET экземпляры string почти всегда содержат данные UTF-16 с правильным форматом, но это не гарантировано. В следующих примерах показан допустимый код C#, который создает данные UTF-16 с неправильным форматом в экземплярах string.

  • Литерал с неправильным форматом:

    const string s = "\ud800";
    
  • Подстрока, которая разделяет суррогатную пару:

    string x = "\ud83e\udd70"; // "🥰"
    string y = x.Substring(1, 1); // "\udd70" standalone low surrogate
    

API-интерфейсы, такие как Encoding.UTF8.GetString, никогда не возвращают экземпляры string с неправильным форматом. Методы Encoding.GetString и Encoding.GetBytes обнаруживают последовательности с неправильным форматом во входных данных и выполняют замену символов при формировании выходных данных. Например, если для метода Encoding.ASCII.GetString(byte[]) во входных данных отображается байт, отличный от ASCII (вне диапазона U+0000–U+007F), он вставляет символ "?" в возвращенный экземпляр string. Метод Encoding.UTF8.GetString(byte[]) заменяет последовательности UTF-8 с неправильным форматом на U+FFFD REPLACEMENT CHARACTER ('�') в возвращаемом экземпляре string. Дополнительные сведения см. в разделах 5.22 и 3.9 стандарта Юникода.

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

byte[] utf8Bytes = ReadFromNetwork();
UTF8Encoding encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
string asString = encoding.GetString(utf8Bytes); // will throw if 'utf8Bytes' is ill-formed

Сведения о том, как использовать встроенные классы Encoding, см. в статье Кодировка символов в .NET.

См. также