我认为我们可以做得更好,而不是在每次添加时天真地计算字符串的总长度。LINQ 很酷,但它可能会意外地鼓励低效的代码。如果我想要一个巨大的 UTF 字符串的前 80,000 个字节怎么办?这是很多不必要的计算。“我有 1 个字节。现在我有 2 个。现在我有 13 个……现在我有 52,384 个……”
这很愚蠢。大多数时候,至少在 l'anglais 中,我们可以准确地剪切那个nth
字节。即使在另一种语言中,我们距离一个好的切割点也只有不到 6 个字节。
所以我将从@Oren 的建议开始,即关闭UTF8 char 值的前导位。让我们从直接切割n+1th
字节开始,并使用 Oren 的技巧来确定我们是否需要提前切割几个字节。
三种可能
如果剪切后的第一个字节在0
前导位中有一个,我知道我在一个单字节(常规 ASCII)字符之前精确地剪切,并且可以干净地剪切。
如果我有一个11
跟随剪切,剪切后的下一个字节是一个多字节字符的开始,所以这也是一个剪切的好地方!
但是,如果我有一个10
,我知道我正处于一个多字节字符的中间,并且需要返回以查看它真正开始的位置。
也就是说,虽然我想在第 n 个字节之后剪切字符串,但如果第 n+1 个字节出现在多字节字符的中间,剪切会创建一个无效的 UTF8 值。我需要备份,直到我找到一个从它开始11
并在它之前切割的东西。
代码
注意:我正在使用类似Convert.ToByte("11000000", 2)
这样的东西,以便很容易分辨出我正在屏蔽哪些位(更多关于位掩码的信息)。简而言之,我要&
返回字节的前两位中的内容,并0
为其余部分返回 s。然后我检查XX
fromXX000000
以查看它是否是10
or 11
,在适当的情况下。
我今天发现C# 6.0 实际上可能支持二进制表示,这很酷,但我们现在将继续使用这个组合来说明正在发生的事情。
这PadLeft
只是因为我对控制台的输出过度强迫症。
因此,这里有一个函数,可以将您缩减为一个n
字节长的字符串或小于n
以“完整”UTF8 字符结尾的最大数字的字符串。
public static string CutToUTF8Length(string str, int byteLength)
{
byte[] byteArray = Encoding.UTF8.GetBytes(str);
string returnValue = string.Empty;
if (byteArray.Length > byteLength)
{
int bytePointer = byteLength;
// Check high bit to see if we're [potentially] in the middle of a multi-byte char
if (bytePointer >= 0
&& (byteArray[bytePointer] & Convert.ToByte("10000000", 2)) > 0)
{
// If so, keep walking back until we have a byte starting with `11`,
// which means the first byte of a multi-byte UTF8 character.
while (bytePointer >= 0
&& Convert.ToByte("11000000", 2) != (byteArray[bytePointer] & Convert.ToByte("11000000", 2)))
{
bytePointer--;
}
}
// See if we had 1s in the high bit all the way back. If so, we're toast. Return empty string.
if (0 != bytePointer)
{
returnValue = Encoding.UTF8.GetString(byteArray, 0, bytePointer); // hat tip to @NealEhardt! Well played. ;^)
}
}
else
{
returnValue = str;
}
return returnValue;
}
我最初把它写成一个字符串扩展。当然,只需添加前面的this
内容string str
即可将其恢复为扩展格式。我删除了,this
以便我们可以将该方法放入Program.cs
一个简单的控制台应用程序中进行演示。
测试和预期输出
这是一个很好的测试用例,它在下面创建输出,编写期望是Main
简单控制台应用程序的Program.cs
.
static void Main(string[] args)
{
string testValue = "12345“”67890”";
for (int i = 0; i < 15; i++)
{
string cutValue = Program.CutToUTF8Length(testValue, i);
Console.WriteLine(i.ToString().PadLeft(2) +
": " + Encoding.UTF8.GetByteCount(cutValue).ToString().PadLeft(2) +
":: " + cutValue);
}
Console.WriteLine();
Console.WriteLine();
foreach (byte b in Encoding.UTF8.GetBytes(testValue))
{
Console.WriteLine(b.ToString().PadLeft(3) + " " + (char)b);
}
Console.WriteLine("Return to end.");
Console.ReadLine();
}
输出如下。请注意,UTF8 中的“智能引号”testValue
长度为三个字节(尽管当我们以 ASCII 将字符写入控制台时,它会输出哑引号)。还要注意?
输出中每个智能引号的第二个和第三个字节的 s 输出。
我们的前五个字符testValue
是 UTF8 中的单字节,所以 0-5 字节值应该是 0-5 个字符。然后我们有一个三字节的智能引用,直到 5 + 3 个字节才能完整包含在内。果然,我们看到在调用8
. 时弹出。我们的下一个智能引号在 8 + 3 = 11 处弹出,然后我们回到 14 到单字节字符。
0: 0::
1: 1:: 1
2: 2:: 12
3: 3:: 123
4: 4:: 1234
5: 5:: 12345
6: 5:: 12345
7: 5:: 12345
8: 8:: 12345"
9: 8:: 12345"
10: 8:: 12345"
11: 11:: 12345""
12: 12:: 12345""6
13: 13:: 12345""67
14: 14:: 12345""678
49 1
50 2
51 3
52 4
53 5
226 â
128 ?
156 ?
226 â
128 ?
157 ?
54 6
55 7
56 8
57 9
48 0
226 â
128 ?
157 ?
Return to end.
所以这很有趣,我就在这个问题五周年之前。尽管 Oren 对这些位的描述有一个小错误,但这正是您想要使用的技巧。感谢您的提问;整洁的。