Поделиться через


Структуру System.Text.Rune

В этой статье приводятся дополнительные замечания к справочной документации по этому API.

Rune Экземпляр представляет скалярное значение Юникода, что означает любую точку кода, за исключением суррогатного диапазона (U+D800.). U+DFFF). Конструкторы и операторы преобразования типа проверяют входные данные, поэтому потребители могут вызывать API, предполагая, что базовый Rune экземпляр хорошо сформирован.

Если вы не знакомы с терминами скалярное значение Юникода, точка кода, суррогатный диапазон и хорошо сформированный диапазон, см . статью "Введение в кодировку символов" в .NET.

Когда следует использовать тип Rune

Попробуйте использовать тип, Rune если код:

  • Вызывает API, требующие скалярных значений Юникода
  • Явным образом обрабатывает суррогатные пары

API, требующие скалярных значений Юникода

Если код выполняет итерацию по char экземплярам в string или в, ReadOnlySpan<char>некоторые char из методов не будут работать правильно для char экземпляров, которые находятся в суррогатном диапазоне. Например, для правильной работы следующих API требуется скалярное значение char :

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

// THE FOLLOWING METHOD SHOWS INCORRECT CODE.
// DO NOT DO THIS IN A PRODUCTION APPLICATION.
int CountLettersBadExample(string s)
{
    int letterCount = 0;

    foreach (char ch in s)
    {
        if (char.IsLetter(ch))
        { letterCount++; }
    }

    return letterCount;
}
// THE FOLLOWING METHOD SHOWS INCORRECT CODE.
// DO NOT DO THIS IN A PRODUCTION APPLICATION.
let countLettersBadExample (s: string) =
    let mutable letterCount = 0

    for ch in s do
        if Char.IsLetter ch then
            letterCount <- letterCount + 1
    
    letterCount

Ниже приведен эквивалентный код, который работает с :ReadOnlySpan<char>

// THE FOLLOWING METHOD SHOWS INCORRECT CODE.
// DO NOT DO THIS IN A PRODUCTION APPLICATION.
static int CountLettersBadExample(ReadOnlySpan<char> span)
{
    int letterCount = 0;

    foreach (char ch in span)
    {
        if (char.IsLetter(ch))
        { letterCount++; }
    }

    return letterCount;
}

Приведенный выше код работает правильно с некоторыми языками, такими как английский:

CountLettersInString("Hello")
// Returns 5

Но он не будет работать правильно для языков за пределами базовой многоязычной плоскости, например Osage:

CountLettersInString("𐓏𐓘𐓻𐓘𐓻𐓟 𐒻𐓟")
// Returns 0

Причина, по которой этот метод возвращает неправильные результаты для текста Osage, заключается в том, что char экземпляры букв Osage являются суррогатными точками кода. Ни одна суррогатная кодовая точка не имеет достаточно информации, чтобы определить, является ли она буквой.

При изменении этого кода вместо этого charметод работает правильно с точками кода Rune за пределами базовой многоязычной плоскости:

int CountLetters(string s)
{
    int letterCount = 0;

    foreach (Rune rune in s.EnumerateRunes())
    {
        if (Rune.IsLetter(rune))
        { letterCount++; }
    }

    return letterCount;
}
let countLetters (s: string) =
    let mutable letterCount = 0

    for rune in s.EnumerateRunes() do
        if Rune.IsLetter rune then
            letterCount <- letterCount + 1

    letterCount

Ниже приведен эквивалентный код, который работает с :ReadOnlySpan<char>

static int CountLetters(ReadOnlySpan<char> span)
{
    int letterCount = 0;

    foreach (Rune rune in span.EnumerateRunes())
    {
        if (Rune.IsLetter(rune))
        { letterCount++; }
    }

    return letterCount;
}

Предыдущий код правильно подсчитывает буквы Osage:

CountLettersInString("𐓏𐓘𐓻𐓘𐓻𐓟 𐒻𐓟")
// Returns 8

Код, который явно обрабатывает суррогатные пары

Рекомендуется использовать тип, Rune если код вызывает API, которые явно работают с суррогатными точками кода, такими как следующие методы:

Например, следующий метод имеет специальную логику для решения суррогатных char пар:

