Я думаю, что мы можем добиться большего успеха, чем наивный подсчет общей длины строки при каждом добавлении. LINQ - это круто, но он может случайно поощрить неэффективный код. Что если бы я хотел получить первые 80 000 байтов гигантской строки UTF? Это лот ненужного подсчета. «У меня есть 1 байт. Теперь у меня есть 2. Теперь у меня есть 13 ... Теперь у меня есть 52,384 ...»
Это глупо. Большую часть времени, по крайней мере в l'anglais, мы можем сократить точно на этом nth
байте. Даже на другом языке мы находимся менее чем в 6 байтах от хорошей точки отсечения.
Итак, я собираюсь начать с предложения @ Oren, которое заключается в отключении старшего бита значения символа UTF8. Давайте начнем с сокращения прямо на n+1th
байт и используем трюк Орена, чтобы выяснить, нужно ли нам сократить несколько байт раньше.
Три возможности
Если первый байт после обрезки имеет 0
в начальном бите, я знаю, что я обрезаю точно перед одиночным байтовым (обычным ASCII) символом и могу обрезать чисто.
Если у меня есть 11
после вырезки, следующий байт после вырезки - это начало многобайтового символа, так что это тоже хорошее место для вырезки!
Однако, если у меня есть 10
, я знаю, что нахожусь в середине многобайтового символа, и мне нужно вернуться, чтобы проверить, где он действительно начинается.
То есть, хотя я хочу вырезать строку после n-го байта, если этот n + 1-й байт находится в середине многобайтового символа, при резании получится недопустимое значение UTF8. Мне нужно выполнить резервное копирование, пока я не доберусь до того, который начинается с 11
и обрезается непосредственно перед ним.
Код
Примечания: я использую такие вещи, как Convert.ToByte("11000000", 2)
, чтобы было легко определить, какие биты я маскирую (немного больше о битовой маскировке здесь ). Короче говоря, я &
собираюсь вернуть то, что находится в первых двух битах байта, и вернуть 0
s для остальных. Затем я проверяю XX
из XX000000
, чтобы увидеть, если это 10
или 11
, где это необходимо.
Я узнал сегодня , что C # 6.0 может фактически поддерживать двоичные представления , что круто, но мы пока будем использовать этот kludge, чтобы проиллюстрировать, что происходит.
* * * * * * * * * * * * * * * * * * * * * * 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();
}
Вывод следует. Обратите внимание, что «умные кавычки» в testValue
имеют длину три байта в UTF8 (хотя, когда мы записываем символы в консоль в 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.
Так что это довольно забавно, и я как раз перед пятилетней годовщиной вопроса. Хотя описание битов в Орене имело небольшую ошибку, это точно уловка, которую вы хотите использовать. Спасибо за вопрос; аккуратный.