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

В этой статье содержатся общие сведения о системах кодирования символов, которые используются в .NET.This article provides an introduction to character encoding systems that are used by .NET. В этой статье описывается, как типы String, Char, Rune и StringInfo работают с кодировками Юникод, UTF-16 и UTF-8.The article explains how the String, Char, Rune, and StringInfo types work with Unicode, UTF-16, and UTF-8.

Термин символ используется здесь в общем смысле того, что читатель воспринимает как отдельный отображаемый элемент.The term character is used here in the general sense of what a reader perceives as a single display element. Распространенными примерами являются буква "а", символ "@" и эмодзи "🐂".Common examples are the letter "a", the symbol "@", and the emoji "🐂". Иногда то, что выглядит как один символ, на самом деле состоит из нескольких независимых отображаемых элементов, как описано в разделе о кластерах графем.Sometimes what looks like one character is actually composed of multiple independent display elements, as the section on grapheme clusters explains.

Типы string и charThe string and char types

Экземпляр класса string представляет некоторый текст.An instance of the string class represents some text. Экземпляр string логически является последовательностью 16-разрядных значений, каждое из которых представляет собой экземпляр структуры char.A string is logically a sequence of 16-bit values, each of which is an instance of the char struct. Свойство string.Length возвращает количество экземпляров char в экземпляре string.The string.Length property returns the number of char instances in the string instance.

Следующий пример функции выводит значения в шестнадцатеричной нотации всех экземпляров char в string:The following sample function prints out the values in hexadecimal notation of all the char instances in a 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" в эту функцию, и вы получите следующие выходные данные:Pass the string "Hello" to this function, and you get the following output:

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.Each character is represented by a single char value. Этот шаблон применяется для большинства языков мира.That pattern holds true for most of the world's languages. Например, вот выходные данные для двух китайских символов, которые звучат как nǐ hǎo и означают Hello:For example, here's the output for two Chinese characters that sound like nǐ hǎo and mean Hello:

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

Однако для некоторых языков, символов и эмодзи, чтобы представить один символ, потребуется два экземпляра char.However, for some languages and for some symbols and emoji, it takes two char instances to represent a single character. Например, сравните символы и экземпляры char в слове, которое означает Osage на языке осейдж:For example, compare the characters and char instances in the word that means Osage in the Osage language:

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.In the preceding example, each character except the space is represented by two char instances.

Один эмодзи в Юникоде также представлен двумя экземплярами char, как показано в следующем примере с эмодзи вола:A single Unicode emoji is also represented by two chars, as seen in the following example showing an ox emoji:

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

В этих примерах показано, что значение string.Length, которое указывает количество экземпляров char, не обязательно указывает количество отображаемых символов.These examples show that the value of string.Length, which indicates the number of char instances, doesn't necessarily indicate the number of displayed characters. Один экземпляр char сам по себе не обязательно представляет символ.A single char instance by itself doesn't necessarily represent a character.

Пары char, которые сопоставляются с одним символом, называются суррогатными парами.The char pairs that map to a single character are called surrogate pairs. Чтобы понять принцип их работы, вам нужно ознакомиться с кодировкой Юникод и UTF-16.To understand how they work, you need to understand Unicode and UTF-16 encoding.

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

Юникод — это международный стандарт кодирования, используемый на различных платформах и с различными языками и скриптами.Unicode is an international encoding standard for use on various platforms and with various languages and scripts.

Стандарт Юникода определяет более 1,1 миллиона кодовых точек.The Unicode Standard defines over 1.1 million code points. Кодовая точка — это целочисленное значение, которое может быть в диапазоне от 0 до U+10FFFF (десятичное число 1 114 111).A code point is an integer value that can range from 0 to U+10FFFF (decimal 1,114,111). Некоторые кодовые точки назначаются буквам, символам или эмодзи.Some code points are assigned to letters, symbols, or emoji. Другие назначаются действиям, которые определяют способ отображения текста или символов, например переход на новую строку.Others are assigned to actions that control how text or characters are displayed, such as advance to a new line. Многие кодовые точки еще не назначены.Many code points are not yet assigned.

Вот несколько примеров назначения кодовых точек со ссылками на диаграммы Юникода, в которых они появляются:Here are some examples of code point assignments, with links to Unicode charts in which they appear:

