Udostępnij za pośrednictwem


Kodowanie znaków na platformie .NET

Ten artykuł zawiera wprowadzenie do systemów kodowania znaków używanych przez platformę .NET. W artykule wyjaśniono, jak typy String, Char, Rune i StringInfo działają z kodami Unicode, UTF-16 i UTF-8.

Ten termin charakter jest używany w ogólnym sensie tego, co czytelnik postrzega jako pojedynczy element wyświetlacza. Typowe przykłady to litera "a", symbol "@" i emoji "🐂". Czasami to, co wygląda jak jeden znak, w rzeczywistości składa się z wielu niezależnych elementów wyświetlania, jak wyjaśnia sekcja dotycząca klastrów grafemów.

Typy string i char

Wystąpienie string klasy reprezentuje jakiś tekst. Element A string jest logicznie sekwencją 16-bitowych wartości, z których każda jest wystąpieniem char struktury. Właściwość string.Length zwraca liczbę char wystąpień w string.

Następująca przykładowa funkcja wyświetla wartości w notacji szesnastkowej wszystkich char wystąpień w obiekcie 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 Przekaż komunikat "Hello" do tej funkcji i uzyskasz następujące dane wyjściowe:

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

Każdy znak jest reprezentowany przez jedną char wartość. Ten wzorzec jest prawdziwy dla większości języków świata. Na przykład poniżej przedstawiono dane wyjściowe dla dwóch chińskich znaków, które brzmią jak nǐ hǎo i oznaczają wartość Hello:

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

Jednak w przypadku niektórych języków, symboli i emoji potrzeba dwóch wystąpień char, aby reprezentować pojedynczy znak. Na przykład porównaj znaki i char wystąpienia w słowie, które oznacza Osage w języku Osage.

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

W poprzednim przykładzie każdy znak z wyjątkiem spacji jest reprezentowany przez dwa char wystąpienia.

Pojedyncza emotikona Unicode jest także reprezentowana przez dwa chars, jak pokazano w poniższym przykładzie przedstawiającym emotikonę wołu.

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

Te przykłady pokazują, że wartość string.Length, która wskazuje liczbę char wystąpień, niekoniecznie wskazuje liczbę wyświetlanych znaków. Pojedyncza instancja char sama w sobie nie musi reprezentować znaku.

Pary char mapowane na pojedynczy znak są nazywane parami zastępczymi (surrogate pairs). Aby zrozumieć, jak działają, musisz zrozumieć kodowanie Unicode i UTF-16.

Punkty kodu Unicode

Unicode to międzynarodowy standard kodowania używany na różnych platformach oraz w różnych językach i skryptach.

Standard Unicode definiuje ponad 1,1 miliona punktów kodu. Punkt kodu to wartość całkowita, która może wahać się od 0 do U+10FFFF (liczba dziesiętna 1114 1111). Niektóre punkty kodu są przypisywane do liter, symboli lub emoji. Inne są przypisywane do akcji, które kontrolują sposób wyświetlania tekstu lub znaków, takie jak przejście do nowego wiersza. Wiele punktów kodu nie jest jeszcze przypisanych.

Oto kilka przykładów przypisań punktów kodu z linkami do wykresów Unicode, w których są wyświetlane:

Dziesiętne Klątwa Przykład opis
10 U+000A Nie dotyczy KANAŁ LINIOWY
97 U+0061 a MAŁA LITERA A (ALFABET ŁACIŃSKI)
562 U+0232 Ȳ WIELKA LITERA Y Z MACRONEM
68,675 U+10C43 𐱃 STARA TURECKA LITERA ORKHON AT
127,801 U+1F339 🌹 Emoji ROSE

Punkty kodowe są zwyczajowo określane w składni U+xxxx, gdzie xxxx jest wartością całkowitą zakodowaną w formacie szesnastkowym.

W pełnym zakresie punktów kodu istnieją dwa podzakresy:

  • Podstawowa wielojęzyczna płaszczyzna (BMP) w zakresie U+0000..U+FFFF. Ten 16-bitowy zakres zapewnia 65 536 punktów kodu na tyle, aby pokryć większość systemów pisania na świecie.
  • Dodatkowe punkty kodowe w zakresie U+10000..U+10FFFF. Ten 21-bitowy zakres udostępnia ponad milion dodatkowych punktów kodu, które mogą być używane w mniej znanych językach i innych celach, takich jak emoji.

Na poniższym diagramie przedstawiono relację między BMP a dodatkowymi punktami kodu.

