Zeichencodierung in .NET

Dieser Artikel bietet eine Einführung in die von .NET verwendeten char-Codierungssysteme. Es wird erläutert, wie die Typen String, Char, Rune und StringInfo mit Unicode, UTF-16 und UTF-8 funktionieren.

Der Begriff char wird hier im allgemeinen Sinn für ein Zeichen verwendet, das vom Leser als einzelnes Anzeigeelement wahrgenommen wird. Gängige Beispiele sind der Buchstabe „a“, das Symbol „@“ und das Emoji 🐂. Mitunter setzt sich ein als ein Zeichen wahrgenommenes char tatsächlich aus mehreren unabhängigen Anzeigeelementen zusammen. Dies wird im Abschnitt zu Graphemhaufen erläutert.

Die string- und char-Typen.

Eine Instanz der string-Klasse stellt Text dar. Ein string ist logisch gesehen eine Abfolge von 16-Bit-Werten, von denen jeder eine Instanz der char-Struktur ist. Die string.Length-Eigenschaft gibt die Anzahl von char-Instanzen in der string-Instanz zurück.

Die folgende Beispielfunktion gibt die Werte aller char-Instanzen in einem string in Hexadezimalnotation zurück:

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

Wenn Sie an diese Funktion die string „Hello“ übergeben, erhalten Sie die folgende Ausgabe:

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

Jedes char wird durch einen einzelnen char-Wert repräsentiert. Dieses Muster gilt für die meisten Sprachen der Welt. Beispielsweise sehen Sie nachfolgend die Ausgabe für zwei chinesische char, die ausgesprochen wie nǐ hǎo klingen und Hallo bedeuten:

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

Für einige Sprachen und für einige Symbole und Emojis werden jedoch zwei char-Instanzen benötigt, um ein einzelnes char darzustellen. Vergleichen Sie beispielsweise die char und char-Instanzen in dem Wort, das in der Osage-Sprache Osage bedeutet:

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

Im vorherigen Beispiel wird jedes char mit Ausnahme des Leerzeichens durch zwei char-Instanzen repräsentiert.

Ein einzelnes Unicode-Emoji wird ebenfalls durch zwei chars dargestellt, wie im folgenden Beispiel eines Emojis für einen Ochsen:

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

Diese Beispiele zeigen, dass der Wert von string.Length, der die Anzahl von char-Instanzen angibt, nicht notwendigerweise der Anzahl angezeigter char entsprechen muss. Eine einzelne char-Instanz repräsentiert für sich genommen nicht zwingend ein char.

Die char-Paare, die einem einzelnen char zugeordnet sind, werden als Ersatzzeichenpaare bezeichnet. Um deren Funktionsweise zu verstehen, müssen wir die Unicode- und die UTF-16-Codierung verstehen.

Unicode-Codepunkte

Unicode ist ein internationaler Codierungsstandard, der in zahlreichen Plattformen und mit verschiedenen Sprachen und Skripts eingesetzt wird.

Der Unicode-Standard definiert über 1,1 Millionen Codepunkte. Ein Codepunkt ist ein ganzzahliger Wert, der zwischen 0 und U+10FFFF liegen kann (in Dezimalschreibweise: 1.114.111). Einige Codepunkte sind Buchstaben, Symbolen oder Emojis zugewiesen. Andere sind Aktionen zugeordnet, die steuern, wie Textelemente oder char angezeigt werden – beispielsweise ein Zeilenvorschub. Viele Codepunkte sind noch nicht zugewiesen.

Nachfolgend werden einige Beispiele für Codepunktzuweisungen aufgelistet, mit Links zu Unicode-charts, in denen sie angezeigt werden:

Decimal Hex Beispiel Beschreibung
10 U+000A Nicht zutreffend ZEILENVORSCHUB
97 U+0061 a LATEINISCHER KLEINBUCHSTABE A
562 U+0232 Ȳ LATEINISCHER GROSSBUCHSTABE MIT MAKRON
68.675 U+10C43 𐱃 ALTTÜRKISCHES ORCHON-SCHRIFTZEICHEN AT
127.801 U+1F339 🌹 Rosen-Emoji