DecimalDecimal HexHex ПримерExample ОписаниеDescription
1010 U+000A Н/ДN/A Перевод строкиLINE FEED
6565 U+0061 аa Латинская строчная буква ALATIN SMALL LETTER A
562562 U+0232 ȲȲ Латинская заглавная буква Y со знаком долготыLATIN CAPITAL LETTER Y WITH MACRON
68 67568,675 U+10C43 𐱃𐱃 Древнетюркская буква (язык орхоно-енисейских надписей)OLD TURKIC LETTER ORKHON AT
127 801127,801 U+1F339 🌹🌹 Эмодзи "Роза"ROSE emoji

Кодовые точки обычно определяются с использованием синтаксиса U+xxxx, где xxxx — это шестнадцатеричное целочисленное значение.Code points are customarily referred to by using the syntax U+xxxx, where xxxx is the hex-encoded integer value.

В пределах всего диапазона кодовых точек существует два поддиапазона:Within the full range of code points there are two subranges:

  • Основная многоязыковая плоскость (BMP) в диапазоне U+0000..U+FFFF.The Basic Multilingual Plane (BMP) in the range U+0000..U+FFFF. Этот 16-разрядный диапазон предоставляет 65 536 кодовых точек. Их достаточно для охвата большинства мировых систем письма.This 16-bit range provides 65,536 code points, enough to cover the majority of the world's writing systems.
  • Дополнительные кодовые точки в диапазоне U+10000..U+10FFFF.Supplementary code points in the range U+10000..U+10FFFF. Этот 21-разрядный диапазон предоставляет более миллиона дополнительных кодовых точек, которые можно использовать для менее известных языков и для других целей, таких как эмодзи.This 21-bit range provides more than a million additional code points that can be used for less well-known languages and other purposes such as emojis.

На следующей схеме показана взаимосвязь между BMP и дополнительными кодовыми точками.The following diagram illustrates the relationship between the BMP and the supplementary code points.

BMP и дополнительные кодовые точки

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

16-разрядный формат преобразования Юникода (UTF-16) — это система кодирования символов, которая использует 16-разрядные единицы кода для представления кодовых точек Юникода.16-bit Unicode Transformation Format (UTF-16) is a character encoding system that uses 16-bit code units to represent Unicode code points. .NET использует UTF-16 для кодирования текста в string..NET uses UTF-16 to encode the text in a string. Экземпляр char представляет собой 16-разрядную единицу кода.A char instance represents a 16-bit code unit.

Одна 16-разрядная единица кода может представлять любую кодовую точку в 16-разрядном диапазоне основной многоязыковой плоскости.A single 16-bit code unit can represent any code point in the 16-bit range of the Basic Multilingual Plane. Однако для кодовой точки в дополнительном диапазоне необходимы два экземпляра char.But for a code point in the supplementary range, two char instances are needed.

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

Преобразование двух 16-разрядных значений в одно 21-разрядное значение обеспечивается специальным диапазоном, который называется суррогатными кодовыми точками, от U+D800 до U+DFFF (десятичное число от 55 296 до 57 343) включительно.The translation of two 16-bit values to a single 21-bit value is facilitated by a special range called the surrogate code points, from U+D800 to U+DFFF (decimal 55,296 to 57,343), inclusive.

На следующей схеме показана взаимосвязь между BMP и суррогатными кодовыми точками.The following diagram illustrates the relationship between the BMP and the surrogate code points.

BMP и суррогатные кодовые точки

Когда за старшей заменяющей кодовой точкой (U+D800..U+DBFF) сразу же следует младшая заменяющая кодовая точка (U+DC00..U+DFFF), пара интерпретируется как дополнительная кодовая точка с помощью следующей формулы:When a high surrogate code point (U+D800..U+DBFF) is immediately followed by a low surrogate code point (U+DC00..U+DFFF), the pair is interpreted as a supplementary code point by using the following formula:

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

Вот та же формула, но с использованием десятичной нотации:Here's the same formula using decimal notation:

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

Старшая заменяющая кодовая точка не имеет значения числа выше, чем младшая заменяющая кодовая точка.A high surrogate code point doesn't have a higher number value than a low surrogate code point. Старшая заменяющая кодовая точка называется "старшей", потому что она используется для вычисления 11 разрядов высшего порядка полного 21-разрядного диапазона кодовых точек.The high surrogate code point is called "high" because it's used to calculate the higher-order 11 bits of the full 21-bit code point range. Младшая заменяющая кодовая точка используется для вычисления 10 разрядов низшего порядка.The low surrogate code point is used to calculate the lower-order 10 bits.