static void ProcessStringUseChar(string s)
{
    Console.WriteLine("Using char");

    for (int i = 0; i < s.Length; i++)
    {
        if (!char.IsSurrogate(s[i]))
        {
            Console.WriteLine($"Code point: {(int)(s[i])}");
        }
        else if (i + 1 < s.Length && char.IsSurrogatePair(s[i], s[i + 1]))
        {
            int codePoint = char.ConvertToUtf32(s[i], s[i + 1]);
            Console.WriteLine($"Code point: {codePoint}");
            i++; // so that when the loop iterates it's actually +2
        }
        else
        {
            throw new Exception("String was not well-formed UTF-16.");
        }
    }
}

Такой код проще, если он используется Rune, как показано в следующем примере:

static void ProcessStringUseRune(string s)
{
    Console.WriteLine("Using Rune");

    for (int i = 0; i < s.Length;)
    {
        if (!Rune.TryGetRuneAt(s, i, out Rune rune))
        {
            throw new Exception("String was not well-formed UTF-16.");
        }

        Console.WriteLine($"Code point: {rune.Value}");
        i += rune.Utf16SequenceLength; // increment the iterator by the number of chars in this Rune
    }
}

Когда не следует использовать Rune

Не нужно использовать тип, Rune если код:

  • Поиск точных char совпадений
  • Разбивает строку на известное значение char

Использование типа может возвращать неверные Rune результаты, если код:

  • Подсчитывает количество отображаемых символов в string

Поиск точных char совпадений

Следующий код выполняет итерацию string по поиску определенных символов, возвращая индекс первого совпадения. Нет необходимости изменять этот код для использования Rune, так как код ищет символы, представленные одним charсимволом.

int GetIndexOfFirstAToZ(string s)
{
    for (int i = 0; i < s.Length; i++)
    {
        char thisChar = s[i];
        if ('A' <= thisChar && thisChar <= 'Z')
        {
            return i; // found a match
        }
    }

    return -1; // didn't find 'A' - 'Z' in the input string
}

Разделение строки на известном char

Обычно можно вызывать string.Split и использовать разделители, такие как ' ' (пробел) или ',' (запятая), как показано в следующем примере:

string inputString = "🐂, 🐄, 🐆";
string[] splitOnSpace = inputString.Split(' ');
string[] splitOnComma = inputString.Split(',');

Здесь нет необходимости использовать Rune , так как код ищет символы, представленные одним charсимволом.

Подсчет количества отображаемых символов в string

Количество Rune экземпляров в строке может не совпадать с количеством символов, отображаемых пользователем при отображении строки.

Так как Rune экземпляры представляют скалярные значения Юникода, компоненты, которые следуют рекомендациям по сегментации текста Юникода, могут использовать Rune в качестве стандартного блока для подсчета отображаемых символов.

Тип StringInfo можно использовать для подсчета отображаемых символов, но он не учитывается в всех сценариях для реализаций .NET, отличных от .NET 5+.

Дополнительные сведения см. в разделе "Кластеры Grapheme".

Создание экземпляра Rune

Существует несколько способов получения экземпляра Rune . Конструктор можно использовать для создания Rune непосредственно из:

  • Кодовая точка.

    Rune a = new Rune(0x0061); // LATIN SMALL LETTER A
    Rune b = new Rune(0x10421); // DESERET CAPITAL LETTER ER
    
  • Один файл char.

    Rune c = new Rune('a');
    
  • Суррогатная char пара.

    Rune d = new Rune('\ud83d', '\udd2e'); // U+1F52E CRYSTAL BALL
    

Все конструкторы вызывают исключение ArgumentException , если входные данные не представляют допустимое скалярное значение Юникода.

Rune.TryCreate Существуют методы, доступные для вызывающих лиц, которые не хотят создавать исключения при сбое.

Rune экземпляры также можно считывать из существующих входных последовательностей. Например, учитывая ReadOnlySpan<char> , что представляет данные UTF-16, Rune.DecodeFromUtf16 метод возвращает первый Rune экземпляр в начале входного диапазона. Метод Rune.DecodeFromUtf8 работает аналогично, принимая ReadOnlySpan<byte> параметр, представляющий данные UTF-8. Существуют эквивалентные методы для чтения с конца диапазона вместо начала диапазона.

Свойства запроса объекта Rune

Чтобы получить целочисленное значение точки кода экземпляра Rune , используйте Rune.Value свойство.

