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. 關於一些常見的陷阱
嘗試以 Windows-1251 讀取一個 UTF-8 檔案: 最好會看到亂碼,最糟可能會拋出例外(若該編碼被設定為會拋例外)。
把包含俄文的檔案寫成 ASCII: 凡是非英文字母的字元會被替換成 "?" 或導致 EncoderFallbackException。
把沒有 BOM 的檔案當成 UTF-8 來讀,但實際上是 UTF-16: 你會看到亂碼,或者根本讀不到正確內容。
來自不可靠來源且沒有明確編碼的檔案: 永遠保持警覺:就算檔案能被打開且沒拋錯,也不代表內容正確。
GO TO FULL VERSION