Punkty kodu BMP i dodatkowe

Jednostki kodu UTF-16

16-bitowy format przekształcenia Unicode (UTF-16) to system kodowania znaków, który używa 16-bitowych jednostek kodu do reprezentowania punktów kodu Unicode. Platforma .NET używa standardu UTF-16 do kodowania tekstu w obiekcie string. Wystąpienie char reprezentuje 16-bitową jednostkę kodu.

Jedna 16-bitowa jednostka kodu może reprezentować dowolny punkt kodu w 16-bitowym zakresie podstawowej płaszczyzny wielojęzycznej. Jednak w przypadku punktu kodu w zakresie pomocniczym potrzebne są dwa char wystąpienia.

Pary zastępcze

Tłumaczenie dwóch wartości 16-bitowych na pojedynczą wartość 21-bitową jest obsługiwane przez specjalny zakres nazywany punktami kodu zastępczego, z U+D800 do U+DFFF (dziesiętne od 55 296 do 57 343), włącznie.

Na poniższym diagramie przedstawiono relację między BMP a zastępczymi punktami kodu.

Punkty kodu BMP i punkty kodu zastępczego

Gdy wysoki punkt kodu zastępczego (U+D800..U+DBFF) jest natychmiast śledzony przez niski punkt kodu zastępczego (U+DC00..U+DFFF), para jest interpretowana jako dodatkowy punkt kodu przy użyciu następującej formuły:

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

Oto ta sama formuła używająca notacji dziesiętnej:

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

Wysoki punkt kodu zastępczego nie ma większej wartości liczbowej niż niski punkt kodu zastępczego. Punkt kodowy wysoki zastępczy jest nazywany "wysokim", ponieważ służy do obliczania wyższych 10 bitów z 20-bitowego zakresu punktów kodowych. Niski punkt kodu zastępczego służy do obliczania 10 bitów niższej kolejności.

Na przykład rzeczywisty punkt kodu odpowiadający parze 0xD83C zastępczej i 0xDF39 jest obliczany w następujący sposób:

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

Oto to samo obliczenie przy użyciu notacji dziesiętnej:

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

W poprzednim przykładzie pokazano, że "\ud83c\udf39" jest kodowaniem UTF-16 punktu kodu U+1F339 ROSE ('🌹') wymienionego wcześniej.

Wartości skalarne Unicode

Termin Wartość skalarna Unicode odnosi się do wszystkich punktów kodu innych niż punkty kodu zastępczego. Innymi słowy, wartość skalarna to dowolny punkt kodowy, który jest przypisany do znaku lub może zostać przypisany do znaku w przyszłości. "Znak" w tym miejscu odnosi się do dowolnych elementów, które można przypisać do punktu kodu, w tym akcji, które kontrolują sposób wyświetlania tekstu lub znaków.

Na poniższym diagramie przedstawiono punkty kodu wartości skalarnych.

Wartości skalarne

Typ Rune jako wartość skalarna

Ważne

Typ Rune nie jest dostępny w programie .NET Framework.

Na platformie .NET System.Text.Rune typ reprezentuje wartość skalarną Unicode.

Konstruktory Rune sprawdzają, czy wynikowe wystąpienie jest prawidłową wartością skalarną Unicode, w przeciwnym razie zgłaszają wyjątek. W poniższym przykładzie pokazano kod, który pomyślnie tworzy wystąpienia Rune, ponieważ dane wejściowe reprezentują prawidłowe wartości skalarne.

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

Poniższy przykład zgłasza wyjątek, ponieważ punkt kodowy znajduje się w zakresie zastępczym i nie jest częścią pary zastępczej.

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

Poniższy przykład zgłasza wyjątek, ponieważ punkt kodu wykracza poza zakres dodatkowy:

Rune g = new Rune(0x12345678);

Rune przykład użycia: zmiana wielkości liter

Interfejs API, który przyjmuje char element i zakłada, że działa z punktem kodu, który jest wartością skalarną, nie działa poprawnie, jeśli char element pochodzi z pary zastępczej. Rozważmy na przykład następującą metodę, która wywołuje Char.ToUpperInvariant dla każdej char w 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();
}

Jeśli element inputstring zawiera małą literę er Deseret (𐑉), ten kod nie zostanie przekonwertowany na wielkie litery (𐐡). Kod wywołuje char.ToUpperInvariant oddzielnie dla każdego zastępczego punktu kodu U+D801 i U+DC49. Ale U+D801 sama nie ma wystarczającej ilości informacji, aby zidentyfikować ją jako małą literę, więc char.ToUpperInvariant pozostawia ją samodzielnie. I obsługuje U+DC49 to w ten sam sposób. Wynikiem jest to, że małe litery "𐑉" w obiekcie inputstring nie są konwertowane na wielkie litery "𐑉".

