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