Использование символов Unicode больше 2 байтов с .Net

Я использую этот код для генерации U+10FFFC

var s = Encoding.UTF8.GetString(new byte[] {0xF4,0x8F,0xBF,0xBC});

Я знаю это для частного использования и т.д., но он отображает один символ, как я ожидал бы при его отображении. Проблемы возникают при манипулировании этим символом юникода.

Если я потом сделаю это:

foreach(var ch in s)
{
    Console.WriteLine(ch);
}

Вместо того, чтобы печатать только один символ, он печатает два символа (т.е. строка, по-видимому, состоит из двух символов). Если я изменю свой цикл, чтобы добавить эти символы в пустую строку, например:

string tmp="";
foreach(var ch in s)
{
    Console.WriteLine(ch);
    tmp += ch;
}

В конце этого, tmp будет печатать только один символ.

Что именно здесь происходит? Я думал, что char содержит один символ Юникода, и мне никогда не приходилось беспокоиться о том, сколько байтов имеет символ, если я не делаю преобразование в байты. Мой реальный прецедент - я должен уметь обнаруживать, когда в строке используются очень большие символы Юникода. В настоящее время у меня есть что-то вроде этого:

foreach(var ch in s)
{
    if(ch>=0x100000 && ch<=0x10FFFF)
    {
        Console.WriteLine("special character!");
    }
}

Однако из-за этого разделения очень больших символов это не работает. Как я могу изменить это, чтобы заставить его работать?

Ответ 1

U + 10FFFC - это одна кодовая точка Юникода, но интерфейс string не предоставляет прямой код кодов Unicode. Его интерфейс предоставляет последовательность кодовых блоков UTF-16. Это очень низкоуровневое представление текста. Очень печально, что такое низкоуровневое представление текста было перенесено на наиболее очевидный и интуитивно понятный интерфейс... Я постараюсь не рассказать о том, как мне не нравится этот дизайн, и просто скажу, что не важно как печально, это просто (печальный) факт, с которым вам нужно жить.

Во-первых, я предлагаю использовать char.ConvertFromUtf32, чтобы получить исходную строку. Гораздо проще и понятнее:

var s = char.ConvertFromUtf32(0x10FFFC);

Итак, эта строка Length не 1, потому что, как я сказал, интерфейс имеет дело с кодовыми единицами UTF-16, а не с кодами Unicode. U + 10FFFC использует два кодовых блока UTF-16, поэтому s.Length равно 2. Все кодовые точки выше U + FFFF требуют для их представления двух кодовых блоков UTF-16.

Следует отметить, что ConvertFromUtf32 не возвращает char: char - это код кода UTF-16, а не кодовая точка Юникода. Чтобы иметь возможность возвращать все коды кода Unicode, этот метод не может вернуть один char. Иногда ему нужно вернуть два, и поэтому он делает его строкой. Иногда вы можете найти некоторые API, работающие в int вместо char, потому что int может использоваться для обработки всех кодовых точек (что то, что ConvertFromUtf32 принимает как аргумент, и что ConvertToUtf32 производит как результат).

string реализует IEnumerable<char>, что означает, что при повторении итерации по string вы получаете один кодовый блок UTF-16 за итерацию. Вот почему повторение строки и ее распечатка приводит к некоторому сломанному результату с двумя "вещами" в ней. Это два блока кода UTF-16, которые составляют представление U + 10FFFC. Их называют "суррогатами". Первый - суррогат высокого/свинцового, а второй - суррогат с низким/низким уровнем. Когда вы печатаете их отдельно, они не создают значимого вывода, потому что одиночные суррогаты даже не действуют в UTF-16, и они также не считаются символами Юникода.

Когда вы добавляете эти два суррогата к строке в цикле, вы фактически восстанавливаете суррогатную пару и печатаете эту пару позже, когда вы получаете правильный результат.

И в разглагольствовающем фронте обратите внимание, как ничто не жалуется на то, что вы использовали неправильную последовательность UTF-16 в этом цикле. Он создает строку с одиноким суррогатом, и все же все происходит так, как будто ничего не произошло: тип string не является даже типом хорошо сформированных UTF-16 последовательностей кода, но тип любой последовательности кода UTF-16.

Структура char предоставляет статические методы для обработки суррогатов: IsHighSurrogate, IsLowSurrogate, IsSurrogatePair, ConvertToUtf32 и ConvertFromUtf32, Если вы хотите, вы можете написать итератор, который выполняет итерацию по символам Unicode вместо кодовых блоков UTF-16:

static IEnumerable<int> AsCodePoints(this string s)
{
    for(int i = 0; i < s.Length; ++i)
    {
        yield return char.ConvertToUtf32(s, i);
        if(char.IsHighSurrogate(s, i))
            i++;
    }
}

Затем вы можете повторить так:

foreach(int codePoint in s.AsCodePoints())
{
     // do stuff. codePoint will be an int will value 0x10FFFC in your example
}

Если вы хотите получить каждую кодовую точку в виде строки, вместо этого измените тип возврата на IEnumerable<string> и строку доходности:

yield return char.ConvertFromUtf32(char.ConvertToUtf32(s, i));

С этой версией работает следующее:

foreach(string codePoint in s.AsCodePoints())
{
     Console.WriteLine(codePoint);
}

Ответ 2

Как уже было опубликовано Martinho, гораздо проще создать строку с этим приватным кодеком таким образом:

var s = char.ConvertFromUtf32(0x10FFFC);

Но для прокрутки двух char элементов этой строки бессмысленно:

foreach(var ch in s)
{
    Console.WriteLine(ch);
}

Зачем? Вы просто получите высокий и низкий суррогат, который кодирует код. Помните, что char - это 16-разрядный тип, поэтому он может содержать только максимальное значение 0xFFFF. Ваш код не подходит для 16-разрядного типа, и для самого высокого кода вам понадобится 21 бит (0x10FFFF), поэтому следующий более широкий тип будет просто 32-битным. Два элемента char не являются символами, а суррогатной. Значение 0x10FFFC кодируется в два суррогата.

Ответ 3

Пока @R. Ответ Martinho Fernandes правильный, его метод расширения AsCodePoints имеет две проблемы:

  • Он набрасывает ArgumentException на неверные кодовые точки (высокий суррогат без низкого суррогата или наоборот).
  • Вы не можете использовать статические методы char, которые принимают (char) или (string, int) (например, char.IsNumber()), если у вас есть только внутренние коды.

Я разделил код на два метода: один похожий на оригинал, но возвращает символ замены Unicode на недопустимые кодовые точки. Второй метод возвращает struct IEnumerable с более полезными полями:

StringCodePointExtensions.cs

public static class StringCodePointExtensions {

    const char ReplacementCharacter = '\ufffd';

    public static IEnumerable<CodePointIndex> CodePointIndexes(this string s) {
        for (int i = 0; i < s.Length; i++) {
            if (char.IsHighSurrogate(s, i)) {
                if (i + 1 < s.Length && char.IsLowSurrogate(s, i + 1)) {
                    yield return CodePointIndex.Create(i, true, true);
                    i++;
                    continue;

                } else {
                    // High surrogate without low surrogate
                    yield return CodePointIndex.Create(i, false, false);
                    continue;
                }

            } else if (char.IsLowSurrogate(s, i)) {
                // Low surrogate without high surrogate
                yield return CodePointIndex.Create(i, false, false);
                continue;
            }

            yield return CodePointIndex.Create(i, true, false);
        }
    }

    public static IEnumerable<int> CodePointInts(this string s) {
        return s
            .CodePointIndexes()
            .Select(
            cpi => {
                if (cpi.Valid) {
                    return char.ConvertToUtf32(s, cpi.Index);
                } else {
                    return (int)ReplacementCharacter;
                }
            });
    }
}

CodePointIndex.cs:

public struct CodePointIndex {
    public int Index;
    public bool Valid;
    public bool IsSurrogatePair;

    public static CodePointIndex Create(int index, bool valid, bool isSurrogatePair) {
        return new CodePointIndex {
            Index = index,
            Valid = valid,
            IsSurrogatePair = isSurrogatePair,
        };
    }
}

CC0

Насколько это возможно по закону, лицо, связавшее CC0 с этой работой, отказалось от всех авторских и смежных или смежных прав на эту работу.Суб >

Ответ 4

Еще одна альтернатива перечислять символы UTF32 в строке С# заключается в использовании метода System.Globalization.StringInfo.GetTextElementEnumerator, как в приведенном ниже коде.

public static class StringExtensions
{
    public static System.Collections.Generic.IEnumerable<UTF32Char> GetUTF32Chars(this string s)
    {
        var tee = System.Globalization.StringInfo.GetTextElementEnumerator(s);

        while (tee.MoveNext())
        {
            yield return new UTF32Char(s, tee.ElementIndex);
        }
    }
}

public struct UTF32Char
{
    private string s;
    private int index;

    public UTF32Char(string s, int index)
    {
        this.s = s;
        this.index = index;
    }

    public override string ToString()
    {
        return char.ConvertFromUtf32(this.UTF32Code);
    }

    public int UTF32Code {  get { return char.ConvertToUtf32(s, index); } }
    public double NumericValue { get { return char.GetNumericValue(s, index); } }
    public UnicodeCategory UnicodeCategory { get { return char.GetUnicodeCategory(s, index); } } 
    public bool IsControl { get { return char.IsControl(s, index); } }
    public bool IsDigit { get { return char.IsDigit(s, index); } }
    public bool IsLetter { get { return char.IsLetter(s, index); } }
    public bool IsLetterOrDigit { get { return char.IsLetterOrDigit(s, index); } }
    public bool IsLower { get { return char.IsLower(s, index); } }
    public bool IsNumber { get { return char.IsNumber(s, index); } }
    public bool IsPunctuation { get { return char.IsPunctuation(s, index); } }
    public bool IsSeparator { get { return char.IsSeparator(s, index); } }
    public bool IsSurrogatePair { get { return char.IsSurrogatePair(s, index); } }
    public bool IsSymbol { get { return char.IsSymbol(s, index); } }
    public bool IsUpper { get { return char.IsUpper(s, index); } }
    public bool IsWhiteSpace { get { return char.IsWhiteSpace(s, index); } }
}