Auf Codepunkte wird hauptsächlich durch Verwendung der Syntax U+xxxx verwiesen, wobei es sich bei xxxx um den ganzzahligen Wert in Hexadezimalcodierung handelt.

Der vollständige Bereich der Codepunkte enthält zwei Unterbereiche:

  • Die Basic Multilingual Plane (BMP) im Bereich U+0000..U+FFFF. Dieser 16-Bit-Bereich umfasst 65.536 Codepunkte und reicht zur Abdeckung der meisten Schriftsysteme der Welt aus.
  • Ergänzende Codepunkte im Bereich U+10000..U+10FFFF. Dieser 21-Bit-Bereich umfasst mehr als eine Million zusätzlicher Codepunkte, die für weniger bekannte Sprachen und zu anderen Zwecken genutzt werden können, wie beispielsweise Emojis.

Das folgende Diagramm veranschaulicht die Beziehung zwischen der BMP und den ergänzenden Codepunkten.

BMP und ergänzende Codepunkte

UTF-16-Codeeinheiten

16-Bit Unicode Transformation Format (UTF-16) ist ein char-Codierungssystem, das 16-Bit-Codeeinheiten zur Darstellung von Unicode-Codepunkten verwendet. .NET verwendet UTF-16 zum Codieren von Text in einem string. Eine char-Instanz repräsentiert eine 16-Bit-Codeeinheit.

Eine einzelne 16-Bit-Codeeinheit kann jeden Codepunkt im 16-Bit-Bereich der Basic Multilingual Plane (BMP) repräsentieren. Aber für einen Codepunkt im ergänzenden Bereich werden zwei char-Instanzen benötigt.

Ersatzzeichenpaare

Die Übersetzung von zwei 16-Bit-Werten in einen einzelnen 21-Bit-Wert wird durch einen speziellen Bereich ermöglicht, der die sogenannten Ersatzcodepunkte von U+D800 bis U+DFFF einschließlich (in Dezimalschreibweise: 55.296 bis 57.343) umfasst.

Das folgende Diagramm veranschaulicht die Beziehung zwischen der BMP und den Ersatzcodepunkten.

BMP und Ersatzcodepunkte

Wenn auf einen hohen Ersatzcodepunkt (U+D800..U+DBFF) unmittelbar ein niedriger Ersatzcodepunkt (U+DC00..U+DFFF) folgt, wird das Paar anhand der folgenden Formel als ergänzender Codepunkt interpretiert:

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

Nachstehend sehen Sie die gleiche Formel unter Verwendung der Dezimalschreibweise:

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

Ein hoher Ersatzcodepunkt weist keinen höheren Zahlenwert auf als ein niedriger Ersatzcodepunkt. Der hohe Ersatzcodepunkt wird so bezeichnet, weil er zum Berechnen der hohen 10 Bits eines 20-Bit-Codepunktbereichs verwendet wird. Der niedrige Ersatzcodepunkt wird zum Berechnen der unteren 10 Bits verwendet.

Beispielsweise wird der tatsächliche Codepunkt, der dem Ersatzzeichenpaar 0xD83C und 0xDF39 entspricht, folgendermaßen berechnet:

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

Hier dieselbe Berechnung in Dezimalschreibweise:

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

Das vorstehende Beispiel zeigt, dass es sich bei "\ud83c\udf39" um die UTF-16-Codierung des zuvor erwähnten Codepunkts U+1F339 ROSE ('🌹') handelt.

Unicode-Skalarwerte

Der Begriff Unicode-Skalarwert bezieht sich auf alle Codepunkte mit Ausnahme der Ersatzcodepunkte. Anders ausgedrückt: Ein Skalarwert ist ein beliebiger Codepunkt, der einem char zugewiesen ist oder in Zukunft einem char zugewiesen werden kann. „Zeichen“ bezieht sich hierbei auf ein beliebiges Element, das einem Codepunkt zugewiesen werden kann, darunter z. B. Aktionen zum Steuern der Anzeige von Text oder char.

Das nachstehende Diagramm veranschaulicht die Skalarwert-Codepunkte.

Skalarwerte

Der Rune-Typ als Skalarwert

Ab .NET Core 3.0 repräsentiert der System.Text.Rune-Typ einen Unicode-Skalarwert. Rune ist in .NET Core 2. oder .NET Framework 4.x nicht verfügbar.

Der Rune-Konstruktor überprüft, ob die Ergebnisinstanz ein gültiger Unicode-Skalarwert ist, anderenfalls wird eine Ausnahme ausgelöst. Das folgende Codebeispiel zeigt eine erfolgreiche Instanziierung von Rune-Instanzen, weil es sich bei der Eingabe um gültige Skalarwerte handelt:

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

Das folgende Beispiel führt zu einer Ausnahme, weil der Codepunkt sich im Ersatzbereich befindet und nicht Teil eines Ersatzzeichenpaars ist:

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

Das nachstehende Beispiel führt zu einer Ausnahme, weil der Codepunkt nicht im ergänzenden Bereich liegt:

Rune g = new Rune(0x12345678);

Beispiel für die Verwendung von Rune: Änderung der Groß-/Kleinschreibung

Eine API, die einen char-Wert verwendet und annimmt, dass sie mit einem Codepunkt arbeitet, bei dem es sich um einen Skalarwert handelt, funktioniert nicht ordnungsgemäß, wenn der char-Wert aus einem Ersatzzeichenpaar stammt. Betrachten Sie beispielsweise die folgende Methode, die Char.ToUpperInvariant für jeden char in einem string aufruft:

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

Wenn die Eingabezeichenfolge (inputstring) den Deseret-Buchstaben er in Kleinschreibung (𐑉) enthält, erfolgt bei diesem Code keine Konvertierung in einen Großbuchstaben (𐐡). Der Code ruft char.ToUpperInvariant separat für jeden Ersatzcodepunkt auf, U+D801 und U+DC49. Aber U+D801 selbst weist nicht genügend Informationen für die Identifizierung als Kleinbuchstabe auf und wird daher von char.ToUpperInvariant nicht verarbeitet. U+DC49 wird in gleicher Weise behandelt. Somit wird der Kleinbuchstabe „𐑉“ in der Eingabezeichenfolge (inputstring) nicht in den Großbuchstaben „𐐡“ konvertiert.

Hier sind zwei Optionen für die korrekte Konvertierung von string in Großbuchstaben:

  • Rufen Sie String.ToUpperInvariant für den Eingabe-string auf, statt char für char zu durchlaufen. Die string.ToUpperInvariant-Methode hat Zugriff auf beide Teile des Ersatzzeichenpaars, deshalb können alle Unicode-Codepunkte ordnungsgemäß verarbeitet werden.

  • Durchlaufen Sie die Unicode-Skalarwerte nicht als char-Instanzen, sondern als Rune-Instanzen, wie im folgenden Beispiel gezeigt. Eine Rune-Instanz ist ein gültiger Unicode-Skalarwert, deshalb kann sie an APIs übergeben werden, die einen Skalarwert für die Verarbeitung erwarten. Beispielsweise liefert der Aufruf von Rune.ToUpperInvariant wie im folgenden Beispiel richtige Ergebnisse:

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

Andere Rune-APIs

Der Rune-Typ macht Entsprechungen vieler der char-APIs verfügbar. Beispielsweise spiegeln die folgenden Methoden statische APIs für den char-Typ:

Um den Rohskalarwert aus einer Rune-Instanz abzurufen, verwenden Sie die Rune.Value-Eigenschaft.

Um eine Rune-Instanz wieder ein eine char-Sequenz zu konvertieren, verwenden Sie Rune.ToString oder die Rune.EncodeToUtf16-Methode.

Da jeder Unicode-Skalarwert durch einen einzelnen char oder durch ein Ersatzzeichenpaar dargestellt wird, kann jede Rune-Instanz durch höchstens 2 char-Instanzen dargestellt werden. Verwenden Sie Rune.Utf16SequenceLength, um anzuzeigen, wie viele char-Instanzen zur Darstellung einer Rune-Instanz benötigt werden.

Weitere Informationen zum .NET-Typ Rune finden Sie in der RuneAPI-Referenz.

Graphemhaufen

Was wie ein einziges char aussieht, kann tatsächlich eine Kombination mehrerer Codepunkte sein, deshalb wird zur Beschreibung anstelle des Begriffs „char“ häufig der Begriff Graphemhaufen verwendet. Der äquivalente Begriff in .NET lautet Textelement.

