我們下面的代碼是從一個(gè)流 stream 中讀取 UTF-8 編碼的字符串。

我們可以先考慮一下其中存在的潛在問(wèn)題。

?

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();
}


問(wèn)題出在:某些情況下返回的字符串與與原始編碼的字符串并不同。

例如,笑臉?lè)?hào)有時(shí)會(huì)被解碼為 4 個(gè)未知字符:


原始字符串: ????
解碼字符串: ????


我們知道:UTF-8 可以使用 1 到 4 個(gè)字節(jié)來(lái)表示一個(gè) Unicode 字符,有關(guān)字符串編碼的知識(shí)可以參考 ??字符編碼??? 一文。


??Stream.Read??? 方法可以把從 1 到?? messageBuffer.Length??? 字節(jié)返回,這意味著緩沖區(qū)可能包含不完整的 UTF-8 字符。


一旦緩沖區(qū)中的最后一個(gè)字符的 UTF-8 編碼不完整,那么 ??Encoding.UTF8.GetString?? 就是轉(zhuǎn)換一個(gè)無(wú)效的 UTF-8 字符串。在這種情況下,該方法返回一個(gè)無(wú)效字符串,因?yàn)樗鼰o(wú)法猜測(cè)丟失的字節(jié)。


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


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

var sb = new StringBuilder();
// 模擬逐個(gè)字節(jié)地讀取數(shù)據(jù)流
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 }


如何修復(fù)代碼


有多種方法可以修復(fù)代碼。

第一種方法:只有當(dāng)你得到全部數(shù)據(jù)時(shí),才將字節(jié)數(shù)組轉(zhuǎn)換為字符串。


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());
}


第二種方法:可以把流包進(jìn)一個(gè)具有正確編碼的 StreamReader 對(duì)象中。


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


另外,還可以使用System.Text.Decoder類(lèi)來(lái)正確解碼緩沖區(qū)內(nèi)的字符。在需要性能的情況下,可以使用PipeReader、Rune類(lèi)來(lái)以?xún)?nèi)存優(yōu)化的方式讀取數(shù)據(jù)。


參考資料:

  1. ??字符編碼??
  2. ??C#教程??