Например, фактическая кодовая точка, которая соответствует суррогатной паре 0xD83C и 0xDF39, вычисляется следующим образом:For example, the actual code point that corresponds to the surrogate pair 0xD83C and 0xDF39 is computed as follows:

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

Вот тот же расчет, но с использованием десятичной нотации:Here's the same calculation using decimal notation:

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 ('🌹'), упомянутой ранее.The preceding example demonstrates that "\ud83c\udf39" is the UTF-16 encoding of the U+1F339 ROSE ('🌹') code point mentioned earlier.

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

Термин скалярное значение Юникода относится ко всем кодовым точкам, кроме суррогатных.The term Unicode scalar value refers to all code points other than the surrogate code points. Другими словами, скалярное значение — это любая кодовая точка, которой присвоен символ или которой может быть присвоен символ в будущем.In other words, a scalar value is any code point that is assigned a character or can be assigned a character in the future. Слово "символ" здесь относится ко всему, что может быть назначено кодовой точке, включая действия, которые определяют способ отображения текста или символов."Character" here refers to anything that can be assigned to a code point, which includes such things as actions that control how text or characters are displayed.

На приведенной ниже схеме показаны точки кода скалярного значения.The following diagram illustrates the scalar value code points.

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

Тип Rune как скалярное значениеThe Rune type as a scalar value

Начиная с версии .NET Core 3.0, тип System.Text.Rune представляет скалярное значение Юникода.Beginning with .NET Core 3.0, the System.Text.Rune type represents a Unicode scalar value. Тип Rune недоступен в .NET Core 2.x или .NET Framework 4.x.Rune is not available in .NET Core 2.x or .NET Framework 4.x.

Конструкторы Rune проверяют, является ли полученный экземпляр допустимым скалярным значением Юникода. В противном случае они создают исключение.The Rune constructors validate that the resulting instance is a valid Unicode scalar value, otherwise they throw an exception. В следующем примере показан код, который создает экземпляры Rune, так как входные данные представляют допустимые скалярные значения:The following example shows code that successfully instantiates Rune instances because the input represents valid scalar values:

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');

В следующем примере создается исключение, так как кодовая точка находится в суррогатном диапазоне и не является частью суррогатной пары:The following example throws an exception because the code point is in the surrogate range and isn't part of a surrogate pair:

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

В следующем примере создается исключение, так как кодовая точка находится за пределами дополнительного диапазона:The following example throws an exception because the code point is beyond the supplementary range:

Rune g = new Rune(0x12345678);

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

API, который принимает char и предполагает, что работает с кодовой точкой, которая является скалярным значением, работает неправильно, если char принадлежит суррогатной паре.An API that takes a char and assumes it is working with a code point that is a scalar value doesn't work correctly if the char is from a surrogate pair. Например, рассмотрим следующий метод, который вызывает Char.ToUpperInvariant для каждого экземпляра char в string:For example, consider the following method that calls Char.ToUpperInvariant on each char in a 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 (𐑉), этот код не преобразует ее в прописную букву (𐐡).If the input string contains the lowercase Deseret letter er (𐑉), this code won't convert it to uppercase (𐐡). Код вызывает char.ToUpperInvariant отдельно для каждой суррогатной кодовой точки U+D801 и U+DC49.The code calls char.ToUpperInvariant separately on each surrogate code point, U+D801 and U+DC49. Однако в самой кодовой точке U+D801 информации недостаточно, чтобы идентифицировать ее как строчную букву. Таким образом char.ToUpperInvariant оставляет ее как есть.But U+D801 doesn't have enough information by itself to identify it as a lowercase letter, so char.ToUpperInvariant leaves it alone. И таким же образом обрабатывает U+DC49.And it handles U+DC49 the same way. В результате буква "𐑉" нижнего регистра в input string не преобразуется в букву "𐐡" верхнего регистра.The result is that lowercase '𐑉' in the input string doesn't get converted to uppercase '𐐡'.

