CodeGym /Cours /C# SELF /Principaux types d'encodage :

Principaux types d'encodage : UTF-8, UTF-16, ASCII

C# SELF
Niveau 37 , Leçon 1
Disponible

1. Introduction

On a déjà compris que, aussi intelligents que soient les ordinateurs, ils ne "comprennent" pas d'eux-mêmes ce qu'est la lettre "A" ou le caractère "⌘". Ils comprennent des zéros et des uns, et pour décoder les caractères compréhensibles par un humain, ils ont besoin d'un traducteur — un encodage.

L'histoire des encodages est une histoire de compromis et d'évolution. Au début tout était simple, puis c'est devenu plus compliqué, et finalement un standard plus ou moins universel est apparu. Parcourons cette chronologie.

Les débuts

Commençons par les sources. Dans la leçon précédente on évoquait déjà l'ancêtre des encodages de texte — ASCII (prononcé "aski"). Rappel : c'est l'abréviation de American Standard Code for Information Interchange, donc le code standard américain pour l'échange d'information. Le nom montre pour qui il a été conçu et pourquoi il est "américain".

ASCII a été développé dans les années 1960 et est devenu le premier standard d'encodage de caractères largement utilisé. Il représente un ensemble de 128 caractères :

  • Lettres latines (majuscules et minuscules) : A-Z, a-z
  • Chiffres : 0-9
  • Ponctuation : .,!?"' etc.
  • Certaines commandes de contrôle : saut de ligne, tabulation, etc.

