1. Introduzione
Abbiamo già capito che per quanto i computer siano intelligenti, da soli non sanno cosa sia la lettera "A" o il simbolo "⌘". Capiscono zeri e uni, e per decodificare i simboli comprensibili all'uomo hanno bisogno di un traduttore — la codifica.
La storia delle codifiche è una storia di compromessi ed evoluzione. All'inizio era tutto semplice, poi è diventato più complesso e infine è apparso uno standard più o meno universale. Facciamo una panoramica cronologica.
Gli inizi della storia
Iniziamo dalle origini. Nella lezione precedente abbiamo già ricordato l'antenato delle codifiche testuali — ASCII (si pronuncia "aski"). Ricordiamo che questo acronimo sta per American Standard Code for Information Interchange, cioè codice standard americano per lo scambio di informazioni. Dal nome si capisce per chi è stato pensato e perché è "americano".
La ASCII è stata sviluppata negli anni '60 ed è diventata il primo standard ampiamente usato per la codifica dei caratteri. Rappresenta un insieme di 128 caratteri:
- Lettere latine (maiuscole e minuscole): A-Z, a-z
- Cifre: 0-9
- Segni di punteggiatura: .,!?"' ecc.
- Alcuni caratteri di controllo: newline, tabulazione, ecc.
Ognuno di questi 128 caratteri veniva codificato in un byte, usando solo 7 bit su 8 disponibili (il bit più significativo rimaneva spesso libero o usato per controllo errori). Questo è molto compatto ed efficiente per l'inglese.
Esempio:
Il carattere 'A' in ASCII è codificato come byte 0x41 (in binario 01000001)
Il carattere '!' in ASCII è codificato come byte 0x21 (in binario 00100001)
Limitazioni:
Il principale limite di ASCII è ovvio: è pensata per l'inglese. Se vuoi scrivere in russo ("Privet"), tedesco ("Grüße") o cinese, ASCII non ti aiuterà. Nella sua tabella semplicemente non ci sono quei caratteri. Questo ha portato alla nascita di molte cosiddette code pages (pagine di codice), che cercavano di estendere i 128 caratteri di ASCII a 256, usando l'ottavo bit. Per esempio, per il russo c'erano code page come CP1251 (Windows Cyrillic), KOI8-R e altre. Il problema era che queste code page non erano compatibili tra loro: lo stesso byte in pagine diverse poteva rappresentare caratteri totalmente diversi! Una vera Torre di Babele.
Uso pratico oggi:
Il formato puro ASCII è raramente usato per i file di testo oggi, salvo per esigenze molto specifiche o in sistemi legacy. Tuttavia il suo lascito è vivo: molte codifiche moderne, come vedremo, sono retrocompatibili con ASCII.
Proviamo a scrivere e leggere qualcosa in ASCII, poi proviamo ad aggiungere lettere russe per vedere cosa succede.
Creiamo un nuovo progetto console in JetBrains Rider e chiamiamolo, ad esempio, FileEncodingExplorer.
using System;
using System.IO;
using System.Text;
string file = "ascii.txt";
string asciiText = "Hello, world!";
string cyrillicText = "Privet, mir!";
// Scrittura in ASCII
using var writer = new StreamWriter(file, false, Encoding.ASCII);
writer.WriteLine(asciiText);
writer.WriteLine(cyrillicText);
// Lettura da ASCII
using var reader = new StreamReader(file, Encoding.ASCII);
string content = reader.ReadToEnd();
Console.WriteLine("Contenuto del file:");
Console.WriteLine(content);
Console.WriteLine("\nLe lettere cirilliche sono state sostituite con '?' perché ASCII non supporta il cirillico!");
Quando esegui questo codice vedrai che la parte inglese viene letta correttamente, mentre quella russa si trasformerà in punti interrogativi (?) o altri simboli "sconosciuti". Questo perché Encoding.ASCII non sa come convertire i caratteri cirillici in byte e li sostituisce con qualcosa di "sicuro" (di solito ?), oppure i byte corrispondenti alle lettere russe in qualche altra codifica vengono interpretati da ASCII come caratteri diversi. Nel nostro caso, StreamWriter sostituisce forzatamente i caratteri non rappresentabili in ASCII con ? durante la scrittura. È una dimostrazione del perché è importante usare la codifica corretta!
2. UTF-8: Il re di internet e della flessibilità
E arriviamo a una delle codifiche più importanti e probabilmente più popolari oggi — UTF-8. È la codifica che usa gran parte di internet, i sistemi Linux e la maggior parte delle applicazioni moderne.
Cos'è?
UTF-8 (Unicode Transformation Format - 8-bit) è un'altra codifica Unicode che risolve il problema dell'inefficienza di UTF-16 per il testo in inglese. UTF-8 è una codifica a lunghezza variabile, ma con un approccio molto intelligente:
- I caratteri che sono semplici ASCII (codici da 0 a 127) sono codificati in un singolo byte. E la cosa bella: questi byte sono esattamente gli stessi della rappresentazione in ASCII! Quindi UTF-8 è retrocompatibile con ASCII.
- Gli altri caratteri sono codificati da 2 a 4 byte:
- Il cirillico — di solito 2 byte.
- Molti caratteri europei con diacritici, arabo, ebraico, greco — 2 byte.
- Ideogrammi cinesi/giapponesi/coreani — spesso 3 byte.
- Caratteri rari e alcune emoji — 4 byte.
Esempi di rappresentazione in byte in UTF-8:
- Carattere 'A' (ASCII): 01000001 (1 byte)
- Carattere 'ya' (russo): 11010001 10111111 (2 byte)
- Carattere '€' (Euro): 11100010 10000010 10101100 (3 byte)
- Carattere '😂' (emoji): 11110000 10011111 10011000 10000010 (4 byte)
Perché UTF-8 è il "re"?
- Efficienza: Molto compatto per testi che contengono molti caratteri ASCII (tipico dell'inglese, del codice sorgente, dei file di configurazione).
- Retrocompatibilità con ASCII: Se leggi un file UTF-8 che contiene solo caratteri ASCII, puoi leggerlo come ASCII e tutto funzionerà!
- Assenza di BOM (di solito): Diversamente da UTF-16, UTF-8 di solito non usa il BOM. Se è presente (es. EF BB BF), è una "feature" opzionale che a volte crea problemi (per esempio quando si fa parsing di certi formati o in script Linux).
Svantaggi:
- La lunghezza variabile dei caratteri può complicare alcune operazioni (per esempio raggiungere il N-esimo carattere senza scansione completa), ma in C# questo non è un problema: la string lavora con caratteri Unicode indipendentemente dalla codifica del file.
Uso pratico:
- Pagine web (HTML, CSS, JavaScript),
- API (JSON, XML),
- File di configurazione,
- Codice sorgente della maggior parte dei linguaggi di programmazione,
- Sistemi operativi della famiglia Linux/Unix.
Scriviamo e leggiamo un file in UTF-8.
string file = "utf8.txt";
string text = "Hello, mir! 😀 €";
// Scrittura in UTF-8 (di default senza BOM)
File.WriteAllText(file, text, Encoding.UTF8);
// Lettura da UTF-8
string readText = File.ReadAllText(file, Encoding.UTF8);
Console.WriteLine(readText); // Tutto viene letto correttamente!
Quando esegui questo codice e confronti le dimensioni dei file, vedrai che utf8.txt per testo misto è solitamente più piccolo del file in UTF-16, e se il testo fosse solo in inglese sarebbe paragonabile in dimensione a ASCII.
3. UTF-16: Unicode per tutti, quasi
Il problema della Torre di Babele delle code page è diventato un vero mal di testa per gli sviluppatori, specialmente quando i programmi sono diventati globali. Serviva una soluzione universale. E questa è arrivata — Unicode. Unicode non è una codifica di per sé, ma una grande tabella che assegna a ogni carattere conosciuto un codice numerico unico (code point).
Cos'è?
UTF-16 (Unicode Transformation Format - 16-bit) è una codifica che inizialmente prevedeva che tutti i caratteri Unicode fossero codificati con due byte (16 bit).
- La maggior parte dei caratteri (BMP, fino a 65535) è codificata con 2 byte.
- Per i caratteri fuori dalla BMP si usano coppie surrogate — 4 byte. Quindi UTF-16 è anch'essa a lunghezza variabile, ma spesso è percepita come 2 byte per carattere.
Endianness e BOM:
- Big-Endian (BE): il byte più significativo viene prima.
- Little-Endian (LE): il byte meno significativo viene prima.
- Per far capire al programma il byte order, all'inizio del file spesso si mette il BOM (Byte Order Mark):
- Per UTF-16 LE: FF FE
- Per UTF-16 BE: FE FF
Vantaggi:
- Supporta la stragrande maggioranza dei caratteri del mondo.
- È comodo lavorare con i caratteri all'interno della BMP (lunghezza fissa di 2 byte).
Svantaggi:
- Non efficiente per testi in inglese: ogni carattere ASCII occupa 2 byte.
- La presenza del BOM può creare problemi se il lettore non lo aspetta.
Uso pratico:
UTF-16 è ampiamente usata internamente in Windows e, per esempio, in Java — per la rappresentazione interna delle stringhe. I file di testo del Blocco Note di Windows con testo cirillico spesso vengono salvati come UTF-16 LE con BOM.
string file = "utf16.txt";
string text = "Hello, mir! 👋";
// Scrittura in UTF-16 (di default Little-Endian, con BOM)
File.WriteAllText(file, text, Encoding.Unicode);
// Lettura da UTF-16
string readText = File.ReadAllText(file, Encoding.Unicode);
Console.WriteLine(readText); // Tutto viene letto correttamente!
Console.WriteLine($"Dimensione del file: {new FileInfo(file).Length} byte");
Dopo l'esecuzione vedrai che tutti i caratteri si mostrano correttamente. I file con testo inglese in UTF-16 occupano circa il doppio rispetto a ASCII o UTF-8 (per il range ASCII).
4. Tabella riassuntiva di confronto delle codifiche
Per sistematizzare le conoscenze, mettiamo le caratteristiche chiave in una tabella.
| Codifica | Minimo byte per carattere | Massimo byte per carattere | Compatibilità con ASCII (diretta) | Usa BOM (di default in .NET) | Esempi di uso |
|---|---|---|---|---|---|
| ASCII | 1 | 1 | Completa | No | Sistemi vecchi, dati testuali molto semplici, protocolli interni |
| UTF-16 | 2 | 4 | No | Sì (Encoding.Unicode) | Rappresentazione interna delle stringhe in Windows, Java; file di testo Windows |
| UTF-8 | 1 | 4 | Completa | No (Encoding.UTF8 in .NET 5+); Sì (Encoding.UTF8 in .NET Framework) | Web (HTML, JSON), file di configurazione, codice sorgente, Linux/Unix |
Una piccola nota su Encoding.UTF8 in .NET:
Storicamente in .NET Framework Encoding.UTF8 aggiungeva il BOM per default. Nel .NET moderno (Core/5+) il comportamento è cambiato: di default il BOM non viene aggiunto. Se serve, usa new UTF8Encoding(true).
5. Come specificare la codifica in C#
Come avrai notato negli esempi, per dire a StreamReader o StreamWriter quale "dizionario" usare, passiamo loro un oggetto della classe System.Text.Encoding.
System.Text.Encoding fornisce opzioni pronte all'uso:
- Encoding.ASCII: per lavorare con ASCII.
- Encoding.Unicode: UTF-16 LE (con BOM).
- Encoding.UTF8: UTF-8 (senza BOM per default nel .NET moderno).
Altre codifiche sono disponibili tramite Encoding.GetEncoding (per esempio, "windows-1251", "koi8-r"), ma qui ci concentriamo su Unicode.
// Scrittura in UTF-8
using var writer = new StreamWriter("my_file.txt", false, Encoding.UTF8);
writer.WriteLine("Qualche testo.");
// Lettura da UTF-16
using var reader = new StreamReader("another_file.txt", Encoding.Unicode);
string content = reader.ReadToEnd();
Console.WriteLine(content);
Questo è tutto il segreto! StreamReader e StreamWriter si occupano della conversione tra caratteri e byte, usando le regole della codifica scelta.
6. Problemi con le codifiche: "Krakozyabry"
Abbiamo già visto le "krakozyabry" quando abbiamo provato a scrivere testo russo in ASCII. Ma cosa succede se scrivi un file in una codifica e lo leggi con un'altra? Qui comincia il vero divertimento!
Immagina che una lettera sia scritta in russo e salvata in UTF-8, ma il tuo client decide di leggerla come CP1251. Le sequenze di byte saranno interpretate in modo sbagliato e invece di "Privet, mir!" otterrai "krakozyabry" (ingl. mojibake).
La ragione è una sola: disallineamento delle codifiche durante scrittura e lettura. Usa sempre la stessa codifica in entrambe le fasi, a meno che tu non stia facendo una ricodifica volontaria.
string file = "mismatch.txt";
string russianText = "Privet, mir!";
// Scrittura in UTF-8 (corretto!)
File.WriteAllText(file, russianText, Encoding.UTF8);
// Lettura errata: proviamo a leggere un file UTF-8 come ASCII
string readAsAscii = File.ReadAllText(file, Encoding.ASCII);
Console.WriteLine($"Originale: {russianText}");
Console.WriteLine($"Lettura come ASCII: {readAsAscii}"); // Qui appariranno le "krakozyabry"!
Esegui il programma e vedrai come il cirillico si trasforma in punti interrogativi o altri simboli senza senso. Nella prossima lezione parleremo di come lavorare con le codifiche in modo più flessibile per evitare questi problemi, e anche come capire quale codifica ha un file che stai leggendo.
GO TO FULL VERSION