Rune rune = new Rune('\ud83d', '\udd2e'); // U+1F52E CRYSTAL BALL
int codePoint = rune.Value; // = 128302 decimal (= 0x1F52E)

Многие статические API, доступные в типе char , также доступны в типе Rune . Например, Rune.IsWhiteSpace это Rune.GetUnicodeCategory эквиваленты и Char.GetUnicodeCategory методыChar.IsWhiteSpace. Методы Rune правильно обрабатывают суррогатные пары.

Следующий пример кода принимает ReadOnlySpan<char> в качестве входных данных и обрезает как от начала, так и от конца диапазона каждый Rune , который не является буквой или цифрой.

static ReadOnlySpan<char> TrimNonLettersAndNonDigits(ReadOnlySpan<char> span)
{
    // First, trim from the front.
    // If any Rune can't be decoded
    // (return value is anything other than "Done"),
    // or if the Rune is a letter or digit,
    // stop trimming from the front and
    // instead work from the end.
    while (Rune.DecodeFromUtf16(span, out Rune rune, out int charsConsumed) == OperationStatus.Done)
    {
        if (Rune.IsLetterOrDigit(rune))
        { break; }
        span = span[charsConsumed..];
    }

    // Next, trim from the end.
    // If any Rune can't be decoded,
    // or if the Rune is a letter or digit,
    // break from the loop, and we're finished.
    while (Rune.DecodeLastFromUtf16(span, out Rune rune, out int charsConsumed) == OperationStatus.Done)
    {
        if (Rune.IsLetterOrDigit(rune))
        { break; }
        span = span[..^charsConsumed];
    }

    return span;
}

Существуют некоторые различия между char API и Rune. Например:

Преобразование в Rune UTF-8 или UTF-16

Rune Так как это скалярное значение Юникода, его можно преобразовать в кодировку UTF-8, UTF-16 или UTF-32. Тип Rune имеет встроенную поддержку преобразования в UTF-8 и UTF-16.

Экземпляр Rune.EncodeToUtf16 преобразуется Rune в char экземпляры. Чтобы запросить количество экземпляров, которое приведет к преобразованию Rune экземпляра char в UTF-16, используйте Rune.Utf16SequenceLength это свойство. Аналогичные методы существуют для преобразования UTF-8.

В следующем примере экземпляр преобразуется Rune в char массив. В коде предполагается, что у вас есть Rune экземпляр в переменной rune :

char[] chars = new char[rune.Utf16SequenceLength];
int numCharsWritten = rune.EncodeToUtf16(chars);

Так как последовательность string символов UTF-16 также преобразует Rune экземпляр в UTF-16:

string theString = rune.ToString();

В следующем примере экземпляр преобразуется Rune в UTF-8 массив байтов:

byte[] bytes = new byte[rune.Utf8SequenceLength];
int numBytesWritten = rune.EncodeToUtf8(bytes);

Rune.EncodeToUtf8 Методы Rune.EncodeToUtf16 возвращают фактическое количество записываемых элементов. Они вызывают исключение, если целевой буфер слишком короткий, чтобы содержать результат. Существуют неисполнение TryEncodeToUtf8 и TryEncodeToUtf16 методы, а также для вызывающих лиц, которые хотят избежать исключений.

Rune в .NET и других языках

Термин "rune" не определен в Стандарте Юникода. Термин восходит к созданию UTF-8. Роб Пайк и Кен Томпсон искали термин, чтобы описать, что в конечном итоге станет известной как кодовая точка. Они поселились на термине "rune", и Роб Пайк позже влияние на язык программирования Go помогло популяризировать термин.

Однако тип .NET Rune не эквивалентен типу Go rune . В Go тип rune является псевдонимом для int32. Rune Go предназначен для представления кодовой точки Юникода, но это может быть любое 32-разрядное значение, включая суррогатные кодовые точки и значения, которые не являются законными кодовых точек Юникода.

Аналогичные типы на других языках программирования см. в разделе "Примитивный char тип Rust" или "SwiftUnicode.Scalar", оба из которых представляют скалярные значения Юникода. Они предоставляют функциональные возможности, аналогичные . Rune Тип NET, и они запрещают создание экземпляров значений, которые не являются законными скалярными значениями Юникода.