Chacun de ces 128 caractères était encodé sur un octet, utilisant seulement 7 bits sur 8 disponibles (le bit de poids fort était généralement libre ou utilisé pour la vérification d'erreurs). Très compact et efficace pour l'anglais.


Exemple :
Le caractère 'A' en ASCII est encodé comme l'octet 0x41 (en binaire 01000001)
Le caractère '!' en ASCII est encodé comme l'octet 0x21 (en binaire 00100001)

Limitations :
La principale limitation de ASCII est évidente : il est taillé pour l'anglais. Si vous voulez écrire en russe ("Привет"), en allemand ("Grüße") ou en chinois, ASCII ne vous aidera pas. Sa table ne contient pas ces caractères. Cela a conduit à l'apparition de nombreuses pages de codes (code pages) qui tentaient d'étendre les 128 caractères ASCII à 256 en utilisant le huitième bit. Par exemple, pour le russe il y avait les pages de codes CP1251 (Windows Cyrillic), KOI8-R et d'autres. Mais le problème, c'est que ces pages de codes étaient incompatibles entre elles : le même octet pouvait représenter des caractères complètement différents selon la page de codes ! Une vraie tour de Babel.

Utilisation pratique aujourd'hui :
Le format pur ASCII est rarement utilisé pour des fichiers texte généraux aujourd'hui, sauf pour des besoins très spécifiques ou des systèmes anciens. Son héritage est néanmoins vivant : de nombreux encodages modernes, comme nous le verrons, sont rétrocompatibles avec ASCII.

Essayons d'écrire et de lire quelque chose en ASCII, puis ajoutons des lettres russes pour voir ce que ça donne.

Créez un nouveau projet console dans JetBrains Rider et appelez-le, par exemple, FileEncodingExplorer.

using System;
using System.IO;
using System.Text;

string file = "ascii.txt";
string asciiText = "Hello, world!";
string cyrillicText = "Привет, мир!";

// Écriture en ASCII
using var writer = new StreamWriter(file, false, Encoding.ASCII);
writer.WriteLine(asciiText);
writer.WriteLine(cyrillicText);

// Lecture en ASCII
using var reader = new StreamReader(file, Encoding.ASCII);
string content = reader.ReadToEnd();
Console.WriteLine("Contenu du fichier :");
Console.WriteLine(content);

Console.WriteLine("\nLes lettres cyrilliques ont été remplacées par '?' parce que ASCII ne prend pas en charge le cyrillique !");

Quand vous exécuterez ce code, vous verrez que la partie anglaise est lisible normalement, et que le texte russe devient des points d'interrogation (?) ou d'autres caractères "inconnus". C'est parce que Encoding.ASCII ne sait pas comment convertir les caractères cyrilliques en octets et les remplace par quelque chose de "sûr" (généralement ?), ou les octets correspondant aux lettres russes dans un autre encodage sont interprétés par ASCII comme d'autres symboles. Dans notre cas, StreamWriter remplace de force les caractères absents de ASCII par ? lors de l'écriture. C'est une démonstration de pourquoi il est important d'utiliser le bon encodage !

2. UTF-8 : le roi d'internet et de la flexibilité

Nous arrivons à l'un des encodages les plus importants et probablement le plus populaire aujourd'hui — UTF-8. C'est l'encodage utilisé par une grande partie d'internet, les systèmes Linux et la plupart des applications modernes.

Qu'est-ce que c'est ?
UTF-8 (Unicode Transformation Format - 8-bit) est un autre encodage Unicode qui résout le problème d'inefficacité de UTF-16 pour le texte anglais. UTF-8 est un encodage à longueur variable, mais avec une approche très astucieuse :

  • Les caractères qui sont de simples caractères ASCII (codes de 0 à 127) sont encodés sur un octet. Et le meilleur : ces octets sont absolument identiques à leur représentation en ASCII ! Cela signifie que UTF-8 est rétrocompatible avec ASCII.
  • Les autres caractères sont encodés sur 2 à 4 octets :
    • Le cyrillique — généralement 2 octets.
    • Beaucoup de symboles européens avec diacritiques, arabe, hébreu, grec — 2 octets.
    • Les caractères chinois/яponais/coréens — souvent 3 octets.
    • Les symboles rares et certains émojis — 4 octets.

Exemples de représentation en octets en UTF-8 :

  • Caractère 'A' (ASCII) : 01000001 (1 octet)
  • Caractère 'я' (russe) : 11010001 10111111 (2 octets)
  • Caractère '€' (euro) : 11100010 10000010 10101100 (3 octets)
  • Caractère '😂' (emoji) : 11110000 10011111 10011000 10000010 (4 octets)

Pourquoi UTF-8 est-il le roi ?

  1. Efficacité : Très compact pour du texte contenant beaucoup de caractères ASCII (typique pour l'anglais, le code source, les fichiers de config).
  2. Rétrocompatibilité avec ASCII : Si vous lisez un fichier UTF-8 contenant seulement des caractères ASCII, vous pouvez le lire comme ASCII et tout fonctionne !
  3. Absence de BOM (généralement) : Contrairement à UTF-16, UTF-8 n'utilise généralement pas de BOM. S'il est présent (par exemple EF BB BF), c'est une "feature" optionnelle qui peut parfois poser problème (par ex. lors du parsing de certains formats ou dans des scripts Linux).

Inconvénients :

  • La longueur variable des caractères peut compliquer certaines opérations (par ex. sauter directement au N-ième caractère sans scanner), mais en C# ce n'est pas un problème : string travaille avec des caractères Unicode indépendamment de l'encodage du fichier.

Usage pratique :

  • Pages web (HTML, CSS, JavaScript),
  • API (JSON, XML),
  • Fichiers de configuration,
  • Code source de la plupart des langages,
  • Systèmes Linux/Unix.

Écrivons et lisons un fichier en UTF-8.

string file = "utf8.txt";
string text = "Hello, мир! 😀 €";

// Écriture en UTF-8 (par défaut sans BOM)
File.WriteAllText(file, text, Encoding.UTF8);

// Lecture en UTF-8
string readText = File.ReadAllText(file, Encoding.UTF8);
Console.WriteLine(readText); // Tout s'affiche correctement !

Quand vous exécuterez ce code et comparerez les tailles des fichiers, vous verrez que utf8.txt pour un texte mixte est généralement plus petit que le fichier en UTF-16, et si le texte était uniquement en anglais, sa taille serait comparable à celle en ASCII.

3. UTF-16 : Unicode pour tous, presque

Le problème de la "tour de Babel" des pages de codes est devenu un vrai casse-tête pour les développeurs, surtout quand les applis deviennent globales. Il fallait une solution universelle. Elle est arrivée — Unicode. Unicode n'est pas un encodage en soi, mais une énorme table où chaque caractère connu reçoit un code numérique unique (code point).

Qu'est-ce que c'est ?
UTF-16 (Unicode Transformation Format - 16-bit) est un encodage qui supposait initialement que tous les caractères Unicode seraient encodés sur deux octets (16 bits).

  • La plupart des caractères (BMP, jusqu'à 65535) sont encodés sur 2 octets.
  • Pour les caractères hors du BMP on utilise des paires substituts — 4 octets. Donc UTF-16 est aussi à longueur variable, mais on le perçoit souvent comme 2 octets par caractère.

Ordre des octets (endianness) et BOM :

  • Big-Endian (BE) : l'octet de poids fort vient en premier.
  • Little-Endian (LE) : l'octet de poids faible vient en premier.
  • Pour que le lecteur sache l'ordre, on place souvent au début du fichier un BOM (Byte Order Mark) :
    • Pour UTF-16 LE : FF FE
    • Pour UTF-16 BE : FE FF
    En C# et Windows l'usage par défaut est UTF-16 LE.

Avantages :

  • Prend en charge la grande majorité des caractères du monde.
  • Facile de travailler avec les caractères dans le BMP (longueur fixe de 2 octets).

Inconvénients :

  • Inefficace pour le texte anglais : chaque caractère ASCII occupe 2 octets.
  • La présence du BOM peut poser problème si le lecteur ne l'attend pas.

Usage pratique :
UTF-16 est largement utilisé à l'intérieur de Windows et, par exemple, en Java — pour la représentation interne des chaînes. Les fichiers texte du Notepad Windows avec du cyrillique sont souvent sauvegardés en UTF-16 LE avec un BOM.

string file = "utf16.txt";
string text = "Hello, мир! 👋";

// Écriture en UTF-16 (par défaut Little-Endian, avec BOM)
File.WriteAllText(file, text, Encoding.Unicode);

// Lecture en UTF-16
string readText = File.ReadAllText(file, Encoding.Unicode);
Console.WriteLine(readText); // Tout s'affiche correctement !

Console.WriteLine($"Taille du fichier : {new FileInfo(file).Length} octets");

Après exécution vous verrez que tous les caractères s'affichent correctement. Les fichiers contenant du texte anglais en UTF-16 occupent environ deux fois plus d'espace que sous ASCII ou UTF-8 (pour la plage ASCII).

4. Tableau récapitulatif des encodages

Pour systématiser nos connaissances, réunissons les caractéristiques clés dans un tableau.

Encodage Nombre minimum d'octets par caractère Nombre maximum d'octets par caractère Compatibilité avec ASCII (directe) Utilise un BOM (par défaut en .NET) Exemples d'utilisation
ASCII 1 1 Pleine Non Anciens systèmes, données texte très simples, protocoles internes
UTF-16 2 4 Non Oui (Encoding.Unicode) Représentation interne des chaînes sous Windows, Java ; fichiers texte Windows
UTF-8 1 4 Pleine Non (Encoding.UTF8 en .NET 5+) ; Oui (Encoding.UTF8 en .NET Framework) Web (HTML, JSON), fichiers de configuration, code source, Linux/Unix

Petite note sur Encoding.UTF8 en .NET :
Historiquement, dans .NET Framework, Encoding.UTF8 ajoutait par défaut un BOM. Dans le .NET moderne (Core/5+), le comportement a changé : par défaut le BOM n'est pas ajouté. Si vous en avez besoin, utilisez new UTF8Encoding(true).

5. Comment spécifier l'encodage en C#

Comme vous l'avez peut-être remarqué dans les exemples, pour dire à StreamReader ou StreamWriter quel "dictionnaire" utiliser, on leur passe un objet du type System.Text.Encoding.

System.Text.Encoding fournit des options prêtes à l'emploi :

  • Encoding.ASCII : pour travailler avec ASCII.
  • Encoding.Unicode : UTF-16 LE (avec BOM).
  • Encoding.UTF8 : UTF-8 (sans BOM par défaut dans le .NET moderne).

D'autres encodages sont disponibles via Encoding.GetEncoding (par ex. "windows-1251", "koi8-r"), mais l'accent ici est sur Unicode.

// Écriture en UTF-8
using var writer = new StreamWriter("my_file.txt", false, Encoding.UTF8);
writer.WriteLine("Quelque texte.");

// Lecture en UTF-16
using var reader = new StreamReader("another_file.txt", Encoding.Unicode);
string content = reader.ReadToEnd();
Console.WriteLine(content);

Voilà le secret ! StreamReader et StreamWriter font tout le boulot de conversion entre caractères et octets, en suivant les règles de l'encodage choisi.

6. Problèmes d'encodage : les "mojibake"

Nous avons déjà vu des "mojibake" quand on essayait d'écrire du texte russe en ASCII. Mais que se passe-t-il si vous écrivez un fichier dans un encodage et que vous essayez de le lire dans un autre ? Là commence le vrai spectacle !

Imaginez qu'une lettre est écrite en russe et sauvegardée en UTF-8, et que votre client décide de la lire comme CP1251. Les séquences d'octets seront interprétées incorrectement, et au lieu de "Привет, мир!" vous obtiendrez des "mojibake" (angl. mojibake).

La raison est simple : décalage d'encodage entre l'écriture et la lecture. Utilisez toujours le même encodage aux deux étapes, sauf si vous faites une recodification consciente.

string file = "mismatch.txt";
string russianText = "Привет, мир!";

// Écriture en UTF-8 (correct !)
File.WriteAllText(file, russianText, Encoding.UTF8);

// Lecture incorrecte : on essaie de lire un fichier UTF-8 comme ASCII
string readAsAscii = File.ReadAllText(file, Encoding.ASCII);

Console.WriteLine($"Original : {russianText}");
Console.WriteLine($"Lecture comme ASCII : {readAsAscii}"); // Voilà les "mojibake" !

Exécutez le programme, et vous verrez comment le cyrillique se transforme en points d'interrogation ou en autres caractères sans sens. Dans la leçon suivante, nous verrons comment travailler avec les encodages de manière plus flexible pour éviter ces problèmes, et comment détecter l'encodage d'un fichier que vous devez lire.

Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION