JavaRush /Курсы /C# SELF /Основные типы кодировок: U...

Основные типы кодировок: UTF-8, UTF-16, ASCII

C# SELF
37 уровень , 1 лекция
Открыта

1. Введение

Мы уже поняли, что как бы ни были умны компьютеры, они сами по себе не понимают, что такое буква "А" или символ "⌘". Они понимают нули с единицами, ну а для декодирования символов, понятных человеку, им нужен переводчик — кодировка.

История кодировок — это история компромиссов и эволюции. Сначала все было просто, потом стало сложнее, а потом, наконец, появился более-менее универсальный стандарт. Давайте пройдемся по этой хронологии.

Начало истории

Начнем с самых истоков. В прошлой лекции мы уже вспоминали прародителя текстовых кодировок — ASCII (произносится "аски"). Напомним, это расшифровывается как American Standard Code for Information Interchange, то есть Американский стандартный код для обмена информацией. Уже из названия понятно, для кого его делали, и почему он "американский".

ASCII была разработана в 1960-х годах и стала первым широко используемым стандартом кодирования символов. Она представляет собой набор из 128 символов:

  • Латинские буквы (заглавные и строчные): A-Z, a-z
  • Цифры: 0-9
  • Знаки препинания: .,!?"' и т.д.
  • Некоторые управляющие символы: перевод строки, табуляция и т.п.

Каждый из этих 128 символов кодировался одним байтом, используя всего 7 бит из 8 доступных (самый старший бит обычно оставался свободным или использовался для проверки ошибок). Это очень компактно и эффективно для английского языка.


Пример:
Символ 'A'  в ASCII кодируется как байт 0x41 (в двоичном виде 01000001)
Символ '!'  в ASCII кодируется как байт 0x21 (в двоичном виде 00100001)

Ограничения:
Главное ограничение ASCII очевидно: она заточена под английский язык. Если вы захотите написать что-то на русском ("Привет"), немецком ("Grüße") или китайском, ASCII вам не поможет. В её таблице просто нет этих символов. Это привело к появлению множества так называемых кодовых страниц (Code Pages), которые пытались расширить 128 символов ASCII до 256, используя тот самый восьмой бит. Например, для русского языка были кодовые страницы CP1251 (Windows Cyrillic), KOI8-R и другие. Но проблема в том, что эти кодовые страницы были несовместимы друг с другом: один и тот же байт в разных кодовых страницах мог означать совершенно разные символы! Настоящая Вавилонская башня.

Практическое использование сегодня:
Чистый формат ASCII редко используется для общих текстовых файлов сегодня, разве что для очень специфических нужд или в старых системах. Однако, его наследие живо: многие современные кодировки, как мы увидим, обратно совместимы с ASCII.

Давайте попробуем записать и прочитать что-то в ASCII, а потом попробуем добавить русские буквы, чтобы увидеть, что получится.

Создадим новый консольный проект в JetBrains Rider и назовем его, скажем, FileEncodingExplorer.

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

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

// Запись в ASCII
using var writer = new StreamWriter(file, false, Encoding.ASCII);
writer.WriteLine(asciiText);
writer.WriteLine(cyrillicText);

// Чтение из ASCII
using var reader = new StreamReader(file, Encoding.ASCII);
string content = reader.ReadToEnd();
Console.WriteLine("Содержимое файла:");
Console.WriteLine(content);

Console.WriteLine("\nКириллические буквы заменились на '?', потому что ASCII не поддерживает кириллицу!");

Когда вы запустите этот код, вы увидите, что английская часть текста читается нормально, а русская превратится в знаки вопроса (?) или другие "неизвестные" символы. Это потому, что Encoding.ASCII не знает, как преобразовать кириллические символы в байты, и просто заменяет их на что-то "безопасное" (обычно ?), или же байты, соответствующие русским буквам в какой-то другой кодировке, интерпретируются ASCII как какие-то другие символы. В нашем случае, StreamWriter принудительно заменяет символы, которых нет в ASCII, на ? при записи. Это демонстрация того, почему важно использовать правильную кодировку!

2. UTF-8: Король интернета и гибкости

И вот мы подходим к одной из самых важных и, пожалуй, самой популярной кодировке на сегодня — UTF-8. Это та кодировка, на которой работает большая часть интернета, Linux-системы и большинство современных приложений.