Betrachten Sie die string-Instanzen „a“, „á“, „á“ und „👩🏽‍🚒“. Wenn Ihr Betriebssystem diese gemäß Spezifikation im Unicode-Standard verarbeitet, wird jede dieser string-Instanzen als ein einzelnes Textelement bzw. als Graphemhaufen angezeigt. Aber die letzten beiden werden durch mehr als einen Skalarwert-Codepunkt repräsentiert.

  • Die string-Instanz „a“ wird durch einen Skalarwert repräsentiert und enthält eine char-Instanz.

    • U+0061 LATIN SMALL LETTER A
  • Die string-Instanz „á“ wird durch einen Skalarwert repräsentiert und enthält eine char-Instanz.

    • U+00E1 LATIN SMALL LETTER A WITH ACUTE
  • Die string-Instanz „á“ sieht aus wie „á“, wird jedoch durch zwei Skalarwerte repräsentiert und enthält zwei char-Instanzen.

    • U+0061 LATIN SMALL LETTER A
    • U+0301 COMBINING ACUTE ACCENT
  • Die string-Instanz 👩🏽‍🚒 schließlich wird durch vier Skalarwerte repräsentiert und enthält sieben char-Instanzen.

    • U+1F469 WOMAN (ergänzender Bereich, erfordert ein Ersatzzeichenpaar)
    • U+1F3FD EMOJI MODIFIER FITZPATRICK TYPE-4 (ergänzender Bereich, erfordert ein Ersatzzeichenpaar)
    • U+200D ZERO WIDTH JOINER
    • U+1F692 FIRE ENGINE (ergänzender Bereich, erfordert ein Ersatzzeichenpaar)

In einigen der vorhergehenden Beispiele – beispielsweise dem kombinierten Akzentmodifizierer oder dem Modifizierer für den Hautton – wird der Codepunkt nicht als eigenständiges Element auf dem Bildschirm angezeigt. Stattdessen dient er zum Ändern des Aussehens eines vorangegangenen Textelements. Diese Beispiele zeigen, dass möglicherweise mehrere Skalarwerte erforderlich sind, um ein einzelnes char oder einen Graphemhaufen zu erzeugen.

Um die Graphemhaufen für einen string aufzulisten, verwenden Sie die StringInfo-Klasse wie im folgenden Beispiel gezeigt. Wenn Sie mit Swift vertraut sind: Der .NET-Typ StringInfo ähnelt vom Konzept her dem character-Typ in Swift.

Beispiel: Zählen von char-, Rune- und Textelementinstanzen

In den .NET-APIs wird ein Graphemhaufen als Textelement bezeichnet. Die folgenden Methoden veranschaulichen die Unterschiede zwischen char-, Rune- und Textelementinstanzen in einem 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

Wenn Sie diesen Code in .NET Framework oder .NET Code 3.1 oder früher ausführen, wird die Textelementanzahl für das Emoji als 4 angezeigt. Dies liegt an einem Fehler in der StringInfo-Klasse, der in .NET 5 behoben wurde.

Beispiel: Aufteilen von string-Instanzen

Vermeiden Sie beim Aufteilen von string-Instanzen das Teilen von Ersatzzeichenpaaren und Graphemhaufen. Sehen Sie sich das folgende fehlerhafte Codebeispiel an, bei dem nach jeweils 10 char in einer string ein Zeilenumbruch eingefügt werden soll:

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

Dieser Code zählt char-Instanzen auf, deshalb wird ein Ersatzzeichenpaar, dass sich über eine 10-char-Grenze erstreckt, aufgeteilt und dazwischen ein Zeilenumbruch eingefügt. Diese Einfügung führt zu einer Datenbeschädigung, weil Ersatzcodepunkte nur als Paar eine Bedeutung tragen.

Die Möglichkeit einer Datenbeschädigung ist nicht ausgeschlossen, wenn Sie anstelle von char-Instanzen Rune-Instanzen (Skalarwerte) aufzählen. Ein Satz aus Rune-Instanzen kann einen Graphemhaufen bilden, der sich über eine 10-char-Grenze erstreckt. Wenn der Graphemhaufen geteilt wird, kann er nicht ordnungsgemäß interpretiert werden.

