CodeGym /Cursos /C# SELF /Principales tipos de codificación:

Principales tipos de codificación: UTF-8, UTF-16, ASCII

C# SELF
Nivel 37 , Lección 1
Disponible

1. Introducción

Ya entendimos que por muy listos que sean los ordenadores, ellos no saben por sí mismos qué es la letra "A" o el símbolo "⌘". Ellos entienden ceros y unos, y para convertir esos bits en símbolos comprensibles para humanos necesitan un traductor — la codificación.

La historia de las codificaciones es la historia de compromisos y evolución. Al principio todo era simple, luego se complicó y finalmente apareció un estándar más o menos universal. Vamos a repasar esa cronología.

Inicio de la historia

Empecemos por los orígenes. En la lección anterior ya recordamos al progenitor de las codificaciones de texto — ASCII (se pronuncia "ask-ee"). Recordemos, se desglosa como American Standard Code for Information Interchange, es decir el código estándar americano para el intercambio de información. Ya por el nombre se entiende para quién se diseñó y por qué es "americano".

ASCII se desarrolló en los años 60 y fue el primer estándar ampliamente usado para codificar caracteres. Representa un conjunto de 128 caracteres:

  • Letras latinas (mayúsculas y minúsculas): A-Z, a-z
  • Dígitos: 0-9
  • Signos de puntuación: .,!?"' etc.
  • Algunos caracteres de control: salto de línea, tabulación, etc.

Cada uno de esos 128 caracteres se codifica en un byte, usando sólo 7 bits de los 8 disponibles (el bit más alto normalmente se dejaba libre o se usaba para comprobación de errores). Es muy compacto y eficiente para el inglés.


Ejemplo:
El carácter 'A' en ASCII se codifica como el byte 0x41 (en binario 01000001)
El carácter '!' en ASCII se codifica como el byte 0x21 (en binario 00100001)

Limitaciones:
La mayor limitación de ASCII es evidente: está orientada al inglés. Si quieres escribir algo en cirílico ("Привіт"), alemán ("Grüße") o chino, ASCII no te va a ayudar. Esos símbolos no están en su tabla. Esto llevó a la aparición de muchas llamadas páginas de código (Code Pages), que intentaban ampliar los 128 símbolos de ASCII hasta 256 usando el octavo bit. Por ejemplo, para el cirílico existían las páginas CP1251 (Windows Cyrillic), KOI8-R y otras. Pero el problema era que esas páginas de código no eran compatibles entre sí: el mismo byte en diferentes code pages podía representar caracteres totalmente distintos. ¡Una verdadera torre de Babel!

Uso práctico hoy:
El formato ASCII puro se usa raramente hoy para archivos de texto generales, salvo para necesidades muy específicas o en sistemas antiguos. Sin embargo, su legado vive: muchas codificaciones modernas, como veremos, son compatibles hacia atrás con ASCII.

Vamos a intentar escribir y leer algo en ASCII, y luego añadir letras cirílicas para ver qué pasa.

Crearemos un proyecto de consola nuevo en JetBrains Rider y lo llamaremos, por ejemplo, FileEncodingExplorer.

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

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

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

// Lectura en ASCII
using var reader = new StreamReader(file, Encoding.ASCII);
string content = reader.ReadToEnd();
Console.WriteLine("Contenido del archivo:");
Console.WriteLine(content);

Console.WriteLine("\n¡Las letras cirílicas se reemplazaron por '?' porque ASCII no soporta el cirílico!");

Cuando ejecutes este código verás que la parte en inglés se lee bien, y la parte en cirílico se convierte en signos de interrogación (?) u otros símbolos "desconocidos". Esto pasa porque Encoding.ASCII no sabe cómo convertir caracteres cirílicos a bytes, y simplemente los reemplaza por algo "seguro" (normalmente ?), o bien los bytes que coinciden con caracteres cirílicos en otra codificación son interpretados por ASCII como otros caracteres. En nuestro caso, StreamWriter fuerza ese reemplazo cuando encuentra caracteres que no existen en ASCII. Esta es una demostración de por qué es importante usar la codificación correcta.

2. UTF-8: El rey de Internet y la flexibilidad

Y aquí llegamos a una de las codificaciones más importantes y probablemente la más popular hoy — UTF-8. Es la codificación en la que funciona gran parte de Internet, sistemas Linux y la mayoría de aplicaciones modernas.

