在 C# 中解碼 UTF-8 意味著使用 .NET 執行階段的 Unicode 轉換機制,將位元組陣列轉成字串。標準做法是先準備 byte[] bytes,再呼叫 string text = Encoding.UTF8.GetString(bytes),這會套用 RFC 3629 所標準化的同一種 Unicode 轉換格式。C# 提供第二條路徑,透過 System.Text.UTF8Encoding,可以設定成在遇到無效位元組時拋出例外,而不是輸出 U+FFFD。兩條路徑都假設位元組序列確實是 UTF-8;如果來源原本是以 Windows-1252、Shift JIS、GBK 或其他舊式編碼產生,同一組位元組陣列繞一圈後會變成不同的字串。因此解碼是個兩步驟的問題:先挑一個符合錯誤處理策略的 C# 方法,再確認位元組一開始就是 UTF-8。本機瀏覽器解碼器讓 C# 開發者可以貼上位元組序列、將解碼後的文字與程式輸出比對,然後關閉分頁,不會有任何資料外洩。之所以需要驗證,是因為即便底層位元組無法忠實呈現來源,Encoding.UTF8 仍會回傳一個字串。

UTF-8 如何將碼點對應到位元組
UTF-8 是個可變寬度編碼:U+0000 到 U+10FFFF 之間每個有效的 Unicode 純量值都會以 1、2、3 或 4 個位元組寫成。U+0000 到 U+007F 的 ASCII 碼點維持單一位元組形式,這就是為什麼一份純英文文字檔在 UTF-8 和 ASCII 底下看起來一模一樣。U+0080 到 U+07FF 的碼點需要 2 個位元組;U+0800 到 U+FFFF 需要 3 個;U+10000 到 U+10FFFF 的輔助字元,包括大多數的表情符號,需要 4 個。這套編碼由 RFC 3629 定義,Unicode 17.0 標準第 3 章也列出相同的分界。
有幾個眾所周知的錨點,在你用肉眼檢視位元組序列時很有用。美元符號 U+0024 編碼為 0x24。大寫 A U+0041 編碼為 0x41。分幣符號 U+00A2 跨越雙位元組分界,編碼為 0xC2 0xA2。歐元符號 U+20AC 佔 3 個位元組:0xE2 0x82 0xAC。咧嘴笑臉表情符號 U+1F600 佔 4 個位元組:0xF0 0x9F 0x98 0x80。最大純量值 U+10FFFF 編碼為 0xF4 0x8F 0xBF 0xBF。分界案例 0x7F 和 0xE0 0xA0 0x80 標記出進入多位元組區段的轉折。這些錨點數值都不是工具計算出來的,而是 Unicode 聯盟和 RFC 3629 的正式定義。
一步步在 C# 中解碼 UTF-8
.NET 執行階段在 System.Text 命名空間下提供兩個實用的解碼器。第一個位於靜態 Encoding.UTF8 屬性,設定為寬鬆的回退機制:遇到無效位元組時會以 U+FFFD 取代。第二個是 UTF8Encoding 類別,接受一個建構函式旗標,可將無效位元組轉為拋出例外。兩者都以同一套 UTF-8 標準為目標,但回答的是不同問題:Encoding.UTF8 在問「我該顯示什麼文字?」,嚴格的 UTF8Encoding 則在問「這些位元組真的就是 UTF-8 嗎?」
- 從來源讀取位元組陣列:byte[] bytes = File.ReadAllBytes(path);或 byte[] bytes = Convert.FromBase64String(text);,端看資料從哪裡來。
- 呼叫寬鬆解碼器:string text = Encoding.UTF8.GetString(bytes);。它永遠會成功,並對任何無法解讀的位元組代以 U+FFFD,正是你在正式解碼環境中想避免的靜默行為。
- 在無法接受取代字元時,改用嚴格解碼器:var strict = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); string text = strict.GetString(bytes);。遇到格式錯誤的輸入會引發 DecoderFallbackException,而不是靜悄悄地產生 U+FFFD。
- 檢視結果。如果嚴格解碼器拋出例外,代表這些位元組不是 UTF-8。請回頭調查上游來源,而不是接住例外繼續往下走,因為吞掉例外會隱藏住和取代字元同樣的證據。
- 與獨立的驗證工具比對。本機瀏覽器解碼器接受同一組十六進位、十進位或二進位的位元組陣列,要嘛產生相符的文字,要嘛拒絕解碼。兩個獨立解碼器得到相同輸出,是位元組確實為 UTF-8 的有力證據。
在瀏覽器中驗證 C# 輸出
一個專門的 UTF-8 編碼/解碼工具 完全在當前分頁中執行,因此 C# 開發者可以貼上位元組序列、確認結果,再關掉分頁,過程中沒有任何位元組離開瀏覽器。此轉換器使用瀏覽器的 TextEncoder 與 fatal 模式的 TextDecoder,因此格式錯誤的輸入會被拒絕而非悄悄被取代,符合 UTF8Encoding(throwOnInvalidBytes: true) 的嚴格行為。操作步驟與該轉換器所記載的運作步驟一致:
- 在轉換器上選擇「UTF-8 位元組轉文字」,再挑選與你 C# 來源資料相符的表示法:十六進位、十進位或 8 位元二進位。
- 以所選表示法輸入位元組符記。十六進位接受以空格或逗號分隔的 1 或 2 位元位元組、可選的 0x 前綴,或一整段偶數長度的連續字串。十進位接受 0 到 255 之間的整數符記。二進位要求每個符記恰好 8 個字元。
- 按下轉換按鈕,讀取結果面板。面板會顯示解碼後的文字,並對格式錯誤的輸入回傳明確的錯誤訊息。
- 將解碼後的文字與 Encoding.UTF8.GetString 或你的嚴格 UTF8Encoding 實例所回傳的字串比對。任何差異都代表表示法弄錯、多了不該有的分隔符號,或是存在需要調查的非 UTF-8 位元組。
- 在刪除來源之前做一輪往返驗證。把解碼後的文字以同一種表示法重新編碼,確認位元組序列與原始輸入一致。如果一致,就代表原始位元組已經是有效的 UTF-8,你的 C# 程式碼可以放心信任。
為何致命解碼優於取代字元
C# 預設的 Encoding.UTF8 使用取代字元回退機制,對任何無法解析的位元組或序列代以 U+FFFD。當你想要顯示文字而不介意是否正確時,這很方便,但它會悄悄毀掉證據。一個收到亂碼位元組並將其印出的 C# 程式,仍會產生一串真實的 Unicode 字元;至於這些字元是否忠實呈現原始輸入,答案是「不」,但程式不會告訴你這件事。
致命解碼器把問題反過來。它會拒絕過長編碼(例如 0xC0 0xAF)、截斷序列(例如 0xE2 0x82 後沒有接續位元組)、孤立的接續位元組(例如 0xA2)、代理項編碼(例如 0xED 0xA0 0x80),以及超過 U+10FFFF 的值。瀏覽器轉換器透過 TextDecoder('utf-8', {fatal:true}) 跑同一種致命模式,並回傳明確錯誤。C# 開發者只要把 throwOnInvalidBytes: true 傳給 UTF8Encoding,就能得到同樣的特性。實務上的差異在於:致命解碼器會促使你回頭調查來源,而取代字元則是給你一張綠燈放行,讓你把損壞的文字送上線。
比較十六進位、十進位與二進位輸入
同一組位元組陣列會因所選的表示法不同而看起來不一樣。這些都不是不同的編碼;它們只是同一組位元組序列上的顯示層次,工具以三種方式序列化同一個陣列,而完全不更動其意義。
| 碼點 | 意義 | 十六進位位元組 | 十進位位元組 | 二進位位元組 |
|---|---|---|---|---|
| U+0024 | 美元符號 | 24 | 36 | 00100100 |
| U+0041 | 大寫 A | 41 | 65 | 01000001 |
| U+00A2 | 分幣符號 | C2 A2 | 194 162 | 11000010 10100010 |
| U+20AC | 歐元符號 | E2 82 AC | 226 130 172 | 11100010 10000010 10101100 |
| U+1F600 | 咧嘴笑臉 | F0 9F 98 80 | 240 159 152 128 | 11110000 10011111 10011000 10000000 |
| U+10FFFF | 最大純量值 | F4 8F BF BF | 244 143 191 191 | 11110100 10001111 10111111 10111111 |
解碼器會拒絕的輸入
有幾種常見的輸入看起來幾乎正確,但無法通過嚴格的 UTF-8 解碼,C# 開發者也應該預期嚴格的 UTF8Encoding 實例會得到同樣的結果:
- 過長編碼,例如 U+002F 的 0xC0 0xAF,某個值用比必要更多的位元組來表示。
- 截斷的多位元組序列,例如 0xE2 0x82 後面沒有接續位元組。
- 孤立的接續位元組,例如 0xA2 或 0xBF,出現時沒有對應的前導位元組。
- 代理項編碼,例如 0xED 0xA0 0x80,代表一個 UTF-16 代理值,而這不是有效的 Unicode 純量值。
- 超過 U+10FFFF 的值,超出已定義的最大純量值。
- 在編碼端,JavaScript 字串內部出現未配對的 UTF-16 代理碼位,當傳給 TextEncoder 時,工具會在編碼前予以拒絕,以免一個號稱無損的轉換悄悄改動輸入。
C# 的經驗法則很簡單:如果你無法證明位元組就是 UTF-8,就不要讓 Encoding.UTF8 把它們變成一個看起來合法的字串。使用嚴格建構函式、接住例外,並回頭找出原始來源。瀏覽器轉換器套用同一條規則,提供一個在本機執行的第二意見,文字輸入上限為 200,000 個 UTF-16 碼位,解碼表示法的輸入上限為 200,000 個位元組。更大的內容應該交給專門的二進位工具,而不是丟進互動式解碼器。
如果你正在權衡各種選項,Base100 安全編碼:精確到位元組的工作流程對此有詳細說明。
如果你正在權衡各種選項,在 Android 上不解安裝任何 App 就能解碼 Base58對此有詳細說明。