Вот два варианта правильного преобразования string в верхний регистр:Here are two options for correctly converting a string to uppercase:

  • Вызовите String.ToUpperInvariant для входного экземпляра string, а не в итерации char-by-char.Call String.ToUpperInvariant on the input string rather than iterating char-by-char. Метод string.ToUpperInvariant имеет доступ к обеим частям каждой суррогатной пары, поэтому он может правильно обрабатывать все кодовые точки Юникода.The string.ToUpperInvariant method has access to both parts of each surrogate pair, so it can handle all Unicode code points correctly.

  • Выполните итерацию скалярных значений Юникода в качестве экземпляров Rune, а не экземпляров char, как показано в следующем примере.Iterate through the Unicode scalar values as Rune instances instead of char instances, as shown in the following example. Так как экземпляр Rune является допустимым скалярным значением Юникода, его можно передать в API-интерфейсы, которые должны работать со скалярным значением.Since a Rune instance is a valid Unicode scalar value, it can be passed to APIs that expect to operate on a scalar value. Например, вызвав Rune.ToUpperInvariant, как показано в следующем примере, вы получите правильные результаты:For example, calling Rune.ToUpperInvariant as shown in the following example gives correct results:

    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-интерфейсы RuneOther Rune APIs

Тип Rune предоставляет аналоги многих API-интерфейсов char.The Rune type exposes analogs of many of the char APIs. Например, приведенные ниже методы отражают статические API-интерфейсы для типа char:For example, the following methods mirror static APIs on the char type:

Чтобы получить необработанное скалярное значение из экземпляра Rune, используйте свойство Rune.Value.To get the raw scalar value from a Rune instance, use the Rune.Value property.

Чтобы преобразовать экземпляр Rune обратно в последовательность типов char, используйте метод Rune.ToString или Rune.EncodeToUtf16.To convert a Rune instance back to a sequence of chars, use Rune.ToString or the Rune.EncodeToUtf16 method.

Так как любое скалярное значение Юникода может быть представлено одним экземпляром char или суррогатной парой, любой экземпляр Rune может быть представлен не более чем двумя экземплярами char.Since any Unicode scalar value is representable by a single char or by a surrogate pair, any Rune instance can be represented by at most 2 char instances. Используйте Rune.Utf16SequenceLength, чтобы узнать количество экземпляров char, требуемых для представления экземпляра Rune.Use Rune.Utf16SequenceLength to see how many char instances are required to represent a Rune instance.

Дополнительные сведения о типе Rune .NET см. в справочнике по API для Rune.For more information about the .NET Rune type, see the Rune API reference.

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

То, что выглядит как один символ, может быть результатом комбинации нескольких кодовых точек. Таким образом, более описательным термином, который часто используется вместо термина "символ", является кластер графем.What looks like one character might result from a combination of multiple code points, so a more descriptive term that is often used in place of "character" is grapheme cluster. В .NET эквивалентным термином является текстовый элемент.The equivalent term in .NET is text element.

Рассмотрим экземпляры string "а", "á".Consider the string instances "a", "á". "á" и "👩🏽‍🚒"."á", and "👩🏽‍🚒". Если операционная система обрабатывает их в соответствии со стандартом Юникода, каждый из этих экземпляров string отображается в виде одного текстового элемента или кластера графем.If your operating system handles them as specified by the Unicode standard, each of these string instances appears as a single text element or grapheme cluster. Однако последние два представлены более чем одной кодовой точкой скалярного значения.But the last two are represented by more than one scalar value code point.

  • Экземпляр string "a" представлен одним скалярным значением и содержит один экземпляр char.The string "a" is represented by one scalar value and contains one char instance.

    • U+0061 LATIN SMALL LETTER A
  • Экземпляр string "á" представлен одним скалярным значением и содержит один экземпляр char.The string "á" is represented by one scalar value and contains one char instance.

    • U+00E1 LATIN SMALL LETTER A WITH ACUTE
  • Экземпляр string "á" выглядит так же, как "á", но представлен двумя скалярными значениями и содержит два экземпляра char.The string "á" looks the same as "á" but is represented by two scalar values and contains two char instances.

    • U+0061 LATIN SMALL LETTER A
    • U+0301 COMBINING ACUTE ACCENT
  • Наконец, экземпляр string "👩🏽‍🚒" представлен четырьмя скалярными значениями и содержит семь экземпляров char.Finally, the string "👩🏽‍🚒" is represented by four scalar values and contains seven char instances.

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

В некоторых из приведенных выше примерах, таких как комбинированный модификатор диакритических знаков или модификатор тона кожи, кодовая точка не отображается как отдельный элемент на экране.In some of the preceding examples - such as the combining accent modifier or the skin tone modifier - the code point does not display as a standalone element on the screen. Вместо этого она служит для изменения внешнего вида текстового элемента, который был до него.Rather, it serves to modify the appearance of a text element that came before it. В этих примерах показано, что может потребоваться несколько скалярных значений, чтобы составить один "символ" или "кластер графем".These examples show that it might take multiple scalar values to make up what we think of as a single "character," or "grapheme cluster."