Ein besserer Ansatz besteht darin, den string durch Zählen der Graphemhaufen oder Textelemente zu teilen, wie im folgenden Beispiel gezeigt:

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 && textElementCount > 0)
        {
            builder.AppendLine(); // newline
        }
        textElementCount++;
    }

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

}

Wie bereits erwähnt, hatte die Klasse StringInfo vor .NET 5 einen Fehler, der dazu führte, dass einige Graphemhaufen falsch behandelt wurden.

UTF-8 und UTF-32

Die vorangegangenen Abschnitte konzentrierten sich auf UTF-16, weil .NET UTF-16 zur Codierung von string-Instanzen verwendet. Für Unicode sind einige weitere Codierungssysteme vorhanden – UTF-8 und UTF-32. Diese Codierungen verwenden 8-Bit-Codeeinheiten bzw. 32-Bit-Codeeinheiten.

Wie UTF-16 werden in UTF-8 mehrere Codeeinheiten benötigt, um einige Unicode-Skalarwerte darzustellen. UTF-32 kann jeden beliebigen Skalarwert in einer einzelnen 32-Bit-Codeeinheit darstellen.

Hier sind einige Beispiele, die zeigen, wie derselbe Unicode-Codepunkt in jedem dieser drei Unicode-Codierungssysteme dargestellt wird:

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)

Wie bereits erwähnt, ist eine einzelne UTF-16-Codeeinheit aus einem Einzelzeichenpaar für sich genommen bedeutungslos. Ebenso ist eine einzelne UTF-8-Codeeinheit für sich genommen bedeutungslos, wenn sie in einer Sequenz von zwei, drei oder vier Einheiten zur Berechnung eines Skalarwerts verwendet wird.

Hinweis

Ab C# 11 können UTF-8-Literale vom Typ „string“ darstellen, indem Sie das Suffix „u8“ für ein string-Literal verwenden. Weitere Informationen zu UTF-8-Literalen vom Typ „string“ finden Sie im C#-Leitfaden im Abschnitt zu UTF-8-Zeichenfolgen-Literalen (Literale vom Typ „string“) des Artikels Integrierte Verweistypen (C#-Referenz).

Endianness

In .NET werden die UTF-16-Codeeinheiten für einen string in zusammenhängenden Speicherbereichen als Sequenz aus 16-Bit-Ganzzahlen (char-Instanzen) gespeichert. Die Bits der einzelnen Codeeinheiten werden gemäß Endianwert der aktuellen Architektur angeordnet.

In einer Little-Endian-Architektur wird der aus UTF-16-Codeeinheiten [ D801 DCCC ] bestehende string im Arbeitsspeicher in Form der Bytes [ 0x01, 0xD8, 0xCC, 0xDC ] angeordnet. In einer Big-Endian-Architektur würde derselbe string im Arbeitsspeicher in Form der Bytes [ 0xD8, 0x01, 0xDC, 0xCC ] angeordnet werden.

Computersysteme, die miteinander kommunizieren, müssen sich auf die Darstellung von Daten einigen, die übertragen werden. Die meisten Netzwerkprotokolle verwenden UTF-8 als Standard für die Textübertragung – unter anderem, um Probleme zu vermeiden, die durch die Kommunikation eines Big-Endian-Computers mit einem Little-Endian-Computer entstehen könnten. Der aus den UTF-8-Codepunkten [ F0 90 93 8C ] bestehende string wird immer in Form der Bytes [ 0xF0, 0x90, 0x93, 0x8C ] dargestellt, unabhängig vom Endianwert.

Um UTF-8 für die Übertragung von Text zu verwenden, nutzen .NET-Anwendungen häufig Code wie im folgenden Beispiel:

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

Im vorstehenden Beispiel decodiert die Methode Encoding.UTF8.GetBytes den UTF-16-string zurück in eine Reihe von Unicode-Skalarwerten. Anschließend werden diese Skalarwerte wieder in UTF-8 codiert, und die Ergebnissequenz wird in einem byte-Array platziert. Die Methode Encoding.UTF8.GetString führt die umgekehrte Transformation durch, sie konvertiert einen UTF-8-byte-Array in einen UTF-16-string.