Что это такое?
UTF-8 (Unicode Transformation Format - 8-bit) — это еще одна кодировка Юникода, которая решает проблему неэффективности UTF-16 для английского текста. UTF-8 — это кодировка переменной длины, но с очень умным подходом:

  • Символы, которые являются обычными ASCII-символами (коды от 0 до 127), кодируются одним байтом. И самое крутое: эти байты абсолютно идентичны их представлению в ASCII! Это означает, что UTF-8 обратно совместим с ASCII.
  • Остальные символы кодируются от 2 до 4 байтов:
    • Кириллица — обычно 2 байта.
    • Многие европейские символы с диакритикой, арабские, иврит, греческие — 2 байта.
    • Китайские/японские/корейские иероглифы — часто 3 байта.
    • Редкие символы и некоторые эмодзи — 4 байта.

Примеры байтового представления в UTF-8:

  • Символ 'A' (ASCII): 01000001 (1 байт)
  • Символ 'я' (русская): 11010001 10111111 (2 байта)
  • Символ '€' (Евро): 11100010 10000010 10101100 (3 байта)
  • Символ '😂' (смайлик): 11110000 10011111 10011000 10000010 (4 байта)

Почему UTF-8 — король?

  1. Эффективность: Очень компактен для текста, который содержит много ASCII-символов (что характерно для английского языка, кода программ, конфигурационных файлов).
  2. Обратная совместимость с ASCII: Если вы читаете UTF-8 файл, содержащий только ASCII-символы, вы можете прочитать его как ASCII, и всё будет работать!
  3. Отсутствие BOM (обычно): В отличие от UTF-16, UTF-8 обычно не использует BOM. Если он и встречается (например, EF BB BF), то это необязательная "фича", которая иногда создаёт проблемы (например, при парсинге некоторых форматов или в скриптах Linux).

Недостатки:

  • Переменная длина символов может усложнять некоторые операции (например, переход к N-му символу без полного сканирования), но в C# это не проблема: string работает с Юникод-символами независимо от кодировки файла.

Практическое использование:

  • Веб-страницы (HTML, CSS, JavaScript),
  • API (JSON, XML),
  • Файлы конфигурации,
  • Исходный код большинства языков программирования,
  • ОС семейства Linux/Unix.

Давайте напишем и прочитаем файл в UTF-8.

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

// Запись в UTF-8 (по умолчанию без BOM)
File.WriteAllText(file, text, Encoding.UTF8);

// Чтение из UTF-8
string readText = File.ReadAllText(file, Encoding.UTF8);
Console.WriteLine(readText); // Всё читается правильно!

Когда вы запустите этот код и сравните размеры файлов, вы увидите, что utf8.txt для смешанного текста обычно меньше, чем файл в UTF-16, а если бы текст был только на английском, он был бы сопоставим по размеру с ASCII.

3. UTF-16: Юникод для всех, почти

Проблема "Вавилонской башни" кодовых страниц стала настоящей головной болью для разработчиков, особенно когда программы стали глобальными. Нужно было универсальное решение. И оно появилось — Unicode. Unicode — это не кодировка сама по себе, а огромная таблица, в которой каждому известному символу присвоен уникальный числовой код (code point).

Что это такое?
UTF-16 (Unicode Transformation Format - 16-bit) — кодировка, которая изначально предполагала, что все символы Юникода будут кодироваться двумя байтами (16 битами).

  • Большинство символов (BMP, до 65535) кодируется 2 байтами.
  • Для символов вне BMP используются суррогатные пары — 4 байта. То есть UTF-16 тоже переменной длины, но чаще воспринимается как 2 байта на символ.

Порядок байтов (Endianness) и BOM:

  • Big-Endian (BE): старший байт идёт первым.
  • Little-Endian (LE): младший байт идёт первым.
  • Чтобы программа-читатель поняла порядок, в начале файла часто ставят BOM (Byte Order Mark):
    • Для UTF-16 LE: FF FE
    • Для UTF-16 BE: FE FF
    В C# и Windows по умолчанию используется UTF-16 LE.

Преимущества:

  • Поддерживает подавляющее большинство символов мира.
  • Просто работать с символами в пределах BMP (фиксированная длина 2 байта).

Недостатки:

  • Неэффективен для английского текста: каждый ASCII-символ занимает 2 байта.
  • Наличие BOM может вызывать проблемы, если читатель его не ожидает.

Практическое использование:
UTF-16 широко используется внутри Windows и, например, в Java — для внутреннего представления строк. Текстовые файлы Блокнота Windows с кириллицей часто сохраняются как UTF-16 LE с BOM.

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