¿Qué es?
UTF-8 (Unicode Transformation Format - 8-bit) es otra codificación de Unicode que resuelve la ineficiencia de UTF-16 para texto en inglés. UTF-8 es una codificación de longitud variable, pero con un enfoque bastante inteligente:

  • Los caracteres que son simples ASCII (códigos de 0 a 127) se codifican en un solo byte. Y lo mejor: esos bytes son idénticos a su representación en ASCII. Esto significa que UTF-8 es compatible hacia atrás con ASCII.
  • Los demás caracteres se codifican en 2 a 4 bytes:
    • El cirílico — normalmente 2 bytes.
    • Mucha Europa con diacríticos, árabe, hebreo, griego — 2 bytes.
    • Caracteres chinos/japoneses/coreanos — frecuentemente 3 bytes.
    • Caracteres raros y algunos emoji — 4 bytes.

Ejemplos de representación en bytes en UTF-8:

  • Símbolo 'A' (ASCII): 01000001 (1 byte)
  • Símbolo 'я' (cirílico): 11010001 10111111 (2 bytes)
  • Símbolo '€' (Euro): 11100010 10000010 10101100 (3 bytes)
  • Símbolo '😂' (emoji): 11110000 10011111 10011000 10000010 (4 bytes)

¿Por qué UTF-8 es el rey?

  1. Eficiencia: Muy compacto para texto que contiene muchos caracteres ASCII (como suele pasar con inglés, código fuente, archivos de configuración).
  2. Compatibilidad hacia atrás con ASCII: Si lees un archivo UTF-8 que sólo tiene caracteres ASCII, puedes leerlo como ASCII y funcionará perfectamente.
  3. Ausencia de BOM (normalmente): A diferencia de UTF-16, UTF-8 normalmente no usa BOM. Si aparece (por ejemplo EF BB BF), es una "feature" opcional que a veces causa problemas (por ejemplo al parsear ciertos formatos o en scripts de Linux).

Desventajas:

  • La longitud variable puede complicar algunas operaciones (por ejemplo, acceder al N-ésimo "carácter" sin escanear), pero en C# no es un problema: string trabaja con puntos de código Unicode independientemente de la codificación del archivo.

Uso práctico:

  • Páginas web (HTML, CSS, JavaScript),
  • APIs (JSON, XML),
  • Archivos de configuración,
  • Código fuente de la mayoría de lenguajes,
  • Sistemas operativos tipo Linux/Unix.

Vamos a escribir y leer un archivo en UTF-8.

string file = "utf8.txt";
string text = "Hello, mundo! 😀 €";

// Escritura en UTF-8 (por defecto sin BOM)
File.WriteAllText(file, text, Encoding.UTF8);

// Lectura en UTF-8
string readText = File.ReadAllText(file, Encoding.UTF8);
Console.WriteLine(readText); // ¡Todo se lee correctamente!

Cuando ejecutes este código y compares tamaños de archivo verás que utf8.txt para texto mixto suele ser más pequeño que el archivo en UTF-16, y si el texto fuera sólo en inglés sería comparable en tamaño con ASCII.

3. UTF-16: Unicode para casi todo

El problema de la "torre de Babel" de las páginas de código fue un dolor de cabeza para desarrolladores, especialmente cuando las aplicaciones se volvieron globales. Hacía falta una solución universal. Y apareció — Unicode. Unicode no es una codificación por sí misma, es una gran tabla donde a cada carácter conocido se le asigna un código único (code point).

¿Qué es?
UTF-16 (Unicode Transformation Format - 16-bit) es una codificación que originalmente asumía que todos los caracteres Unicode se codificarían en dos bytes (16 bits).

  • La mayoría de los caracteres (BMP, hasta 65535) se codifican en 2 bytes.
  • Para caracteres fuera del BMP se usan pares sustitutos — 4 bytes. Así que UTF-16 también es de longitud variable, aunque a menudo se percibe como 2 bytes por carácter.

Orden de bytes (Endianness) y BOM:

  • Big-Endian (BE): el byte más significativo va primero.
  • Little-Endian (LE): el byte menos significativo va primero.
  • Para que el lector entienda el orden, a menudo se pone un BOM al inicio del archivo:
    • Para UTF-16 LE: FF FE
    • Para UTF-16 BE: FE FF
    En C# y Windows por defecto se usa UTF-16 LE.

Ventajas:

  • Soporta la gran mayoría de caracteres del mundo.
  • Fácil de trabajar con caracteres dentro del BMP (longitud fija de 2 bytes).

Desventajas:

  • Poco eficiente para texto en inglés: cada carácter ASCII ocupa 2 bytes.
  • La presencia de BOM puede causar problemas si el lector no lo espera.