Чтобы перечислить кластеры графем для string, используйте класс StringInfo, как показано в приведенном ниже примере.To enumerate the grapheme clusters of a string, use the StringInfo class as shown in the following example. Если вы знакомы с Swift, тип StringInfo .NET концептуально похож на тип character Swift.If you're familiar with Swift, the .NET StringInfo type is conceptually similar to Swift's character type.

Пример: количество char, Rune и экземпляров текстовых элементовExample: count char, Rune, and text element instances

В интерфейсах API .NET кластер графем называется текстовым элементом.In .NET APIs, a grapheme cluster is called a text element. Приведенный ниже метод демонстрирует различия между char, Rune и экземплярами текстового элемента в string:The following method demonstrates the differences between char, Rune, and text element instances in a 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 текстовых элемента.If you run this code in .NET Framework or .NET Core 3.1 or earlier, the text element count for the emoji shows 4. Это связано с ошибкой в классе StringInfo, которая исправлена в .NET 5.That is due to a bug in the StringInfo class that is fixed in .NET 5.

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

При разделении экземпляров string не разделяйте суррогатные пары и кластеры графем.When splitting string instances, avoid splitting surrogate pairs and grapheme clusters. Рассмотрим приведенный ниже пример неправильного кода, который будет вставлять разрывы строк через каждые 10 символов в string.Consider the following example of incorrect code, which intends to insert line breaks every 10 characters in a 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, будет разделена и между ними будет введена новая строка.Because this code enumerates char instances, a surrogate pair that happens to straddle a 10-char boundary will be split and a newline injected between them. Эта вставка представляет собой повреждение данных, так как суррогатные кодовые точки имеют смысл только как пары.This insertion introduces data corruption, because surrogate code points are meaningful only as pairs.

При перечислении экземпляров Rune (скалярные значения) вместо экземпляров char возможность повреждения данных не исключается.The potential for data corruption isn't eliminated if you enumerate Rune instances (scalar values) instead of char instances. Набор экземпляров Rune может составлять кластер графем, который выходит за границу 10-char.A set of Rune instances might make up a grapheme cluster that straddles a 10-char boundary. Если набор кластеров графем разделен, он не может быть правильно интерпретирован.If the grapheme cluster set is split up, it can't be interpreted correctly.

Лучшим подходом является разделение string путем подсчета кластеров графем или текстовых элементов, как показано в приведенном ниже примере.A better approach is to break the string by counting grapheme clusters, or text elements, as in the following example:

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 может некорректно обрабатывать некоторые кластеры графем.As noted earlier, however, in implementations of .NET other than .NET 5, the StringInfo class might handle some grapheme clusters incorrectly.

UTF-8 и UTF-32UTF-8 and UTF-32

В предыдущих разделах основное внимание уделялось UTF-16, потому что именно эту кодировку .NET использует для кодирования экземпляров string.The preceding sections focused on UTF-16 because that's what .NET uses to encode string instances. Существуют и другие системы кодирования для Юникода: UTF-8 и UTF-32.There are other encoding systems for Unicode - UTF-8 and UTF-32. Эти кодировки используют 8-разрядные и 32-разрядные единицы кода соответственно.These encodings use 8-bit code units and 32-bit code units, respectively.

Как и в системе UTF-16, для UTF-8 требуется несколько единиц кода, чтобы предоставить некоторые скалярные значения Юникода.Like UTF-16, UTF-8 requires multiple code units to represent some Unicode scalar values. UTF-32 может представлять любое скалярное значение в одной 32-разрядной единице кода.UTF-32 can represent any scalar value in a single 32-bit code unit.

Ниже приведено несколько примеров, показывающих, как одна и та же кодовая точка Юникода представлена в каждой из этих трех систем кодирования Юникода.Here are some examples showing how the same Unicode code point is represented in each of these three Unicode encoding systems:

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 из суррогатной пары сама по себе не имеет смысла.As noted earlier, a single UTF-16 code unit from a surrogate pair is meaningless by itself. Таким же образом одна единица кода UTF-8 сама по себе не имеет смысла, если она находится в последовательности из двух, трех или четырех единиц, используемых для вычисления скалярных значений.In the same way, a single UTF-8 code unit is meaningless by itself if it's in a sequence of two, three, or four used to calculate a scalar value.

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

