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


Структура 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 являются суррогатными точками кода. Ни одна суррогатная кодовая точка не имеет достаточно информации, чтобы определить, является ли она буквой.

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

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.IsWhiteSpace и Char.GetUnicodeCategory методам. Методы 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. Чтобы узнать количество экземпляров char, которое будет результатом преобразования экземпляра Rune в 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.EncodeToUtf16 и Rune.EncodeToUtf8 возвращают фактическое количество записанных элементов. Вызывается исключение, если целевой буфер слишком короток, чтобы уместить результат. Существуют невыбрасывающие TryEncodeToUtf8 и TryEncodeToUtf16 методы для вызывающих функций, которые хотят избежать выброса исключений.

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

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

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

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