1. Вступ
Настав час поговорити про прикрі ситуації, коли робота з файлами обертається зустріччю з подекуди дуже загадковими помилками. Якщо ви коли-небудь стикалися з помилками на кшталт System.Text.DecoderFallbackException, тема вам уже знайома.
У цій лекції ми розглянемо:
- Які бувають помилки, повʼязані з кодуваннями у .NET;
- Як поводяться пошкоджені або некоректні файли;
- Практичні приклади перехоплення та обробки таких помилок;
- На що звернути увагу під час роботи з чужими файлами (або зі «старими добрими» файлами, знайденими на старому диску).
Отже, якщо ASCII був надто простим, а Unicode — надто розумним, інколи трапляються файли, які взагалі ніхто не вміє прочитати. Саме тоді й зʼявляються винятки.
Чому так стається?
Коли ви відкриваєте файл за допомогою StreamReader, указуючи кодування (або використовуючи кодування за замовчуванням), .NET припускає, що всі байти з цього файлу можна коректно перетворити на символи. Якщо ж файл містить байти, які в цьому кодуванні не відповідають жодному символу, виникає помилка декодування.
2. Винятки під час читання файлів із неправильним кодуванням
Найчастіший виняток — DecoderFallbackException
Цей виняток .NET викидає, коли не вдається зіставити послідовність байтів із символом в очікуваному кодуванні.
Простий приклад, щоб усе стало очевидним:
// Припустімо, старий файл у Windows-1251 (кирилиця)
string win1251File = "win1251_test.txt";
File.WriteAllText(win1251File, "Привіт, світ!", Encoding.GetEncoding("windows-1251"));
try
{
// Спробуємо прочитати цей файл як UTF-8
using var reader = new StreamReader(win1251File, Encoding.UTF8);
string content = reader.ReadToEnd();
Console.WriteLine(content); // і побачимо набір нерозбірних символів (або станеться виняток)
}
catch (DecoderFallbackException ex)
{
Console.WriteLine("Помилка декодування: " + ex.Message);
}
У більшості випадків під час читання файлу, збереженого в Windows-1251, як UTF-8 замість осмисленого тексту отримаєте набір нерозбірних символів. За замовчуванням StreamReader у таких ситуаціях не кидає виняток, а підставляє символ заміни "�" на місце незрозумілих байтів. Однак якщо явно налаштувати кодування з жорстким DecoderExceptionFallback або якщо в потоці трапляться особливо некоректні байти, буде згенеровано DecoderFallbackException.
DecoderFallbackException у деталях
- Коли виникає: під час спроби прочитати набір байтів, який не можна перетворити на символи поточного кодування.
- Що робити: читайте файл із правильним кодуванням. Якщо ви не знаєте, у якому кодуванні файл, спробуйте його визначити (інколи — за BOM або навіть за назвою файлу) або запитайте в того, хто цей файл створив.
3. Приклад із явно зіпсованим файлом
Тепер ускладнимо завдання. Уявімо, що файл пошкоджений: усередині послідовності байтів трапляються уривки недописаних символів. Таке буває під час переривання запису файлу, мережевих помилок, невдалих перетворень або під час роботи канцелярським ножем… у прямому сенсі: файл «порізано» де завгодно.
Створімо «пошкоджений» файл
// Запишемо коректний рядок у UTF-8
byte[] valid = Encoding.UTF8.GetBytes("Привіт, світ!");
// А тепер створимо неправильний масив байтів (обріжемо частину символу)
byte[] corrupted = new byte[valid.Length - 1];
Array.Copy(valid, corrupted, valid.Length - 1); // Відрізали останній байт
// Збережемо файл
File.WriteAllBytes("corrupted.txt", corrupted);
try
{
using var reader = new StreamReader("corrupted.txt", Encoding.UTF8);
string s = reader.ReadToEnd();
Console.WriteLine("Прочитаний текст: " + s);
}
catch (DecoderFallbackException ex)
{
Console.WriteLine("Файл пошкоджено! " + ex.Message);
}
Результат: .NET не зможе коректно зібрати останній символ. За замовчуванням він замінить його на спеціальний символ "�" (або "?") або, якщо кодування налаштоване відповідним чином, кине DecoderFallbackException.
4. Fallback-стратегії: чи можна уникнути винятку?
Іноді, коли символ не розпізнається, краще не завершуватися помилкою, а, наприклад, замінити його на «?» або щось інше. Для цього в .NET існують так звані fallback-стратегії.
Приклад: використовуємо заміну символу замість винятку
// Масив із неприпустимою послідовністю для UTF-8
byte[] data = { 0xD0, 0x9F, 0xD1, 0x80, 0xD0, 0xB8, 0xD0, 0xB2, 0xD0, 0xB5, 0xD1, 0x82, 0xD1 }; // Останній байт обрізано
File.WriteAllBytes("broken_utf8.txt", data);
// Fallback-стратегія: замінювати проблемний символ знаком питання
var encodingWithFallback = Encoding.GetEncoding(
"UTF-8",
new EncoderReplacementFallback("?"),
new DecoderReplacementFallback("?")
);
using var reader = new StreamReader("broken_utf8.txt", encodingWithFallback);
string s = reader.ReadToEnd();
Console.WriteLine("Текст (із заміною помилок): " + s);
Результат: із файлу буде прочитано текст із невідомими символами, заміненими на "?". Так ви уникаєте падіння програми, але отримуєте не зовсім «рідний» текст.
5. Проблеми з BOM і несумісністю
Нагадаємо: BOM — це Byte Order Mark, спеціальна послідовність байтів на початку файлу, яка ніби каже: «привіт, я маю таке кодування!».
Коли BOM може спричинити головний біль
- Якщо файл містить BOM, а застосунок не вміє його читати, то перший символ рядка буде незвичним (наприклад, "" або невидимим символом).
- Іноді відсутність BOM призводить до неправильного визначення кодування.
Винятки, повʼязані з BOM
Зазвичай C# уміє коректно обробляти BOM під час читання, але якщо вказати неправильне кодування або вручну обрізати BOM, ви ризикуєте отримати:
- Неочікуваний символ на початку (наприклад, символ "�");
- Виняток, якщо кодування налаштоване на кидання винятку, і BOM сприймається як некоректна послідовність байтів.
Практична порада: завжди явно вказуйте кодування під час читанні/записі, якщо для вас принциповий його тип.
6. Інші цікаві винятки та сценарії
Не те кодування під час запису
Коли ви намагаєтеся записати рядок, що містить символи, які не підтримуються вибраним кодуванням. Наприклад, спробуйте зберегти смайлик «😊» у файлі з Encoding.ASCII:
try
{
using var writer = new StreamWriter("ascii.txt", false, Encoding.ASCII);
writer.WriteLine("Це тест 😊");
}
catch (EncoderFallbackException ex)
{
Console.WriteLine("Помилка кодування: " + ex.Message);
}
Результат: отримаєте або виняток EncoderFallbackException, або символ буде замінено на "?" — залежить від вибраної fallback-стратегії кодування.
Проблема під час конвертації між кодуваннями (втрата даних)
Під час перетворення файлу можна ненароком втратити частину даних, якщо в цільовому кодуванні немає всіх символів із вихідного (наприклад, під час перетворення файлу з UTF-8 у Windows-1251, де трапляється японський текст).
Пошкодження файлу диском, мережею або «ручним редагуванням»
Якщо у файл потрапили випадкові або пошкоджені байти (наприклад, після збою диска або редагування бінарного файлу текстовим редактором), спроба прочитати такий файл часто спричинятиме винятки декодування.
7. Як перехоплювати й обробляти помилки на практиці?
Оскільки помилки можуть виникати на різних етапах роботи з файлами, рекомендується:
- Використовувати try-catch-блоки, щоб перехоплювати винятки — передусім DecoderFallbackException і EncoderFallbackException.
- Не соромтеся інформувати користувача: якщо файл пошкоджено або кодування «не те» — краще про це сказати, ніж видати дивний текст.
- За можливості автоматизуйте визначення кодування (наприклад, за BOM або за допомогою бібліотек, таких як Ude), але завжди давайте користувачеві змогу обрати кодування у разі невдачі.
Типова структура коду:
try
{
using var reader = new StreamReader("file.txt", Encoding.GetEncoding("windows-1251"));
string s = reader.ReadToEnd();
Console.WriteLine(s);
}
catch (DecoderFallbackException ex)
{
Console.WriteLine($"Файл не вдалося прочитати: {ex.Message}");
// Можна запропонувати користувачеві спробувати інше кодування
}
catch (IOException ex)
{
Console.WriteLine($"Помилка введення-виведення: {ex.Message}");
}
8. Трохи про «типові граблі»
Спроба прочитати файл у UTF-8 як Windows-1251: у кращому разі побачите набір нерозбірних символів, у гіршому — отримаєте виняток (якщо кодування налаштовано на його кидання).
Запис файлу в ASCII із кириличним текстом: усе, що не належить до англійської абетки, буде замінено на "?" або спричинить EncoderFallbackException.
Читання файлу без BOM як UTF-8, якщо це UTF-16: отримаєте абракадабру або навіть не зможете прочитати файл узагалі.
Файли без явного кодування з ненадійних джерел: завжди будьте насторожі: навіть якщо файл відкривається «без помилок», це не гарантує коректний результат.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