Poniżej przedstawiono dwie opcje poprawnego konwertowania elementu string na wielkie litery:

  • Wywołaj String.ToUpperInvariant na danych wejściowych string zamiast iterować char-po-char. Metoda string.ToUpperInvariant ma dostęp do obu części każdej pary zastępczej, dzięki czemu może poprawnie obsłużyć wszystkie punkty kodu Unicode.

  • Iteruj wartości skalarne Unicode jako wystąpienia Rune zamiast char, jak pokazano w poniższym przykładzie. Ponieważ instancja Rune jest prawidłową wartością skalarną Unicode, można ją przekazać do interfejsów API, które operują na wartości skalarnej. Na przykład wywołanie Rune.ToUpperInvariant, jak pokazano w poniższym przykładzie, daje poprawne wyniki:

    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();
    }
    

Inne Rune interfejsy API

Typ Rune uwidacznia analogię wielu char interfejsów API. Na przykład następujące metody odzwierciedlają statyczne API w typie char:

Aby uzyskać nieprzetworzoną wartość skalarną z wystąpienia Rune, użyj właściwości Rune.Value.

Aby przekonwertować wystąpienie Rune z powrotem na sekwencję char, użyj metody Rune.ToString lub Rune.EncodeToUtf16.

Ponieważ dowolna wartość skalarna Unicode jest reprezentowana przez jedną char lub przez parę zastępczą, każde Rune wystąpienie może być reprezentowane przez co najwyżej 2 char wystąpienia. Użyj Rune.Utf16SequenceLength, aby zobaczyć, ile char instancji jest wymaganych do reprezentowania Rune instancji.

Aby uzyskać więcej informacji na temat typu .NET, zajrzyj do dokumentacji referencyjnej API.

Klastry Grapheme

To, co wygląda jak jeden znak, może wynikać z kombinacji wielu punktów kodu, dlatego bardziej opisowy termin, który jest często używany zamiast "znaku" to klaster grafeme. Równoważny termin na platformie .NET jest elementem tekstowym.

string Rozważ wystąpienia "a", "á", "á" i "👩🏽‍🚒". Jeśli system operacyjny obsługuje je zgodnie ze standardem Unicode, każde z tych string wystąpień jest wyświetlane jako pojedynczy element tekstowy lub klaster grapheme. Jednak ostatnie dwa są reprezentowane przez więcej niż jeden punkt kodu wartości skalarnej.

  • Wartość string "a" jest reprezentowana przez jedną wartość skalarną i zawiera jedno char wystąpienie.

    • U+0061 LATIN SMALL LETTER A
  • Wartość string "á" jest reprezentowana przez jedną wartość skalarną i zawiera jedno char wystąpienie.

    • U+00E1 LATIN SMALL LETTER A WITH ACUTE
  • Wyrażenie string "á" wygląda tak samo jak "á", ale jest reprezentowane przez dwie wartości skalarne i zawiera dwa char wystąpienia.

    • U+0061 LATIN SMALL LETTER A
    • U+0301 COMBINING ACUTE ACCENT
  • Na koniec element string "👩🏽‍🚒" jest reprezentowany przez cztery wartości skalarne i zawiera siedem char wystąpień.

    • U+1F469 WOMAN (zakres dodatkowy, wymaga pary zastępczej)
    • U+1F3FD EMOJI MODIFIER FITZPATRICK TYPE-4 (zakres uzupełniający, wymaga pary zastępczej)
    • U+200D ZERO WIDTH JOINER
    • U+1F692 FIRE ENGINE (zakres dodatkowy, wymaga pary zastępczej)

W niektórych z powyższych przykładów — takich jak łączący modyfikator akcentu lub modyfikator tonu skóry — punkt kodowy nie jest wyświetlany jako samodzielny element na ekranie. Zamiast tego służy do modyfikowania wyglądu elementu tekstowego, który przyszedł przed nim. Te przykłady pokazują, że potrzeba wielu wartości skalarnych, aby utworzyć to, co uważamy za pojedynczy "znak" lub "klaster grafemów".

Aby wyliczyć klastry grafemów string, użyj klasy StringInfo, jak pokazano w poniższym przykładzie. Jeśli znasz język Swift, typ platformy .NET StringInfo jest koncepcyjnie podobny do typu Swifta character.