В .NET единицы кода UTF-16 string хранятся в непрерывной памяти в виде последовательности 16-разрядных целых чисел (экземпляров char).In .NET, the UTF-16 code units of a string are stored in contiguous memory as a sequence of 16-bit integers (char instances). Разряды отдельных единиц кода размещаются в соответствии с порядком байтов текущей архитектуры.The bits of individual code units are laid out according to the endianness of the current architecture.

В архитектуре с прямым порядком байтов экземпляр string, состоящий из кодовых точек UTF-16 [ D801 DCCC ], будет размещен в памяти в виде байтов [ 0x01, 0xD8, 0xCC, 0xDC ].On a little-endian architecture, the string consisting of the UTF-16 code points [ D801 DCCC ] would be laid out in memory as the bytes [ 0x01, 0xD8, 0xCC, 0xDC ]. В архитектуре с обратным порядком байтов тот же экземпляр string будет размещен в памяти в виде байтов [ 0xD8, 0x01, 0xDC, 0xCC ].On a big-endian architecture that same string would be laid out in memory as the bytes [ 0xD8, 0x01, 0xDC, 0xCC ].

Компьютерные системы, которые взаимодействуют друг с другом, должны согласовать представление передаваемых данных.Computer systems that communicate with each other must agree on the representation of data crossing the wire. Большинство сетевых протоколов используют систему UTF-8 в качестве стандарта при передаче текста, частично во избежание проблем, которые могут возникнуть из-за того, что компьютер с обратным порядком байтов взаимодействует с компьютером с прямым порядком байтов.Most network protocols use UTF-8 as a standard when transmitting text, partly to avoid issues that might result from a big-endian machine communicating with a little-endian machine. Экземпляр string, состоящий из кодовых точек UTF-8 [ F0 90 93 8C ], всегда будет представлен в виде байтов [ 0xF0, 0x90, 0x93, 0x8C ], независимо от порядка байтов.The string consisting of the UTF-8 code points [ F0 90 93 8C ] will always be represented as the bytes [ 0xF0, 0x90, 0x93, 0x8C ] regardless of endianness.

Чтобы использовать систему UTF-8 для передачи текста, приложения .NET часто применяют код, как в следующем примере:To use UTF-8 for transmitting text, .NET applications often use code like the following example:

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

В предыдущем примере метод Encoding.UTF8.GetBytes декодирует экземпляр stringUTF-16 обратно в ряд скалярных значений Юникода, затем он повторно кодирует эти скалярные значения в UTF-8 и помещает полученную последовательность в массив byte.In the preceding example, the method Encoding.UTF8.GetBytes decodes the UTF-16 string back into a series of Unicode scalar values, then it re-encodes those scalar values into UTF-8 and places the resulting sequence into a byte array. Метод Encoding.UTF8.GetString выполняет обратное преобразование, преобразовывая массив byte UTF-8 в string UTF-16.The method Encoding.UTF8.GetString performs the opposite transformation, converting a UTF-8 byte array to a UTF-16 string.

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

Так как система UTF-8 очень часто используется в Интернете, очевидным решением может показаться считывать необработанные байты из сети и обрабатывать данные, как если бы это была система кодировки UTF-8.Since UTF-8 is commonplace on the internet, it may be tempting to read raw bytes from the wire and to treat the data as if it were UTF-8. Однако вы должны проверить, что она действительно имеет правильный формат.However, you should validate that it is indeed well-formed. Вредоносный клиент может отправить в службу неверно сформированную кодировку UTF-8.A malicious client might submit ill-formed UTF-8 to your service. Если вы выполняете операции с этими данными так, как если бы они были правильно сформированы, это может вызвать ошибки или бреши в системе безопасности приложения.If you operate on that data as if it were well-formed, it could cause errors or security holes in your application. Чтобы проверить данные UTF-8, вы можете использовать метод, например Encoding.UTF8.GetString, который при преобразовании входящих данных в string будет выполнять проверку.To validate UTF-8 data, you can use a method like Encoding.UTF8.GetString, which will perform validation while converting the incoming data to a string.

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