Warnung

Weil UTF-8 im Internet so weit verbreitet ist, kann es verlockend sein, Rohbytes aus der Verbindung zu lesen und die Daten so zu behandeln, als handele es sich um UTF-8. Sie müssen jedoch sicherstellen, dass die Daten tatsächlich wohlgeformt sind. Ein schädlicher Client überträgt möglicherweise nicht wohlgeformte UTF-8-Daten an Ihren Dienst. Wenn Sie mit diesen Daten arbeiten, als seien es wohlgeformte Daten, kann dies zu Fehlern oder Sicherheitslücken in Ihrer Anwendung führen. Um UTF-8-Daten zu validieren, können Sie eine Methode wie Encoding.UTF8.GetString verwenden, die während der Konvertierung der eingehenden Daten in einen string eine Überprüfung durchführt.

Wohlgeformte Codierung

Eine wohlgeformte Unicode-Codierung ist ein string aus Codeeinheiten, die eindeutig decodiert und ohne Fehler in eine Sequenz von Unicode-Skalarwerten konvertiert werden können. Wohlgeformte Daten können ohne Einschränkung zwischen UTF-8, UTF-16 und UTF-32 hin- und her transcodiert werden.

Die Frage, ob eine Codierungssequenz wohlgeformt ist oder nicht, hat nichts mit dem Endianwert einer Computerarchitektur zu tun. Eine nicht wohlgeformte UTF-8-Sequenz ist sowohl für einen Big-Endian- als auch für einen Little-Endian-Computer nicht wohlgeformt.

Hier sehen Sie einige Beispiele für nicht wohlgeformte Codierungen:

  • In UTF-8 ist die Sequenz [ 6C C2 61 ] nicht wohlgeformt, weil auf C2 nicht 61 folgen darf.

  • In UTF-16 ist die Sequenz [ DC00 DD00 ] (oder in C# die Zeichenfolge (string) "\udc00\udd00") nicht wohlgeformt, weil auf das niedrige Ersatzzeichen DC00 nicht ein weiteres niedriges Ersatzzeichen DD00 folgen darf.

  • In UTF-32 ist die Sequenz [ 0011ABCD ] nicht wohlgeformt, weil 0011ABCD außerhalb des Bereichs für Unicode-Skalarwerte liegt.

In .NET enthalten string-Instanzen fast immer wohlgeformte UTF-16-Daten, aber dies ist nicht garantiert. Die folgenden Beispiele zeigen gültigen C#-Code, der nicht wohlgeformte UTF-16-Daten in string-Instanzen erzeugt.

  • Ein nicht wohlgeformtes Literal:

    const string s = "\ud800";
    
  • Eine Teil-string, die ein Ersatzzeichenpaar aufteilt:

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

APIs wie Encoding.UTF8.GetString geben keine nicht wohlgeformten string-Instanzen zurück. Die Methoden Encoding.GetString und Encoding.GetBytes erkennen falsch formatierte Sequenzen in der Eingabe und führen beim Generieren der Ausgabe char-Ersetzungen durch. Wenn Encoding.ASCII.GetString(byte[]) beispielsweise ein Nicht-ASCII-Byte (außerhalb des Bereichs U+0000..U+007F) in der Eingabe erkennt, wird ein ? in die zurückgegebene string-Instanz eingefügt. Encoding.UTF8.GetString(byte[]) ersetzt nicht wohlgeformte UTF-8-Sequenzen in der zurückgegebenen string-Instanz durch U+FFFD REPLACEMENT CHARACTER ('�'). Weitere Informationen finden Sie unter Unicode-Standard in den Abschnitten 5.22 und 3.9.

Die integrierten Encoding-Klassen können auch so konfiguriert werden, dass sie anstelle einer char-Ersetzung eine Ausnahme auslösen, wenn falsch formatierte Sequenzen erkannt werden. Dieser Ansatz findet häufig in Anwendungen mit hohen Sicherheitsanforderungen Anwendung, bei denen eine char-Ersetzung möglicherweise nicht akzeptabel ist.

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

Informationen zur Verwendung der integrierten Encoding-Klassen finden Sie unter Verwenden von char-Codierungsklassen in .NET.

Siehe auch