Использование символов Юникода размером более 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!");
    }
}

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


person Earlz    schedule 29.05.2013    source источник


Ответы (4)


U+10FFFC — это одна кодовая точка Unicode, но интерфейс 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. Чтобы иметь возможность возвращать все кодовые точки Unicode, этот метод не может возвращать один char. Иногда ему нужно вернуть два, и поэтому он делает его строкой. Иногда вы найдете некоторые API, работающие с ints вместо char, потому что int также может использоваться для обработки всех кодовых точек (это то, что ConvertFromUtf32 принимает в качестве аргумента, и что ConvertToUtf32 создает в качестве результата).

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

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

И в начале разглагольствований обратите внимание, что ничто не жалуется на то, что вы использовали искаженную последовательность UTF-16 в этом цикле. Он создает строку с единственным суррогатом, но все продолжается как ни в чем не бывало: тип string — это даже не тип правильных последовательностей кодовых единиц UTF-16, а тип < strong>любая последовательность кодовых единиц 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);
}
person R. Martinho Fernandes    schedule 29.05.2013

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

var s = char.ConvertFromUtf32(0x10FFFC);

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

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

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

person brighty    schedule 17.06.2014

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

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

Я разделил код на два метода, один из которых похож на исходный, но возвращает замену Unicode. Символ в недопустимых кодовых точках. Второй метод возвращает структуру 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 с этим произведением, отказывается от всех авторских и смежных или смежных прав на это произведение.

person imgx64    schedule 26.05.2016

Еще одна альтернатива перечислению символов UTF32 в строке C# — использование метода 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); } }
}
person Andrei Bozantan    schedule 28.11.2016
comment
System.Globalization.StringInfo — это то, что вам нужно. Остальной код не правильный. Взгляните на: msdn. microsoft.com/en-us/library/ - person X181; 27.04.2017
comment
Непонятно, что вы имеете в виду. Есть ли проблема с кодом из этого ответа? - person Andrei Bozantan; 28.04.2017