Кодировка Юникод с правильным форматом — это экземпляр string кодовых единиц, который может быть однозначно и без ошибок декодирован в последовательность скалярных значений Юникода.A well-formed Unicode encoding is a string of code units that can be decoded unambiguously and without error into a sequence of Unicode scalar values. Данные с правильным форматом могут быть свободно перекодированы между UTF-8, UTF-16 и UTF-32.Well-formed data can be transcoded freely back and forth between UTF-8, UTF-16, and UTF-32.

Вопрос в том, имеет ли последовательность кодирования правильный формат или нет, независимо от порядка байтов в архитектуре компьютера.The question of whether an encoding sequence is well-formed or not is unrelated to the endianness of a machine's architecture. Неверно сформированная последовательность UTF-8 имеет неправильный формат как на компьютерах с обратным порядком байтов, так и на компьютерах с прямым порядком байтов.An ill-formed UTF-8 sequence is ill-formed in the same way on both big-endian and little-endian machines.

Вот несколько примеров кодировок с неправильным форматом:Here are some examples of ill-formed encodings:

  • В UTF-8 последовательность [ 6C C2 61 ] имеет неправильный формат, потому что за C2 не может следовать 61.In UTF-8, the sequence [ 6C C2 61 ] is ill-formed because C2 cannot be followed by 61.

  • В UTF-16 последовательность [ DC00 DD00 ] (или, в C#, string "\udc00\udd00") имеет неправильный формат, потому что за младшей заменяющей кодовой точкой DC00 не может следовать другая младшая заменяющая точка DD00.In UTF-16, the sequence [ DC00 DD00 ] (or, in C#, the string "\udc00\udd00") is ill-formed because the low surrogate DC00 cannot be followed by another low surrogate DD00.

  • В UTF-32 последовательность [ 0011ABCD ] имеет неправильный формат, так как 0011ABCD находится вне диапазона скалярных значений Юникода.In UTF-32, the sequence [ 0011ABCD ] is ill-formed because 0011ABCD is outside the range of Unicode scalar values.

В .NET экземпляры string почти всегда содержат данные UTF-16 с правильным форматом, но это не гарантировано.In .NET, string instances almost always contain well-formed UTF-16 data, but that isn't guaranteed. В следующих примерах показан допустимый код C#, который создает данные UTF-16 с неправильным форматом в экземплярах string.The following examples show valid C# code that creates ill-formed UTF-16 data in string instances.

  • Литерал с неправильным форматом:An ill-formed literal:

    const string s = "\ud800";
    
  • Подстрока, которая разделяет суррогатную пару:A substring that splits up a surrogate pair:

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

API-интерфейсы, такие как Encoding.UTF8.GetString, никогда не возвращают экземпляры string с неправильным форматом.APIs like Encoding.UTF8.GetString never return ill-formed string instances. Методы Encoding.GetString и Encoding.GetBytes обнаруживают последовательности с неправильным форматом во входных данных и выполняют замену символов при формировании выходных данных.Encoding.GetString and Encoding.GetBytes methods detect ill-formed sequences in the input and perform character substitution when generating the output. Например, если для метода Encoding.ASCII.GetString(byte[]) во входных данных отображается байт, отличный от ASCII (вне диапазона U+0000–U+007F), он вставляет символ "?" в возвращенный экземпляр string.For example, if Encoding.ASCII.GetString(byte[]) sees a non-ASCII byte in the input (outside the range U+0000..U+007F), it inserts a '?' into the returned string instance. Метод Encoding.UTF8.GetString(byte[]) заменяет последовательности UTF-8 с неправильным форматом на U+FFFD REPLACEMENT CHARACTER ('�') в возвращаемом экземпляре string.Encoding.UTF8.GetString(byte[]) replaces ill-formed UTF-8 sequences with U+FFFD REPLACEMENT CHARACTER ('�') in the returned string instance. Дополнительные сведения см. в разделах 5.22 и 3.9 стандарта Юникода.For more information, see the Unicode Standard, Sections 5.22 and 3.9.

Встроенные классы Encoding также можно настроить для создания исключения, а не для замены символов, когда отображаются последовательности с неправильным форматом.The built-in Encoding classes can also be configured to throw an exception rather than perform character substitution when ill-formed sequences are seen. Этот подход часто используется в приложениях, требующих особых мер безопасности, где замена символов может быть неприемлемой.This approach is often used in security-sensitive applications where character substitution might not be acceptable.

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.For information about how to use the built-in Encoding classes, see How to use character encoding classes in .NET.

См. такжеSee also