Преобразование строки C# в шестнадцатеричное представление ее символов в кодировке UTF8

У меня есть строка на C#, которая может содержать любой набор символов Юникода, и я хочу преобразовать ее в шестнадцатеричное представление кодировки UTF8 этой строки с пробелом между каждым символом Юникода, например, строка "$£€𐍈" будет преобразовано в выходную строку «24 C2A3 E282AC F0908D88». Однако я не понимаю, как это сделать. Поскольку строки в C# — это UTF16, я не могу просто сказать foreach (char entry in myString) { ... }, потому что глиф Unicode может быть представлен либо 1, либо 2 char, как в случае с последним глифом в моем примере выше.

Я чувствую, что мне нужно получить byte[][], который представляет список символов, каждый из которых представлен в виде списка байтов в кодировке UTF8, определяющих символ. Затем я мог бы преобразовать эти байты в их шестнадцатеричное представление с пробелами между символами Юникода.

Как я мог достичь желаемого результата?

Как именно вы определяете «символ Юникода»? Это расширенный кластер графем (обычно согласованное определение того, что такое один редактируемый символ), или кодовая точка Юникода, или что-то еще?

canton7 30.04.2024 16:55

(Чтобы узнать код Юникода, посмотрите класс Rune. Для EGC посмотрите StringInfo)

canton7 30.04.2024 16:59

Кажется, что StringInfo даст мне TextElementEnumerator, чей метод GetTextElement позволит мне работать со строкой для каждого символа Юникода; но даже тогда, как мне преобразовать эти символы в их эквивалентные байты UTF8, особенно если это символ UTF16, занимающий 3 или 4 байта (в этом случае возвращаемый string.Length будет равен 2)?

Jez 30.04.2024 17:09

Предполагая, что под «символом Юникода» вы имеете в виду EGC, тогда: TextElementEnumerator дает вам список подстрок. Каждая подстрока содержит набор кодовых точек, составляющих этот EGC. Если вы хотите преобразовать одну из этих подстрок в байты UTF-8, используйте Encoding.UTF8.GetBytes

canton7 30.04.2024 17:10

См. вторую часть моего ответа здесь stackoverflow.com/a/66064611/1086121 для обсуждения того, как кодовые точки объединяются в EGC, на примере смайликов.

canton7 30.04.2024 17:15
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
5
105
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы можете минимизировать промежуточное выделение памяти, используя структуру 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();
    }
}

Примечания:

  • В более ранних версиях вы можете использовать StringInfo.GetTextElementEnumerator(String), однако использование этого метода приведет к выделению строки для каждого кластера графем,

Демо-рабочий пример №2 здесь.

canton7 сказал, что Rune не включает поддержку расширенных кластеров графем (я не претендую на то, что знаю, что это такое)...

Jez 30.04.2024 17:12

Опять же, все сводится к тому, что вы определяете как «символ Юникода». Если вы имеете в виду код, то Rune — это то, что вам нужно.

canton7 30.04.2024 17:15

@Jez - но это не то, о чем ты просил. В своем вопросе вы попросили «пробел между каждым символом Юникода». Вы этого хотите или вам нужно пространство только между кластерами?

dbc 30.04.2024 17:15

@Jez - ОК, я добавил версию, которая вставляет только пробел между кластерами графем.

dbc 30.04.2024 17:27

@dbc Для span, почему это не byte[6]? Результат, который я получаю от Encoding.UTF8.GetMaxByteCount(1), — 6. Кроме того, максимальное количество шестнадцатеричных символов в значении байта равно 2, так почему charSpan не является char[2]?

Jez 30.04.2024 17:55

@Jez - Никаких особенно веских причин. В какой-то момент у меня появилась привычка выравнивать небольшие буферы по 8 байт. Наверное, я просто показываю свой возраст. Я могу изменить это, если хочешь.

dbc 30.04.2024 18:03

Мне удобнее быть перфекционистом. :-) Это сработало, когда я изменил его на 6 и 2, просто хотел проверить, не упустил ли что-нибудь.

Jez 30.04.2024 18:08

@Jez - если подумать, здесь есть небольшая ошибка: Rune может представлять до 2 символов Юникода, поэтому мне нужно было использовать значение Encoding.UTF8.GetMaxByteCount(2) в качестве размера буфера. Зафиксированный.

dbc 30.04.2024 18:13

@dbc Странно то, что для символа 𐍈, который представляет собой двухсимвольную руну, значение 6 работало нормально. Кстати в твоем комментарии написано 9, а не 12 ;-)

Jez 30.04.2024 18:20

@Jez - Насколько я помню, Encoding.UTF8.GetMaxByteCount(2) возвращает больше, чем можно было бы ожидать, из-за возможности вставки резервных вариантов для символов, которые не могут быть закодированы. (Примечание: кодирование потенциально может быть с потерями.)

dbc 30.04.2024 18:25

@Jez - глядя на справочный источник , Rune.TryEncodeToUtf8(Rune value, Span<byte> destination, out int bytesWritten) не использует резервные варианты и не проверяет достоверность, поэтому максимальное количество символов, которые могут быть записаны, равно 4. Не стесняйтесь это изменить.

dbc 30.04.2024 18:34
Rune всегда представляет собой только один код Unicode — не существует такого понятия, как «двухсимвольная руна». Для представления некоторых кодовых точек требуются 2 кодовые единицы UTF-16 (т. е. два 2-байтовых символа), но это особенность кодировки UTF-16 и кодирования 149 878 различных кодовых точек с использованием единиц шириной 2 байта. Для кодирования любого кода UTF-8 требуется максимум 4 байта, см. en.wikipedia.org/wiki/UTF-8#Encoding
canton7 30.04.2024 19:08

@canton7 Это кажется различием без разницы. Если для кодирования требуется 4 байта, то это «2-символьная руна», т.е. для представления char в виде char требуется 2 значения C# char, это 16 бит.

Jez 05.05.2024 17:17

Руны @Jez представляют собой кодовые точки Unicode. Существует 1 114 112 различных кодовых точек, представляющих такие вещи, как буквы, символы, смайлы и т. д. Существует ряд различных стандартов представления кодовых точек с использованием байтов. UTF-16 использует 2 или 4 байта для представления каждой кодовой точки, UTF-32 всегда использует 4, UTF-8 использует 1–4. Таким образом, кодировка, количество байтов, представляет собой уровень абстракции, отличный от концепции кодовой точки (которая представляет собой просто число). Таким образом, говорить «4-байтовый код/руна» не имеет смысла без ссылки на кодировку. При использовании UTF-32 для кодирования всех кодовых точек требуется 4 байта!

canton7 05.05.2024 19:26

Другие вопросы по теме