Uso práctico:
UTF-16 se usa mucho internamente en Windows y, por ejemplo, en Java — para la representación interna de strings. Los archivos de texto del Bloc de notas de Windows con cirílico a menudo se guardan como UTF-16 LE con BOM.

string file = "utf16.txt";
string text = "Hello, mundo! 👋";

// Escritura en UTF-16 (por defecto Little-Endian, con BOM)
File.WriteAllText(file, text, Encoding.Unicode);

// Lectura en UTF-16
string readText = File.ReadAllText(file, Encoding.Unicode);
Console.WriteLine(readText); // ¡Todo se lee correctamente!

Console.WriteLine($"Tamaño del archivo: {new FileInfo(file).Length} bytes");

Después de ejecutar verás que todos los caracteres se muestran correctamente. Los archivos con texto en inglés en UTF-16 ocupan aproximadamente el doble que en ASCII o UTF-8 (para el rango ASCII).

4. Tabla resumen comparativa de codificaciones

Para sistematizar lo aprendido, juntamos las características clave en una tabla.

Codificación Mínimo bytes por carácter Máximo bytes por carácter Compatibilidad con ASCII (directa) Usa BOM (por defecto en .NET) Ejemplos de uso
ASCII 1 1 Completa No Sistemas antiguos, datos de texto muy simples, protocolos internos
UTF-16 2 4 No Sí (Encoding.Unicode) Representación interna de strings en Windows, Java; archivos de texto de Windows
UTF-8 1 4 Completa No (Encoding.UTF8 en .NET 5+); Sí (Encoding.UTF8 en .NET Framework) Web (HTML, JSON), archivos de configuración, código fuente, Linux/Unix

Pequeña nota sobre Encoding.UTF8 en .NET:
Históricamente, en .NET Framework Encoding.UTF8 añadía BOM por defecto. En .NET moderno (Core/5+) el comportamiento cambió: por defecto no se añade BOM. Si lo necesitas, usa new UTF8Encoding(true).

5. Cómo indicar la codificación en C#

Como ya viste en los ejemplos, para decirle a StreamReader o StreamWriter qué "diccionario" usar, les pasamos un objeto del tipo System.Text.Encoding.

System.Text.Encoding ofrece variantes listas para usar:

  • Encoding.ASCII: para trabajar con ASCII.
  • Encoding.Unicode: UTF-16 LE (con BOM).
  • Encoding.UTF8: UTF-8 (sin BOM por defecto en .NET moderno).

Otras codificaciones están disponibles mediante Encoding.GetEncoding (por ejemplo, "windows-1251", "koi8-r"), pero aquí el foco está en Unicode.

// Escritura en UTF-8
using var writer = new StreamWriter("my_file.txt", false, Encoding.UTF8);
writer.WriteLine("Algún texto.");

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

¡Ese es todo el secreto! StreamReader y StreamWriter se ocupan de convertir caracteres a bytes y viceversa, usando las reglas de la codificación elegida.

6. Problemas con codificaciones: "mojibake" (símbolos corruptos)

Ya vimos "símbolos corruptos" cuando intentamos escribir texto cirílico en ASCII. Pero ¿qué pasa si escribes un archivo en una codificación y lo intentas leer con otra? Ahí empieza la verdadera diversión.

Imagina que un mensaje está escrito en cirílico y guardado en UTF-8, y tu cliente decide leerlo como CP1251. Las secuencias de bytes serán interpretadas incorrectamente, y en vez de "Привіт, світе!" obtendrás caracteres corruptos (en inglés mojibake).

La razón es una sola: incoherencia entre la codificación usada al escribir y la usada al leer. Usa siempre la misma codificación en ambas fases, salvo que hagas una recodificación intencionada.

string file = "mismatch.txt";
string ukrainianText = "Привіт, світе!";

// Escritura en UTF-8 (correcto)
File.WriteAllText(file, ukrainianText, Encoding.UTF8);

// Lectura incorrecta: intentamos leer un archivo UTF-8 como ASCII
string readAsAscii = File.ReadAllText(file, Encoding.ASCII);

Console.WriteLine($"Original: {ukrainianText}");
Console.WriteLine($"Lectura como ASCII: {readAsAscii}"); // ¡Ahí estarán los caracteres corruptos!

Ejecuta el programa y verás cómo el cirílico se convierte en signos de interrogación u otros símbolos sin sentido. En la siguiente lección hablaremos de cómo trabajar con codificaciones de forma más flexible para evitar estos problemas, y cómo detectar qué codificación tiene un archivo que vas a leer.

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