1. Einführung
Wir haben schon verstanden, dass Computer, so clever sie auch sein mögen, von sich aus nicht wissen, was der Buchstabe "A" oder das Zeichen "⌘" ist. Sie kennen nur Nullen und Einsen — und um diese in für Menschen verständliche Zeichen zu übersetzen, brauchen sie einen Übersetzer: eine Kodierung.
Die Geschichte der Kodierungen ist eine Geschichte von Kompromissen und Evolution. Zuerst war alles einfach, dann wurde es komplizierter, und schließlich entstand ein mehr oder weniger universeller Standard. Gehen wir die Chronologie durch.
Anfang der Geschichte
Fangen wir bei den Wurzeln an. In der vorherigen Vorlesung haben wir bereits den Urahn der Textkodierungen erwähnt — ASCII (ausgesprochen "aski"). Zur Erinnerung: das steht für American Standard Code for Information Interchange, also Amerikanischer Standard-Code für Informationsaustausch. Schon aus dem Namen ist klar, für wen es gemacht wurde und warum es "amerikanisch" ist.
ASCII wurde in den 1960er Jahren entwickelt und war der erste weitverbreitete Standard zur Codierung von Zeichen. Es ist ein Zeichensatz aus 128 Zeichen:
- Lateinische Buchstaben (groß und klein): A-Z, a-z
- Ziffern: 0-9
- Satzzeichen: .,!?"' usw.
- Einige Steuerzeichen: Zeilenumbruch, Tab und Ähnliches.
Jedes dieser 128 Zeichen wurde als ein Byte kodiert, wobei nur 7 der 8 Bits genutzt wurden (das höchstwertige Bit blieb meist frei oder wurde für Prüfungen genutzt). Das ist sehr kompakt und effizient für die englische Sprache.
Beispiel:
Das Zeichen 'A' in ASCII wird als Byte 0x41 kodiert (binär 01000001)
Das Zeichen '!' in ASCII wird als Byte 0x21 kodiert (binär 00100001)
Beschränkungen:
Die wichtigste Einschränkung von ASCII ist offensichtlich: es ist auf die englische Sprache zugeschnitten. Wenn du etwas auf Russisch ("Привет"), Deutsch ("Grüße") oder Chinesisch schreiben willst, hilft dir ASCII nicht weiter. Diese Zeichen sind einfach nicht in der Tabelle enthalten. Das führte zur Entstehung zahlreicher sogenannter Code Pages, die die 128 ASCII-Zeichen auf 256 erweiterten, indem sie das achte Bit nutzten. Zum Beispiel gab es für Russisch die Code Pages CP1251 (Windows Cyrillic), KOI8-R und andere. Das Problem war, dass diese Code Pages untereinander inkompatibel waren: dasselbe Byte konnte in verschiedenen Code Pages völlig unterschiedliche Zeichen bedeuten — ein echtes Babylon.
Praktische Nutzung heute:
Reines ASCII-Format wird für allgemeine Textdateien heutzutage selten verwendet, höchstens für sehr spezielle Zwecke oder alte Systeme. Sein Erbe lebt allerdings weiter: viele moderne Kodierungen sind, wie wir sehen werden, rückwärtskompatibel zu ASCII.
Probieren wir aus, etwas in ASCII zu schreiben und zu lesen, und fügen dann russische Buchstaben hinzu, um zu sehen, was passiert.
Erstelle ein neues Konsolenprojekt in JetBrains Rider und nenne es zum Beispiel FileEncodingExplorer.
using System;
using System.IO;
using System.Text;
string file = "ascii.txt";
string asciiText = "Hello, world!";
string cyrillicText = "Hallo, Welt!";
// Schreiben in ASCII
using var writer = new StreamWriter(file, false, Encoding.ASCII);
writer.WriteLine(asciiText);
writer.WriteLine(cyrillicText);
// Lesen aus ASCII
using var reader = new StreamReader(file, Encoding.ASCII);
string content = reader.ReadToEnd();
Console.WriteLine("Inhalt der Datei:");
Console.WriteLine(content);
Console.WriteLine("\nKyrillische Zeichen wurden durch '?' ersetzt, weil ASCII Kyrillisch nicht unterstützt!");
Wenn du diesen Code startest, wirst du sehen, dass der englische Teil korrekt gelesen wird, während der russische Teil in Fragezeichen (?) oder andere "unbekannte" Zeichen verwandelt wird. Das liegt daran, dass Encoding.ASCII nicht weiß, wie man kyrillische Zeichen in Bytes umwandelt, und sie einfach durch etwas "Sicheres" ersetzt (gewöhnlich ?), oder die Bytes, die andere Kodierungen für russische Buchstaben verwenden, von ASCII als andere Zeichen interpretiert werden. In unserem Fall ersetzt StreamWriter beim Schreiben Zeichen, die es in ASCII nicht gibt, zwangsweise durch ?. Das zeigt, warum es wichtig ist, die richtige Kodierung zu verwenden!
2. UTF-8: Der King des Internets und der Flexibilität
Wir kommen zu einer der wichtigsten und wohl beliebtesten Kodierungen heute — UTF-8. Das ist die Kodierung, auf der ein großer Teil des Internets, Linux-Systeme und viele moderne Anwendungen laufen.
Was ist das?
UTF-8 (Unicode Transformation Format - 8-bit) ist eine Unicode-Kodierung, die das Problem der Ineffizienz von UTF-16 für englischen Text löst. UTF-8 ist eine variable Länge-Kodierung mit einem schlauen Design:
- Zeichen, die normale ASCII-Zeichen sind (Codes von 0 bis 127), werden in einem Byte kodiert. Und das Beste: diese Bytes sind völlig identisch zu ihrer Darstellung in ASCII! Das bedeutet, dass UTF-8 rückwärtskompatibel mit ASCII ist.
- Alle anderen Zeichen werden mit 2 bis 4 Bytes kodiert:
- Kyrillisch — normalerweise 2 Bytes.
- Viele europäische Zeichen mit Diakritika, Arabisch, Hebräisch, Griechisch — 2 Bytes.
- Chinesische/japanische/koreanische Ideogramme — oft 3 Bytes.
- Seltene Zeichen und einige Emojis — 4 Bytes.
Beispiele für die Byte-Darstellung in UTF-8:
- Zeichen 'A' (ASCII): 01000001 (1 Byte)
- Zeichen 'я' (russisch): 11010001 10111111 (2 Bytes)
- Zeichen '€' (Euro): 11100010 10000010 10101100 (3 Bytes)
- Zeichen '😂' (Emoji): 11110000 10011111 10011000 10000010 (4 Bytes)
Warum ist UTF-8 der King?
- Effizienz: Sehr kompakt für Text mit vielen ASCII-Zeichen (typisch für englische Texte, Quellcode, Konfigurationsdateien).
- Rückwärtskompatibilität mit ASCII: Wenn du eine UTF-8-Datei mit nur ASCII-Zeichen liest, kannst du sie als ASCII lesen — und alles funktioniert.
- Meist kein BOM: Im Gegensatz zu UTF-16 verwendet UTF-8 normalerweise kein BOM. Falls es vorkommt (z. B. EF BB BF), ist das optional und kann manchmal Probleme machen (z. B. beim Parsen mancher Formate oder in Skripten auf Linux).
Nachteile:
- Die variable Länge kann einige Operationen erschweren (z. B. direktes Springen zum N-ten Zeichen ohne komplettes Scannen), aber in C# ist das kein Problem: string arbeitet mit Unicode-Zeichen unabhängig von der Datei-Kodierung.
Praktische Nutzung:
- Webseiten (HTML, CSS, JavaScript),
- APIs (JSON, XML),
- Konfigurationsdateien,
- Quellcode der meisten Programmiersprachen,
- Betriebssysteme der Linux/Unix-Familie.
Lass uns eine Datei in UTF-8 schreiben und lesen.
string file = "utf8.txt";
string text = "Hello, Welt! 😀 €";
// Schreiben in UTF-8 (standardmäßig ohne BOM)
File.WriteAllText(file, text, Encoding.UTF8);
// Lesen aus UTF-8
string readText = File.ReadAllText(file, Encoding.UTF8);
Console.WriteLine(readText); // Alles wird korrekt gelesen!
Wenn du diesen Code ausführst und die Dateigrößen vergleichst, wirst du sehen, dass utf8.txt für gemischten Text meist kleiner ist als eine Datei in UTF-16, und wenn der Text nur auf Englisch ist, ist die Größe vergleichbar mit ASCII.
3. UTF-16: Unicode für fast alle
Das Problem der "Babylonischen" Code Pages wurde zur echten Kopfschmerzquelle für Entwickler, besonders als Programme global wurden. Es brauchte eine universelle Lösung. Die kam in Form von Unicode. Unicode ist keine Kodierung per se, sondern eine riesige Tabelle, die jedem bekannten Zeichen einen eindeutigen numerischen Codepunkt (code point) zuweist.
Was ist das?
UTF-16 (Unicode Transformation Format - 16-bit) ist eine Kodierung, die ursprünglich annahm, dass alle Unicode-Zeichen in zwei Bytes (16 Bit) passen würden.
- Die meisten Zeichen (BMP, bis 65535) werden mit 2 Bytes kodiert.
- Für Zeichen außerhalb der BMP werden sog. Surrogatpaare verwendet — insgesamt 4 Bytes. Also ist auch UTF-16 eine variable-länge-Kodierung, wird aber oft als 2 Bytes pro Zeichen wahrgenommen.
Byte-Reihenfolge (Endianness) und BOM:
- Big-Endian (BE): das höherwertige Byte kommt zuerst.
- Little-Endian (LE): das niederwertige Byte kommt zuerst.
- Damit ein lesendes Programm die Reihenfolge versteht, wird oft ein BOM (Byte Order Mark) am Datei-Anfang gesetzt:
- Für UTF-16 LE: FF FE
- Für UTF-16 BE: FE FF
Vorteile:
- Unterstützt den Großteil der Zeichen der Welt.
- Einfach zu arbeiten mit Zeichen innerhalb der BMP (feste Länge 2 Bytes).
Nachteile:
- Nicht effizient für englischen Text: jedes ASCII-Zeichen belegt 2 Bytes.
- Das Vorhandensein eines BOM kann Probleme verursachen, wenn der Leser es nicht erwartet.
Praktische Nutzung:
UTF-16 wird intensiv innerhalb von Windows und z. B. in Java für die interne Darstellung von Strings verwendet. Textdateien aus dem Windows-Editor mit kyrillischem Text werden oft als UTF-16 LE mit BOM gespeichert.
string file = "utf16.txt";
string text = "Hello, Welt! 👋";
// Schreiben in UTF-16 (standardmäßig Little-Endian, mit BOM)
File.WriteAllText(file, text, Encoding.Unicode);
// Lesen aus UTF-16
string readText = File.ReadAllText(file, Encoding.Unicode);
Console.WriteLine(readText); // Alles wird korrekt gelesen!
Console.WriteLine($"Dateigröße: {new FileInfo(file).Length} Bytes");
Nach dem Ausführen wirst du sehen, dass alle Zeichen korrekt angezeigt werden. Dateien mit englischem Text in UTF-16 belegen grob doppelt so viel Platz wie in ASCII oder UTF-8 (für den ASCII-Bereich).
4. Zusammenfassende Vergleichstabelle der Kodierungen
Zur Systematisierung fassen wir die wichtigsten Eigenschaften in einer Tabelle zusammen.
| Kodierung | Minimale Bytes pro Zeichen | Maximale Bytes pro Zeichen | Kompatibilität mit ASCII (direkt) | Verwendet BOM (Standard in .NET) | Anwendungsbeispiele |
|---|---|---|---|---|---|
| ASCII | 1 | 1 | Vollständig | Nein | Alte Systeme, sehr einfache Textdaten, interne Protokolle |
| UTF-16 | 2 | 4 | Nein | Ja (Encoding.Unicode) | Internes String-Format in Windows, Java; Windows-Textdateien |
| UTF-8 | 1 | 4 | Vollständig | Nein (Encoding.UTF8 in .NET 5+); Ja (Encoding.UTF8 im .NET Framework) | Web (HTML, JSON), Konfigurationsdateien, Quellcode, Linux/Unix |
Kleine Anmerkung zu Encoding.UTF8 in .NET:
Historisch hat Encoding.UTF8 im .NET Framework standardmäßig ein BOM hinzugefügt. In modernem .NET (Core/5+) hat sich das Verhalten geändert: standardmäßig wird kein BOM angehängt. Wenn du eines brauchst, verwende new UTF8Encoding(true).
5. Wie man die Kodierung in C# angibt
Wie du in den Beispielen gesehen hast, geben wir StreamReader oder StreamWriter die gewünschte "Wortliste" mit, indem wir ihnen ein Objekt aus System.Text.Encoding übergeben.
System.Text.Encoding stellt fertige Varianten bereit:
- Encoding.ASCII: für die Arbeit mit ASCII.
- Encoding.Unicode: UTF-16 LE (mit BOM).
- Encoding.UTF8: UTF-8 (standardmäßig ohne BOM in modernem .NET).
Andere Kodierungen sind über Encoding.GetEncoding verfügbar (z. B. "windows-1251", "koi8-r"), aber unser Fokus liegt gerade auf Unicode.
// Schreiben in UTF-8
using var writer = new StreamWriter("my_file.txt", false, Encoding.UTF8);
writer.WriteLine("Irgendein Text.");
// Lesen aus UTF-16
using var reader = new StreamReader("another_file.txt", Encoding.Unicode);
string content = reader.ReadToEnd();
Console.WriteLine(content);
Das war's! StreamReader und StreamWriter übernehmen die komplette Arbeit der Umwandlung von Zeichen in Bytes und zurück, basierend auf den Regeln der gewählten Kodierung.
6. Probleme mit Kodierungen: "Kauderwelsch"
Wir haben die "Kauderwelsch"-Effekte schon gesehen, als wir russischen Text in ASCII geschrieben haben. Aber was passiert, wenn du eine Datei in einer Kodierung speicherst und mit einer anderen Kodierung liest? Dann beginnt der Spaß!
Stell dir vor, ein Text auf Russisch wurde in UTF-8 gespeichert, und dein Client versucht, ihn als CP1251 zu lesen. Die Bytefolgen werden falsch interpretiert, und statt "Привет, мир!" bekommst du Kauderwelsch (engl. mojibake).
Die Ursache ist immer dieselbe: Mismatch der Kodierungen beim Schreiben und Lesen. Benutze immer dieselbe Kodierung in beiden Schritten, sofern du nicht bewusst rekodierst.
string file = "mismatch.txt";
string russianText = "Hallo, Welt!";
// Schreiben in UTF-8 (richtig!)
File.WriteAllText(file, russianText, Encoding.UTF8);
// Falsches Lesen: wir versuchen, die UTF-8-Datei als ASCII zu lesen
string readAsAscii = File.ReadAllText(file, Encoding.ASCII);
Console.WriteLine($"Original: {russianText}");
Console.WriteLine($"Als ASCII gelesen: {readAsAscii}"); // Hier taucht das "Kauderwelsch" auf!
Starte das Programm und du wirst sehen, wie Kyrillisch in Fragezeichen oder andere sinnlose Zeichen verwandelt wird. In der nächsten Lektion sprechen wir darüber, wie man flexibler mit Kodierungen arbeitet und solche Probleme vermeidet — und wie man herausfindet, welche Kodierung eine Datei tatsächlich hat.
GO TO FULL VERSION