Przykład: liczba wystąpień elementów char, Rune i wystąpień elementów tekstowych

W interfejsach API platformy .NET klaster grapheme jest nazywany elementem tekstowym. Poniższa metoda demonstruje różnice między wystąpieniami elementów char, Rune oraz instancjami tekstowymi w kontekście 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("a");
// 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

Przykład: dzielenie string wystąpień

Podczas dzielenia wystąpień string unikaj dzielenia par zastępczych i klastrów graficznych. Rozważmy następujący przykład nieprawidłowego kodu, który zamierza wstawić podziały wierszy co 10 znaków w obiekcie 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();
}

Ponieważ ten kod wylicza char wystąpienia, para zastępcza, która przypadkowo przebiega przez granicę 10-char, zostanie podzielona i nowa linia będzie wstawiona między nimi. To wprowadzenie wprowadza uszkodzenie danych, ponieważ punkty kodowe zastępcze mają znaczenie tylko jako pary.

Możliwość uszkodzenia danych nie jest wyeliminowana, jeśli wyliczasz Rune wystąpienia (wartości skalarne) zamiast char wystąpień. Zestaw Rune wystąpień może składać się z klastra grafemów, który przecina granicę 10-char. Jeśli zestaw klastra grapheme został podzielony, nie można go poprawnie zinterpretować.

Lepszym podejściem jest podzielenie string przez zliczanie klastrów grafemów lub elementów tekstowych, jak w poniższym przykładzie:

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

    // Append chunks in multiples of 10 chars

    TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(input);

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

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

}

Jak wspomniano wcześniej, przed platformą .NET 5 StringInfo klasa miała usterkę powodującą nieprawidłowe obsługę niektórych klastrów grafeme.

UTF-8 i UTF-32

Poprzednie sekcje koncentrowały się na architekturze UTF-16, ponieważ jest to używane przez platformę .NET do kodowania string wystąpień. Istnieją inne systemy kodowania Unicode — UTF-8 i UTF-32. Te kodowania używają odpowiednio 8-bitowych jednostek kodu i 32-bitowych jednostek kodu.

Podobnie jak UTF-16, kod UTF-8 wymaga wielu jednostek kodu do reprezentowania niektórych wartości skalarnych Unicode. UtF-32 może reprezentować dowolną wartość skalarną w jednej 32-bitowej jednostce kodu.

Poniżej przedstawiono kilka przykładów pokazujących, jak ten sam punkt kodu Unicode jest reprezentowany w każdym z tych trzech systemów kodowania Unicode:

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)

Jak wspomniano wcześniej, pojedyncza jednostka kodu UTF-16 z pary zastępczej jest bez znaczenia sama w sobie. W ten sam sposób pojedyncza jednostka kodu UTF-8 jest bez znaczenia, jeśli znajduje się w sekwencji dwóch, trzech lub czterech używanych do obliczenia wartości skalarnej.

Uwaga

Począwszy od języka C# 11, można reprezentować literały UTF-8 string przy użyciu sufiksu "u8" na literału string. Aby uzyskać więcej informacji na temat literałów UTF-8 string, zobacz sekcję "string literały" artykułu na temat wbudowanych typów referencyjnych w przewodniku języka C#.

Kolejność bajtów

Jednostki kodu UTF-16 elementu string są przechowywane w ciągłej pamięci jako sekwencja 16-bitowych liczb całkowitych (wystąpienia char) na platformie .NET. Bity poszczególnych jednostek kodu są określone zgodnie z endianness bieżącej architektury.

W architekturze typu little-endian string, składające się z punktów kodu UTF-16 [ D801 DCCC ] zostaną zapisane w pamięci jako bajty [ 0x01, 0xD8, 0xCC, 0xDC ]. W architekturze big-endian, string zostanie rozłożone w pamięci jako bajty [ 0xD8, 0x01, 0xDC, 0xCC ].

Systemy komputerowe komunikujące się ze sobą muszą uzgodnić reprezentację danych przekraczających przewód. Większość protokołów sieciowych używa UTF-8 jako standardu podczas przesyłania tekstu, częściowo w celu uniknięcia problemów, które mogą wynikać z komunikacji między maszyną big-endian a maszyną little-endian. Element string, składający się z punktów kodowych [ F0 90 93 8C ] UTF-8, będzie zawsze reprezentowany jako bajty [ 0xF0, 0x90, 0x93, 0x8C ], niezależnie od kolejności bajtów.