// Запись в UTF-16 (по умолчанию Little-Endian, с BOM)
File.WriteAllText(file, text, Encoding.Unicode);

// Чтение из UTF-16
string readText = File.ReadAllText(file, Encoding.Unicode);
Console.WriteLine(readText); // Всё читается правильно!

Console.WriteLine($"Размер файла: {new FileInfo(file).Length} байт");

После запуска вы увидите, что все символы отображаются правильно. Файлы с английским текстом в UTF-16 занимают примерно вдвое больше места, чем в ASCII или UTF-8 (для ASCII-диапазона).

4. Сводная таблица сравнения кодировок

Чтобы систематизировать наши знания, сведём ключевые характеристики в одну таблицу.

Кодировка Минимальное кол-во байт на символ Максимальное кол-во байт на символ Совместимость с ASCII (прямая) Использует BOM (по умолчанию в .NET) Примеры применения
ASCII 1 1 Полная Нет Старые системы, очень простые текстовые данные, внутренние протоколы
UTF-16 2 4 Нет Да (Encoding.Unicode) Внутреннее представление строк в Windows, Java; текстовые файлы Windows
UTF-8 1 4 Полная Нет (Encoding.UTF8 в .NET 5+); Да (Encoding.UTF8 в .NET Framework) Веб (HTML, JSON), конфигурационные файлы, исходный код, Linux/Unix

Маленькая заметка по Encoding.UTF8 в .NET:
Исторически в .NET Framework Encoding.UTF8 по умолчанию добавлял BOM. В современном .NET (Core/5+) поведение изменилось: по умолчанию BOM не добавляется. Если он нужен, используйте new UTF8Encoding(true).

5. Как указать кодировку в C#

Как вы уже могли заметить в примерах, чтобы сказать StreamReader или StreamWriter, какой "словарь" использовать, мы передаём им объект из класса System.Text.Encoding.

System.Text.Encoding предоставляет готовые варианты:

  • Encoding.ASCII: для работы с ASCII.
  • Encoding.Unicode: UTF-16 LE (с BOM).
  • Encoding.UTF8: UTF-8 (без BOM по умолчанию в современном .NET).

Другие кодировки доступны через Encoding.GetEncoding (например, "windows-1251", "koi8-r"), но фокус сейчас — на Unicode.

// Запись в UTF-8
using var writer = new StreamWriter("my_file.txt", false, Encoding.UTF8);
writer.WriteLine("Какой-то текст.");

// Чтение из UTF-16
using var reader = new StreamReader("another_file.txt", Encoding.Unicode);
string content = reader.ReadToEnd();
Console.WriteLine(content);

Вот и весь секрет! StreamReader и StreamWriter берут на себя всю работу по преобразованию символов в байты и обратно, используя правила выбранной кодировки.

6. Проблемы с кодировками: "Кракозябры"

Мы уже наблюдали "кракозябры", когда пытались записать русский текст в ASCII. Но что, если вы записали файл в одной кодировке, а пытаетесь прочитать в другой? Вот где начинается настоящее веселье!

Представьте, что письмо написано на русском и сохранено в UTF-8, а ваш клиент решает прочитать его как CP1251. Последовательности байтов будут интерпретированы неверно, и вместо "Привет, мир!" вы получите "кракозябры" (англ. mojibake).

Причина одна: несоответствие кодировок при записи и чтении. Всегда используйте одну и ту же кодировку на обеих стадиях, если только вы не занимаетесь осознанной перекодировкой.

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

// Запись в UTF-8 (правильно!)
File.WriteAllText(file, russianText, Encoding.UTF8);

// Неправильное чтение: пытаемся читать UTF-8 файл как ASCII
string readAsAscii = File.ReadAllText(file, Encoding.ASCII);

Console.WriteLine($"Оригинал: {russianText}");
Console.WriteLine($"Чтение как ASCII: {readAsAscii}"); // Тут и будут "кракозябры"!

Запустите программу, и вы увидите, как кириллица превращается в знаки вопроса или иные бессмысленные символы. В следующем уроке поговорим о том, как работать с кодировками более гибко и избежать подобных проблем, а также как понять, какая кодировка у файла, который вы читаете.

2
Задача
C# SELF, 37 уровень, 1 лекция
Недоступна
Сравнение размеров файлов для разных кодировок
Сравнение размеров файлов для разных кодировок
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