1. Giới thiệu
Đã đến lúc bàn về những tình huống gây khó chịu, khi làm việc với tệp biến thành cuộc gặp gỡ với các lỗi (đôi khi khá bí ẩn). Nếu bạn từng gặp lỗi kiểu System.Text.DecoderFallbackException, thì bạn đã quen với chủ đề này không chỉ nghe kể thôi!
Trong bài này chúng ta sẽ xem xét:
- Các lỗi liên quan đến encoding trong .NET là những loại nào;
- Các tệp bị hỏng hoặc không đúng sẽ biểu hiện ra sao;
- Các ví dụ thực tế về cách bắt và xử lý những lỗi như vậy;
- Chú ý khi làm việc với tệp của người khác (hoặc những “tệp cũ tốt” tìm thấy trên ổ đĩa cổ).
Vậy nên, nếu ASCII quá đơn giản, còn Unicode quá thông minh, thỉnh thoảng bạn sẽ gặp những tệp mà chẳng ai đọc nổi. Và đây là lúc các ngoại lệ xuất hiện.
Tại sao chuyện này xảy ra?
Khi bạn mở tệp bằng StreamReader, chỉ định encoding (hoặc dùng encoding mặc định), .NET giả định rằng tất cả các byte trong tệp có thể chuyển thành ký tự đúng. Nhưng nếu tệp chứa các byte mà trong encoding đang dùng không ứng với ký tự nào, sẽ xảy ra lỗi giải mã.
2. Ngoại lệ khi đọc tệp với encoding sai
Ngoại lệ phổ biến nhất — DecoderFallbackException
Đây là ngoại lệ .NET ném khi không thể khớp một chuỗi byte với ký tự trong encoding mong đợi.
Ví dụ đơn giản để mọi thứ rõ ràng:
// Giả sử, tệp cũ ở Windows-1251 (ki-tự Cyrillic)
string win1251File = "win1251_test.txt";
File.WriteAllText(win1251File, "Privet, mir!", Encoding.GetEncoding("windows-1251"));
try
{
// Thử đọc tệp này như UTF-8
using var reader = new StreamReader(win1251File, Encoding.UTF8);
string content = reader.ReadToEnd();
Console.WriteLine(content); // ...và sẽ in ra các ký tự lạ (hoặc ném ngoại lệ)
}
catch (DecoderFallbackException ex)
{
Console.WriteLine("Lỗi giải mã: " + ex.Message);
}
Trong hầu hết trường hợp khi đọc tệp lưu bằng Windows-1251 như UTF-8, thay vì văn bản có nghĩa bạn sẽ nhận được chuỗi “kí tự rác”. Mặc định StreamReader trong trường hợp này thường không ném ngoại lệ mà thay thế bằng ký tự thay thế "�" cho các byte không hiểu được. Tuy nhiên nếu bạn cấu hình encoding với DecoderExceptionFallback cứng nhắc hoặc trong luồng gặp những byte “không tiêu hóa” được, sẽ xảy ra DecoderFallbackException.
DecoderFallbackException chi tiết
- Khi nào xảy ra: khi cố đọc một chuỗi byte không thể chuyển thành ký tự theo encoding hiện tại.
- Nên làm gì: đọc tệp bằng encoding đúng! Nếu bạn không biết tệp ở encoding nào, thử đoán (đôi khi có thể dựa vào BOM hoặc tên tệp) hoặc hỏi người tạo tệp.
3. Ví dụ với tệp bị hỏng rõ rệt
Giờ làm chuyện khó hơn. Giả sử tệp bị hỏng: trong chuỗi byte xuất hiện các đoạn ký tự bị cắt. Điều này xảy ra khi việc ghi tệp bị gián đoạn, lỗi mạng, chuyển đổi thất bại hoặc do... dao cắt giấy tờ theo nghĩa đen: tệp bị “cắt” lung tung.
Tạo tệp “bị hỏng”
// Ghi một chuỗi hợp lệ trong UTF-8
byte[] valid = Encoding.UTF8.GetBytes("Privet, mir!");
// Giờ tạo mảng byte sai (cắt mất phần của ký tự)
byte[] corrupted = new byte[valid.Length - 1];
Array.Copy(valid, corrupted, valid.Length - 1); // Cắt mất byte cuối cùng
// Lưu tệp
File.WriteAllBytes("corrupted.txt", corrupted);
try
{
using var reader = new StreamReader("corrupted.txt", Encoding.UTF8);
string s = reader.ReadToEnd();
Console.WriteLine("Văn bản đã đọc: " + s);
}
catch (DecoderFallbackException ex)
{
Console.WriteLine("Tệp bị hỏng! " + ex.Message);
}
Kết quả: .NET sẽ không thể ghép đúng ký tự cuối cùng. Mặc định nó sẽ thay bằng ký tự đặc biệt "�" (hoặc "?") hoặc, nếu encoding được cấu hình tương ứng, sẽ ném DecoderFallbackException.
4. Chiến lược fallback: có tránh được ngoại lệ không?
Đôi khi khi ký tự “không hiểu” xuất hiện, bạn không muốn ném lỗi mà muốn thay bằng “?” hoặc cái gì đó khác. Trong .NET có các chiến lược gọi là fallback cho vấn đề này.
Ví dụ: thay ký tự lỗi thay vì ném ngoại lệ
// Mảng với chuỗi không hợp lệ cho UTF-8
byte[] data = { 0xD0, 0x9F, 0xD1, 0x80, 0xD0, 0xB8, 0xD0, 0xB2, 0xD0, 0xB5, 0xD1, 0x82, 0xD1 }; // Byte cuối bị cắt
File.WriteAllBytes("broken_utf8.txt", data);
// Chiến lược fallback: thay ký tự lỗi bằng dấu hỏi
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("Văn bản (đã thay lỗi): " + s);
Kết quả: tệp sẽ được đọc thành văn bản mà các ký tự không xác định được thay bằng "?". Như vậy bạn tránh được crash chương trình, nhưng nội dung không còn hoàn toàn “nguyên vẹn”.
5. Vấn đề với BOM và không tương thích
Nhắc lại, BOM là Byte Order Mark, một chuỗi byte ở đầu tệp báo “xin chào, tôi ở encoding này!”.
Khi nào BOM gây rắc rối
- Nếu tệp có BOM mà ứng dụng không biết xử lý, ký tự đầu tiên sẽ kỳ lạ (ví dụ "" hoặc ký tự vô hình).
- Đôi khi thiếu BOM khiến xác định encoding sai.
Những ngoại lệ liên quan đến BOM
Thông thường C# xử lý BOM ổn khi đọc, nhưng nếu chỉ định encoding sai hoặc tự cắt BOM đi, bạn có nguy cơ gặp:
- Ký tự bất ngờ ở đầu (ví dụ ký tự "�");
- Ngoại lệ nếu encoding được cấu hình để ném và BOM bị coi là chuỗi byte không hợp lệ.
Lời khuyên thực tế: luôn chỉ định rõ encoding khi đọc/ghi nếu kiểu encoding quan trọng với bạn.
6. Những ngoại lệ và tình huống thú vị khác
Ghi bằng encoding sai
Khi bạn cố ghi một chuỗi chứa ký tự mà encoding đã chọn không hỗ trợ. Ví dụ, thử lưu emoji “:D” trong tệp dùng Encoding.ASCII:
try
{
using var writer = new StreamWriter("ascii.txt", false, Encoding.ASCII);
writer.WriteLine("Eto test 😊");
}
catch (EncoderFallbackException ex)
{
Console.WriteLine("Lỗi mã hóa: " + ex.Message);
}
Kết quả: bạn sẽ nhận được hoặc ngoại lệ EncoderFallbackException, hoặc ký tự bị thay bằng "?" — tùy chiến lược fallback bạn chọn.
Vấn đề khi chuyển đổi giữa các encoding (mất dữ liệu)
Khi chuyển đổi tệp có thể vô tình mất dữ liệu nếu encoding đích không có tất cả ký tự từ nguồn (ví dụ chuyển từ UTF-8 sang Windows-1251 với văn bản tiếng Nhật).
Tệp bị hỏng do ổ đĩa, mạng hoặc “chỉnh sửa thủ công”
Nếu tệp chứa các byte ngẫu nhiên hoặc bị hỏng (ví dụ sau hỏng ổ đĩa hoặc mở tệp nhị phân bằng trình soạn thảo văn bản), cố đọc tệp như văn bản thường ném ngoại lệ giải mã.
7. Cách bắt và xử lý lỗi trong thực tế?
Vì lỗi có thể xuất hiện ở nhiều giai đoạn khác nhau khi làm việc với tệp, khuyến nghị:
- Dùng khối try-catch để bắt ngoại lệ — trước hết là DecoderFallbackException và EncoderFallbackException;
- Đừng ngại thông báo cho người dùng: nếu tệp bị hỏng hoặc encoding “không đúng” — tốt hơn là nói rõ thay vì đưa ra văn bản lạ;
- Nếu có thể, tự động xác định encoding (ví dụ dựa trên BOM hoặc bằng thư viện như Ude), nhưng luôn cho người dùng tùy chọn chọn encoding khi tự động nhận diện thất bại.
Cấu trúc mã mẫu kiểu thường gặp:
try
{
using var reader = new StreamReader("file.txt", Encoding.GetEncoding("windows-1251"));
string s = reader.ReadToEnd();
Console.WriteLine(s);
}
catch (DecoderFallbackException ex)
{
Console.WriteLine($"Không thể đọc tệp: {ex.Message}");
// Có thể đề nghị người dùng thử encoding khác
}
catch (IOException ex)
{
Console.WriteLine($"Lỗi I/O: {ex.Message}");
}
8. Một chút về những “bẫy” thường gặp
Cố đọc tệp UTF-8 bằng Windows-1251: tốt nhất là bạn sẽ thấy “kí tự rác”, tệ hơn là nhận ngoại lệ (nếu encoding được cấu hình để ném).
Ghi tệp bằng ASCII với văn bản tiếng Nga: mọi thứ không thuộc bảng chữ cái tiếng Anh sẽ bị thay bằng "?" hoặc gây EncoderFallbackException.
Đọc tệp không có BOM như UTF-8, trong khi nó thực ra là UTF-16: bạn sẽ đọc được thứ linh tinh hoặc thậm chí không đọc được tệp.
Tệp không có encoding rõ ràng từ nguồn không đáng tin: luôn cảnh giác: ngay cả khi tệp mở mà “không lỗi”, không có nghĩa là nội dung đúng.
GO TO FULL VERSION