1. Introdução
A gente já entendeu que, por mais inteligentes que sejam os computadores, eles não entendem por si só o que é a letra "A" ou o símbolo "⌘". Eles entendem zeros e uns, e para decodificar símbolos legíveis por humanos precisam de um tradutor — uma codificação.
A história das codificações é uma história de compromissos e evolução. No começo tudo era simples, depois ficou mais complicado, e finalmente surgiu um padrão mais ou menos universal. Vamos percorrer essa cronologia.
Começo da história
Vamos começar do início. Na aula anterior já lembramos do antecessor das codificações de texto — ASCII (lê-se "aski"). Lembre que isso se expande como American Standard Code for Information Interchange, ou seja, o código padrão americano para troca de informação. Pelo próprio nome já dá pra entender para quem foi feito e por que é "americano".
ASCII foi desenvolvida nos anos 1960 e se tornou o primeiro padrão amplamente usado para codificação de caracteres. Ela consiste em um conjunto de 128 caracteres:
- Letras latinas (maiúsculas e minúsculas): A-Z, a-z
- Números: 0-9
- Sinais de pontuação: .,!?"' etc.
- Alguns caracteres de controle: quebra de linha, tabulação, etc.
Cada um desses 128 caracteres era codificado em um byte, usando apenas 7 bits dos 8 disponíveis (o bit mais significativo normalmente ficava livre ou era usado para checagem). Isso é bem compacto e eficiente para inglês.
Exemplo:
O caractere 'A' em ASCII é codificado como o byte 0x41 (em binário 01000001)
O caractere '!' em ASCII é codificado como o byte 0x21 (em binário 00100001)
Limitações:
A principal limitação do ASCII é óbvia: ele foi pensado para inglês. Se você quiser escrever algo em russo ("Привет"), alemão ("Grüße") ou chinês, o ASCII não vai ajudar — esses caracteres não existem na sua tabela. Isso levou ao surgimento de várias code pages (páginas de código) que tentavam estender os 128 caracteres do ASCII para 256, usando o oitavo bit. Por exemplo, para russo existiam páginas de código como CP1251 (Windows Cyrillic), KOI8-R e outras. O problema é que essas páginas de código eram incompatíveis entre si: o mesmo byte em páginas diferentes podia representar caracteres totalmente distintos! Uma verdadeira Torre de Babel.
Uso prático hoje:
O formato puro ASCII é raro hoje em dia para arquivos de texto gerais, exceto para usos muito específicos ou em sistemas legados. No entanto, seu legado vive: muitas codificações modernas, como veremos, são compatíveis retroativamente com ASCII.
Vamos tentar escrever e ler algo em ASCII, e depois vamos adicionar letras russas para ver o que acontece.
Crie um novo projeto de console no JetBrains Rider e chame, por exemplo, FileEncodingExplorer.
using System;
using System.IO;
using System.Text;
string file = "ascii.txt";
string asciiText = "Hello, world!";
string cyrillicText = "Olá, mundo!";
// Gravação em ASCII
using var writer = new StreamWriter(file, false, Encoding.ASCII);
writer.WriteLine(asciiText);
writer.WriteLine(cyrillicText);
// Leitura em ASCII
using var reader = new StreamReader(file, Encoding.ASCII);
string content = reader.ReadToEnd();
Console.WriteLine("Conteúdo do arquivo:");
Console.WriteLine(content);
Console.WriteLine("\nOs caracteres cirílicos foram substituídos por '?', porque ASCII não suporta cirílico!");
Ao executar esse código você verá que a parte em inglês é lida normalmente, mas o texto em russo vira pontos de interrogação (?) ou outros símbolos "desconhecidos". Isso acontece porque Encoding.ASCII não sabe converter os caracteres cirílicos em bytes, e simplesmente os substitui por algo "seguro" (normalmente ?), ou então bytes que correspondem a letras russas em alguma outra codificação são interpretados como outros símbolos pelo ASCII. No nosso caso, o StreamWriter força a substituição dos caracteres que não existem no ASCII por ? durante a escrita. Isso mostra por que é importante usar a codificação correta!
2. UTF-8: O rei da internet e da flexibilidade
Chegamos a uma das codificações mais importantes e provavelmente a mais popular hoje — UTF-8. É a codificação usada pela maior parte da internet, sistemas Linux e a maioria das aplicações modernas.
O que é?
UTF-8 (Unicode Transformation Format - 8-bit) é mais uma codificação do Unicode que resolve a ineficiência do UTF-16 para textos em inglês. UTF-8 é uma codificação de comprimento variável, mas com uma sacada inteligente:
- Caracteres que são ASCII (códigos de 0 a 127) são codificados em um byte. E o melhor: esses bytes são idênticos à representação em ASCII! Isso significa que UTF-8 é retrocompatível com ASCII.
- Outros símbolos são codificados em 2 a 4 bytes:
- Cirílico — normalmente 2 bytes.
- Muitos símbolos europeus com diacríticos, árabe, hebraico, grego — 2 bytes.
- Ideogramas chinês/japonês/coreano — frequentemente 3 bytes.
- Símbolos raros e alguns emojis — 4 bytes.
Exemplos da representação em bytes no UTF-8:
- Caractere 'A' (ASCII): 01000001 (1 byte)
- Caractere 'я' (russo): 11010001 10111111 (2 bytes)
- Caractere '€' (Euro): 11100010 10000010 10101100 (3 bytes)
- Caractere '😂' (emoji): 11110000 10011111 10011000 10000010 (4 bytes)
Por que UTF-8 é o rei?
- Eficiência: É bem compacto para textos com muitos caracteres ASCII (como inglês, código fonte, arquivos de configuração).
- Compatibilidade retroativa com ASCII: Se você lê um arquivo UTF-8 que contém apenas ASCII, pode lê-lo como ASCII que tudo funciona.
- Normalmente sem BOM: Ao contrário do UTF-16, o UTF-8 geralmente não usa BOM. Se houver um BOM (por exemplo EF BB BF), é opcional e às vezes causa problemas (por exemplo, ao parsear certos formatos ou em scripts Linux).
Desvantagens:
- O comprimento variável pode complicar algumas operações (por exemplo, pular para o N-ésimo caractere sem escanear), mas em C# isso não é um problema: string já trabalha com pontos de código Unicode independentemente da codificação do arquivo.
Uso prático:
- Páginas web (HTML, CSS, JavaScript),
- APIs (JSON, XML),
- Arquivos de configuração,
- Código-fonte da maioria das linguagens,
- Sistemas Linux/Unix.
Vamos escrever e ler um arquivo em UTF-8.
string file = "utf8.txt";
string text = "Hello, mundo! 😀 €";
// Gravação em UTF-8 (por padrão sem BOM)
File.WriteAllText(file, text, Encoding.UTF8);
// Leitura em UTF-8
string readText = File.ReadAllText(file, Encoding.UTF8);
Console.WriteLine(readText); // Tudo é lido corretamente!
Ao executar esse código e comparar o tamanho dos arquivos, você verá que o utf8.txt para texto misto costuma ser menor que o arquivo em UTF-16, e se o texto fosse só em inglês ele teria tamanho parecido com o ASCII.
3. UTF-16: Unicode para quase tudo
O problema da "Torre de Babel" das páginas de código virou uma dor de cabeça para desenvolvedores, especialmente quando programas ficaram globais. Precisávamos de uma solução universal. E ela surgiu — Unicode. Unicode não é uma codificação por si só, é uma grande tabela que associa a cada símbolo conhecido um código numérico único (code point).
O que é?
UTF-16 (Unicode Transformation Format - 16-bit) é uma codificação que originalmente supunha que todos os pontos de código do Unicode seriam codificados em dois bytes (16 bits).
- A maioria dos caracteres (BMP, até 65535) é codificada em 2 bytes.
- Para pontos fora do BMP são usadas pares substitutos — 4 bytes. Ou seja, UTF-16 também é de comprimento variável, mas frequentemente é visto como 2 bytes por caractere.
Ordem de bytes (Endianness) e BOM:
- Big-Endian (BE): o byte mais significativo vem primeiro.
- Little-Endian (LE): o byte menos significativo vem primeiro.
- Para o leitor entender a ordem, frequentemente colocam-se no começo do arquivo um BOM (Byte Order Mark):
- Para UTF-16 LE: FF FE
- Para UTF-16 BE: FE FF
Vantagens:
- Suporta a grande maioria dos símbolos do mundo.
- Fácil trabalhar com caracteres dentro do BMP (comprimento fixo de 2 bytes).
Desvantagens:
- Ineficiente para textos em inglês: cada caractere ASCII ocupa 2 bytes.
- A presença do BOM pode causar problemas se o leitor não esperar por ele.
Uso prático:
UTF-16 é amplamente usado internamente no Windows e, por exemplo, no Java — para a representação interna de strings. Arquivos de texto do Notepad com caracteres cirílicos frequentemente são salvos como UTF-16 LE com BOM.
string file = "utf16.txt";
string text = "Hello, mundo! 👋";
// Gravação em UTF-16 (por padrão Little-Endian, com BOM)
File.WriteAllText(file, text, Encoding.Unicode);
// Leitura em UTF-16
string readText = File.ReadAllText(file, Encoding.Unicode);
Console.WriteLine(readText); // Tudo é lido corretamente!
Console.WriteLine($"Tamanho do arquivo: {new FileInfo(file).Length} bytes");
Após executar você verá que todos os caracteres aparecem corretamente. Arquivos com texto em inglês em UTF-16 ocupam aproximadamente o dobro do espaço que ocupariam em ASCII ou UTF-8 (no intervalo ASCII).
4. Tabela resumida de comparação de codificações
Para sistematizar, vamos colocar as características principais em uma tabela.
| Codificação | Mínimo de bytes por caractere | Máximo de bytes por caractere | Compatibilidade com ASCII (direta) | Usa BOM (por padrão no .NET) | Exemplos de uso |
|---|---|---|---|---|---|
| ASCII | 1 | 1 | Completa | Não | Sistemas antigos, dados de texto muito simples, protocolos internos |
| UTF-16 | 2 | 4 | Não | Sim (Encoding.Unicode) | Representação interna de strings no Windows, Java; arquivos de texto do Windows |
| UTF-8 | 1 | 4 | Completa | Não (Encoding.UTF8 no .NET 5+); Sim (Encoding.UTF8 no .NET Framework) | Web (HTML, JSON), arquivos de configuração, código-fonte, Linux/Unix |
Uma observação sobre Encoding.UTF8 no .NET:
Historicamente, no .NET Framework o Encoding.UTF8 adicionava um BOM por padrão. No .NET moderno (Core/5+) o comportamento mudou: por padrão o BOM não é adicionado. Se você precisa do BOM, use new UTF8Encoding(true).
5. Como especificar a codificação em C#
Como você já viu nos exemplos, para dizer ao StreamReader ou StreamWriter qual "dicionário" usar, passamos um objeto do tipo System.Text.Encoding.
System.Text.Encoding fornece opções prontas:
- Encoding.ASCII: para trabalhar com ASCII.
- Encoding.Unicode: UTF-16 LE (com BOM).
- Encoding.UTF8: UTF-8 (sem BOM por padrão no .NET moderno).
Outras codificações estão disponíveis via Encoding.GetEncoding (por exemplo, "windows-1251", "koi8-r"), mas o foco aqui é Unicode.
// Gravação em UTF-8
using var writer = new StreamWriter("my_file.txt", false, Encoding.UTF8);
writer.WriteLine("Algum texto.");
// Leitura em UTF-16
using var reader = new StreamReader("another_file.txt", Encoding.Unicode);
string content = reader.ReadToEnd();
Console.WriteLine(content);
É isso! StreamReader e StreamWriter cuidam de converter caracteres em bytes e bytes em caracteres, seguindo as regras da codificação escolhida.
6. Problemas com codificações: "caracteres estranhos" (кракозябры)
Já vimos "caracteres estranhos" quando tentamos gravar texto russo em ASCII. Mas e se você gravou um arquivo em uma codificação e tentou ler em outra? Aí começa a diversão de verdade!
Imagine que uma mensagem em russo foi salva em UTF-8, mas seu cliente tenta lê-la como CP1251. As sequências de bytes serão interpretadas de forma errada, e em vez de "Привет, мир!" você vai ver "caracteres estranhos" (em inglês mojibake).
A razão é simples: incompatibilidade entre codificação na escrita e na leitura. Sempre use a mesma codificação nas duas etapas, a menos que você esteja intencionalmente convertendo.
string file = "mismatch.txt";
string russianText = "Olá, mundo!";
// Gravação em UTF-8 (correto!)
File.WriteAllText(file, russianText, Encoding.UTF8);
// Leitura incorreta: tentamos ler um arquivo UTF-8 como ASCII
string readAsAscii = File.ReadAllText(file, Encoding.ASCII);
Console.WriteLine($"Original: {russianText}");
Console.WriteLine($"Leitura como ASCII: {readAsAscii}"); // É aí que aparecem os 'caracteres estranhos' (mojibake)!
Execute o programa e você verá como o cirílico vira pontos de interrogação ou símbolos sem sentido. Na próxima aula vamos ver como trabalhar com codificações de forma mais flexível e evitar esses problemas, além de como detectar qual codificação um arquivo usa.
GO TO FULL VERSION