У меня есть строка на C#, которая может содержать любой набор символов Юникода, и я хочу преобразовать ее в шестнадцатеричное представление кодировки UTF8 этой строки с пробелом между каждым символом Юникода, например, строка "$£€𐍈" будет преобразовано в выходную строку «24 C2A3 E282AC F0908D88». Однако я не понимаю, как это сделать. Поскольку строки в C# — это UTF16, я не могу просто сказать foreach (char entry in myString) { ... }
, потому что глиф Unicode может быть представлен либо 1, либо 2 char
, как в случае с последним глифом в моем примере выше.
Я чувствую, что мне нужно получить byte[][]
, который представляет список символов, каждый из которых представлен в виде списка байтов в кодировке UTF8, определяющих символ. Затем я мог бы преобразовать эти байты в их шестнадцатеричное представление с пробелами между символами Юникода.
Как я мог достичь желаемого результата?
(Чтобы узнать код Юникода, посмотрите класс Rune
. Для EGC посмотрите StringInfo
)
Кажется, что StringInfo
даст мне TextElementEnumerator
, чей метод GetTextElement
позволит мне работать со строкой для каждого символа Юникода; но даже тогда, как мне преобразовать эти символы в их эквивалентные байты UTF8, особенно если это символ UTF16, занимающий 3 или 4 байта (в этом случае возвращаемый string.Length
будет равен 2)?
Предполагая, что под «символом Юникода» вы имеете в виду EGC, тогда: TextElementEnumerator
дает вам список подстрок. Каждая подстрока содержит набор кодовых точек, составляющих этот EGC. Если вы хотите преобразовать одну из этих подстрок в байты UTF-8, используйте Encoding.UTF8.GetBytes
См. вторую часть моего ответа здесь stackoverflow.com/a/66064611/1086121 для обсуждения того, как кодовые точки объединяются в EGC, на примере смайликов.
Вы можете минимизировать промежуточное выделение памяти, используя структуру Rune и используя промежуточные буферы, выделенные в стеке, например:
public static partial class TextHelper
{
public static string ToUtf8HexValues(this string s)
{
Span<byte> runeByteSpan = stackalloc byte[4]; // rune.EncodeToUtf8() can be up to 4 bytes as shown in https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs#L1060
Span<char> charSpan = stackalloc char[2]; // Greater than or equal to the max number of hex chars in a byte value, which is 2.
var sb = new StringBuilder();
foreach (var rune in s.EnumerateRunes())
{
if (sb.Length > 0)
sb.Append(' ');
for (int i = 0, length = rune.EncodeToUtf8(runeByteSpan); i < length; i++)
if (runeByteSpan[i].TryFormat(charSpan, out var n, "X"))
sb.Append(charSpan.Slice(0, n));
}
return sb.ToString();
}
}
Примечания:
Руна появилась в .NET Core 3. Эта структура:
Представляет скалярное значение Юникода ([ U+0000..U+D7FF] включительно; или [ U+E000..U+10FFFF] включительно).
Byte.TryFormat(Span<Char>, Int32, ReadOnlySpan<Char>, IFormatProvider) был представлен в .NET Core 2.1 и позволяет форматировать Byte
до фиксированной длины Span<char>
без выделения строки.
Демо-рабочий пример №1 здесь.
Если вы хотите, чтобы между кластерами графем , такими как Ĥ
, вставлялся только пробел, то (начиная с .NET 6) вы можете использовать StringInfo.GetNextTextElementLength(ReadOnlySpan<Char>) для перечисления по строке, как в кусках этот кластер, объединяющий символы вместе:
public static partial class TextHelper
{
public static IEnumerable<ReadOnlyMemory<char>> TextElements(this string s) => (s ?? "").AsMemory().TextElements();
public static IEnumerable<ReadOnlyMemory<char>> TextElements(this ReadOnlyMemory<char> s)
{
for (int index = 0, length = StringInfo.GetNextTextElementLength(s.Span);
length > 0;
index += length, length = StringInfo.GetNextTextElementLength(s.Span.Slice(index)))
yield return s.Slice(index, length);
}
public static string ToUtf8GraphemeHexValues(this string s)
{
Span<byte> runeByteSpan = stackalloc byte[4]; // rune.EncodeToUtf8() can be up to 4 bytes as shown in https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs#L1060
Span<char> charSpan = stackalloc char[2]; // Greater than or equal to the max number of hex chars in a byte value, which is 2.
var sb = new StringBuilder();
foreach (var chunk in s.TextElements())
{
if (sb.Length > 0)
sb.Append(' ');
foreach (var rune in chunk.Span.EnumerateRunes())
for (int i = 0, length = rune.EncodeToUtf8(runeByteSpan); i < length; i++)
if (runeByteSpan[i].TryFormat(charSpan, out var n, "X"))
sb.Append(charSpan.Slice(0, n));
}
return sb.ToString();
}
}
Примечания:
Демо-рабочий пример №2 здесь.
canton7 сказал, что Rune
не включает поддержку расширенных кластеров графем (я не претендую на то, что знаю, что это такое)...
Опять же, все сводится к тому, что вы определяете как «символ Юникода». Если вы имеете в виду код, то Rune
— это то, что вам нужно.
@Jez - но это не то, о чем ты просил. В своем вопросе вы попросили «пробел между каждым символом Юникода». Вы этого хотите или вам нужно пространство только между кластерами?
@Jez - ОК, я добавил версию, которая вставляет только пробел между кластерами графем.
@dbc Для span
, почему это не byte[6]
? Результат, который я получаю от Encoding.UTF8.GetMaxByteCount(1)
, — 6
. Кроме того, максимальное количество шестнадцатеричных символов в значении байта равно 2, так почему charSpan
не является char[2]
?
@Jez - Никаких особенно веских причин. В какой-то момент у меня появилась привычка выравнивать небольшие буферы по 8 байт. Наверное, я просто показываю свой возраст. Я могу изменить это, если хочешь.
Мне удобнее быть перфекционистом. :-) Это сработало, когда я изменил его на 6 и 2, просто хотел проверить, не упустил ли что-нибудь.
@Jez - если подумать, здесь есть небольшая ошибка: Rune
может представлять до 2 символов Юникода, поэтому мне нужно было использовать значение Encoding.UTF8.GetMaxByteCount(2)
в качестве размера буфера. Зафиксированный.
@dbc Странно то, что для символа 𐍈, который представляет собой двухсимвольную руну, значение 6 работало нормально. Кстати в твоем комментарии написано 9, а не 12 ;-)
@Jez - Насколько я помню, Encoding.UTF8.GetMaxByteCount(2)
возвращает больше, чем можно было бы ожидать, из-за возможности вставки резервных вариантов для символов, которые не могут быть закодированы. (Примечание: кодирование потенциально может быть с потерями.)
@Jez - глядя на справочный источник , Rune.TryEncodeToUtf8(Rune value, Span<byte> destination, out int bytesWritten)
не использует резервные варианты и не проверяет достоверность, поэтому максимальное количество символов, которые могут быть записаны, равно 4. Не стесняйтесь это изменить.
Rune
всегда представляет собой только один код Unicode — не существует такого понятия, как «двухсимвольная руна». Для представления некоторых кодовых точек требуются 2 кодовые единицы UTF-16 (т. е. два 2-байтовых символа), но это особенность кодировки UTF-16 и кодирования 149 878 различных кодовых точек с использованием единиц шириной 2 байта. Для кодирования любого кода UTF-8 требуется максимум 4 байта, см. en.wikipedia.org/wiki/UTF-8#Encoding@canton7 Это кажется различием без разницы. Если для кодирования требуется 4 байта, то это «2-символьная руна», т.е. для представления char
в виде char
требуется 2 значения C# char
, это 16 бит.
Руны @Jez представляют собой кодовые точки Unicode. Существует 1 114 112 различных кодовых точек, представляющих такие вещи, как буквы, символы, смайлы и т. д. Существует ряд различных стандартов представления кодовых точек с использованием байтов. UTF-16 использует 2 или 4 байта для представления каждой кодовой точки, UTF-32 всегда использует 4, UTF-8 использует 1–4. Таким образом, кодировка, количество байтов, представляет собой уровень абстракции, отличный от концепции кодовой точки (которая представляет собой просто число). Таким образом, говорить «4-байтовый код/руна» не имеет смысла без ссылки на кодировку. При использовании UTF-32 для кодирования всех кодовых точек требуется 4 байта!
Как именно вы определяете «символ Юникода»? Это расширенный кластер графем (обычно согласованное определение того, что такое один редактируемый символ), или кодовая точка Юникода, или что-то еще?