Aby używać formatu UTF-8 do przesyłania tekstu, aplikacje platformy .NET często używają kodu takiego jak w poniższym przykładzie:

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

W poprzednim przykładzie metoda Encoding.UTF8.GetBytes dekoduje UTF-16 string z powrotem na serię skalarów Unikodu, następnie ponownie koduje te skalary do UTF-8 i umieszcza wynikową sekwencję w tablicy byte. Metoda Encoding.UTF8.GetString wykonuje odwrotną transformację, konwertując tablicę UTF-8 byte na utF-16 string.

Ostrzeżenie

Ponieważ UTF-8 jest powszechny w Internecie, może być kuszące odczytywać surowe bajty z sieci i traktować dane tak, jakby były w formacie UTF-8. Należy jednak sprawdzić, czy jest rzeczywiście dobrze sformułowany. Złośliwy klient może przesłać do usługi źle sformułowany kod UTF-8. Jeśli korzystasz z tych danych tak, jakby były prawidłowo sformułowane, może to spowodować błędy lub luki w zabezpieczeniach w aplikacji. Aby zweryfikować dane UTF-8, można użyć metody takiej jak Encoding.UTF8.GetString, która przeprowadzi walidację podczas konwertowania danych przychodzących na string.

Poprawnie sformułowane kodowanie

Poprawnie sformułowane kodowanie Unicode to string zestaw jednostek kodu, które mogą być dekodowane jednoznacznie i bez błędów na sekwencję wartości skalarnych Unicode. Dobrze sformułowane dane mogą być transkodowane swobodnie między UTF-8, UTF-16 i UTF-32.

Pytanie, czy sekwencja kodowania jest prawidłowo sformułowana, nie ma związku z kolejnością bajtów w architekturze maszyny. Źle sformułowana sekwencja UTF-8 jest źle sformułowana w ten sam sposób zarówno na maszynach big-endian, jak i little-endian.

Oto kilka przykładów źle sformułowanych kodowań:

  • W formacie UTF-8 sekwencja [ 6C C2 61 ] jest źle sformułowana, ponieważ C2 nie może być poprzedzona przez 61.

  • W UTF-16 sekwencja [ DC00 DD00 ] (lub, w C#, string"\udc00\udd00") jest niepoprawnie sformułowana, ponieważ niski znak zastępczy DC00 nie może być poprzedzony przez inny niski znak zastępczy DD00.

  • W formacie UTF-32 sekwencja [ 0011ABCD ] jest źle sformułowana, ponieważ 0011ABCD znajduje się poza zakresem wartości skalarnych Unicode.

Na platformie .NET string wystąpienia prawie zawsze zawierają dobrze sformułowane dane UTF-16, ale nie jest to gwarantowane. W poniższych przykładach pokazano prawidłowy kod języka C#, który tworzy nieprawidłowo sformułowane dane UTF-16 w string wystąpieniach.

  • Źle sformułowany literał:

    const string s = "\ud800";
    
  • Podciąg który dzieli parę zastępczą

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

Interfejsy API takie jak Encoding.UTF8.GetString nigdy nie zwracają źle ukształtowanych string wystąpień. Metody Encoding.GetString i Encoding.GetBytes wykrywają źle uformowane sekwencje w danych wejściowych i zastępują znaki podczas generowania danych wyjściowych. Jeśli na przykład Encoding.ASCII.GetString(byte[]) natrafi na bajt inny niż ASCII w danych wejściowych (poza zakresem U+0000..U+007F), wstawi znak '?' do zwróconego string wystąpienia. Encoding.UTF8.GetString(byte[]) zastępuje sekwencje UTF-8, które są nieprawidłowo sformułowane, na U+FFFD REPLACEMENT CHARACTER ('�') w zwróconym wystąpieniu string. Aby uzyskać więcej informacji, zobacz Standard Unicode, Sekcje 5.22 i 3.9.

Wbudowane Encoding klasy można również skonfigurować pod kątem zgłaszania wyjątku zamiast dokonywania zamiany znaków, gdy występują źle sformułowane sekwencje. Takie podejście jest często stosowane w aplikacjach z uwzględnieniem zabezpieczeń, gdzie zastępowanie znaków może nie być akceptowalne.

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

Aby uzyskać informacje na temat używania wbudowanych klas, zobacz How to use character encoding classes in Encoding .NET (Jak używać klas kodowania znaków na platformie .NET).

Zobacz też