C# 從 UTF-8 流中讀取字元串的正確方法

我們下面的程式碼是從一個流 stream 中讀取 UTF-8 編碼的字元串。我們可以先考慮一下其中存在的潛在問題。

string ReadString(Stream stream)
{
    var sb = new StringBuilder();
    var buffer = new byte[4096];
    int readCount;
    while ((readCount = stream.Read(buffer)) > 0)
    {
        var s = Encoding.UTF8.GetString(buffer, 0, readCount);
        sb.Append(s);
    }

    return sb.ToString();
}

問題出在:某些情況下返回的字元串與與原始編碼的字元串並不同。
例如,笑臉符號😊 有時會被解碼為 4 個未知字元:

編碼字元串: 😊
解碼字元串: ????

我們知道:UTF-8 可以使用 1 到 4 個位元組來表示一個 Unicode 字元,有關字元串編碼的知識可以參考 ​​字元編碼​​​ 一文。

​​Stream.Read​​​ 方法可以把從 1 到​​ messageBuffer.Length​​​ 位元組返回,這意味著緩衝區可能包含不完整的 UTF-8 字元。

一旦緩衝區中的最後一個字元的 UTF-8 編碼不完整,那麼 ​​Encoding.UTF8.GetString​​ 就是轉換一個無效的 UTF-8 字元串。在這種情況下,該方法返回一個無效字元串,因為它無法猜測丟失的位元組。

我們使用以下程式碼演示以上行為:

var bytes = Encoding.UTF8.GetBytes("?");
// bytes = new byte[4] { 240, 159, 152, 138 }

var sb = new StringBuilder();
// 模擬逐個位元組地讀取數據流
for (var i = 0; i < bytes.Length; i++)
{
    sb.Append(Encoding.UTF8.GetString(bytes, i, 1));
}

Console.WriteLine(sb.ToString());
// "????" 代替了 "😊"

Encoding.UTF8.GetBytes(sb.ToString());
// new byte[12] { 239, 191, 189, 239, 191, 189, 239, 191, 189, 239, 191, 189 }
 

如何修復程式碼

有多種方法可以修復程式碼。
第一種方法:只有當你得到全部數據時,才將位元組數組轉換為字元串。

string ReadString(Stream stream)
{
    using var ms = new MemoryStream();
    var buffer = new byte[4096];
    int readCount;
    while ((readCount = stream.Read(buffer)) > 0)
    {
        ms.Write(buffer, 0, readCount);
    }

    return Encoding.UTF8.GetString(ms.ToArray());
}

第二種方法:可以把流包進一個具有正確編碼的 StreamReader 對象中。

string ReadString(Stream stream)
{
    using var sr = new StreamReader(stream, Encoding.UTF8);
    return sr.ReadToEnd();
}

另外,還可以使用System.Text.Decoder類來正確解碼緩衝區內的字元。在需要性能的情況下,可以使用PipeReader、Rune類來以記憶體優化的方式讀取數據。

參考資料:

  1. ​​字元編碼​ ​​
  2. C#教程​