Estructura System.Text.Rune

En este artículo se proporcionan comentarios adicionales a la documentación de referencia de esta API.

Una Rune instancia representa un valor escalar Unicode, lo que significa que cualquier punto de código excluyendo el intervalo suplente (U+D800.). U+DFFF). Los constructores y operadores de conversión del tipo validan la entrada, por lo que los consumidores pueden llamar a las API suponiendo que la instancia subyacente Rune esté bien formada.

Si no está familiarizado con los términos valor escalar Unicode, punto de código, intervalo suplente y correcto, consulte Introducción a la codificación de caracteres en .NET.

Cuándo usar el tipo rune

Considere la posibilidad de usar el Rune tipo si el código:

  • Llama a las API que requieren valores escalares Unicode.
  • Controla explícitamente los pares suplentes

API que requieren valores escalares Unicode

Si el código recorre en iteración las char instancias de o string , ReadOnlySpan<char>algunos de los char métodos no funcionarán correctamente en char instancias que se encuentran en el intervalo suplente. Por ejemplo, las SIGUIENTES API requieren que un valor char escalar funcione correctamente:

En el ejemplo siguiente se muestra el código que no funcionará correctamente si alguna de las char instancias son puntos de código suplentes:

// 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

Este es el código equivalente que funciona con :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;
}

El código anterior funciona correctamente con algunos idiomas como inglés:

CountLettersInString("Hello")
// Returns 5

Pero no funcionará correctamente para idiomas fuera del plano multilingüe básico, como Osage:

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

La razón por la que este método devuelve resultados incorrectos para el texto de Osage es que las char instancias de letras de Osage son puntos de código suplentes. Ningún punto de código suplente único tiene suficiente información para determinar si es una letra.

Si cambia este código para usar Rune en lugar de , el método funciona correctamente con puntos de charcódigo fuera del plano multilingüe básico:

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

Este es el código equivalente que funciona con :ReadOnlySpan<char>

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

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

    return letterCount;
}

El código anterior cuenta correctamente las letras de Osage:

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

Código que controla explícitamente pares suplentes

Considere la posibilidad de usar el tipo si el Rune código llama a las API que operan explícitamente en puntos de código suplentes, como los métodos siguientes:

Por ejemplo, el método siguiente tiene lógica especial para tratar con pares suplentes 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.");
        }
    }
}

Este código es más sencillo si usa Rune, como en el ejemplo siguiente:

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
    }
}

Cuándo no debe usarse Rune

No es necesario usar el Rune tipo si el código:

  • Busca coincidencias exactas char
  • Divide una cadena en un valor char conocido.

El uso del Rune tipo puede devolver resultados incorrectos si el código:

  • Cuenta el número de caracteres para mostrar de un string

Buscar coincidencias exactas char

El código siguiente recorre en iteración una string búsqueda de caracteres específicos y devuelve el índice de la primera coincidencia. No es necesario cambiar este código para usar Rune, ya que el código busca caracteres representados por un único 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
}

Dividir una cadena en un conocido char

Es habitual llamar string.Split a y usar delimitadores como ' ' (espacio) o ',' (coma), como en el ejemplo siguiente:

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

No es necesario usar Rune aquí, ya que el código busca caracteres representados por un solo char.

Recuento del número de caracteres para mostrar de un string

Es posible que el número de instancias de Rune una cadena no coincida con el número de caracteres reconocibles por el usuario que se muestran al mostrar la cadena.

Dado Rune que las instancias representan valores escalares Unicode, los componentes que siguen las instrucciones de segmentación de texto Unicode pueden usarse Rune como bloque de creación para contar caracteres para mostrar.

El StringInfo tipo se puede usar para contar caracteres para mostrar, pero no cuenta correctamente en todos los escenarios de implementaciones de .NET que no sean .NET 5+.

Para más información, consulte Clústeres de Grapheme.

Creación de instancias de un Rune

Hay varias maneras de obtener una Rune instancia. Puede usar un constructor para crear un Rune directamente desde:

  • Un punto de código.

    Rune a = new Rune(0x0061); // LATIN SMALL LETTER A
    Rune b = new Rune(0x10421); // DESERET CAPITAL LETTER ER
    
  • Un char único.

    Rune c = new Rune('a');
    
  • Un par suplente char .

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

Todos los constructores inician si ArgumentException la entrada no representa un valor escalar Unicode válido.

Hay Rune.TryCreate métodos disponibles para los autores de llamadas que no quieren que se produzcan excepciones en caso de error.

Rune Las instancias también se pueden leer de secuencias de entrada existentes. Por ejemplo, dado un ReadOnlySpan<char> que representa datos UTF-16, el Rune.DecodeFromUtf16 método devuelve la primera Rune instancia al principio del intervalo de entrada. El Rune.DecodeFromUtf8 método funciona de forma similar, aceptando un ReadOnlySpan<byte> parámetro que representa datos UTF-8. Hay métodos equivalentes para leer desde el final del intervalo en lugar del principio del intervalo.

Propiedades de consulta de un Rune

Para obtener el valor de punto de código entero de una Rune instancia, use la Rune.Value propiedad .

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

Muchas de las API estáticas disponibles en el char tipo también están disponibles en el Rune tipo . Por ejemplo, Rune.IsWhiteSpace y Rune.GetUnicodeCategory son equivalentes a Char.IsWhiteSpace los métodos y Char.GetUnicodeCategory . Los Rune métodos controlan correctamente los pares suplentes.

El código de ejemplo siguiente toma como ReadOnlySpan<char> entrada y recorta desde el principio y el final del intervalo cada Rune uno que no es una letra o un dígito.

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;
}

Hay algunas diferencias de API entre char y Rune. Por ejemplo:

Convertir a Rune UTF-8 o UTF-16

Dado que es Rune un valor escalar Unicode, se puede convertir en codificación UTF-8, UTF-16 o UTF-32. El Rune tipo tiene compatibilidad integrada para la conversión a UTF-8 y UTF-16.

Rune.EncodeToUtf16 convierte una Rune instancia en char instancias. Para consultar el número de char instancias resultantes de convertir una Rune instancia en UTF-16, use la Rune.Utf16SequenceLength propiedad . Existen métodos similares para la conversión UTF-8.

En el ejemplo siguiente se convierte una Rune instancia en una char matriz. El código supone que tiene una Rune instancia en la rune variable :

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

Dado que es string una secuencia de caracteres UTF-16, el ejemplo siguiente también convierte una Rune instancia en UTF-16:

string theString = rune.ToString();

En el ejemplo siguiente se convierte una Rune instancia en una matriz de UTF-8 bytes:

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

Los Rune.EncodeToUtf16 métodos y Rune.EncodeToUtf8 devuelven el número real de elementos escritos. Inician una excepción si el búfer de destino es demasiado corto para contener el resultado. Hay métodos y TryEncodeToUtf16 no iniciadoresTryEncodeToUtf8, así como para los autores de llamadas que desean evitar excepciones.

Ejecutar en .NET frente a otros lenguajes

El término "rune" no se define en el estándar Unicode. El término se remonta a la creación de UTF-8. Rob Pike y Ken Thompson buscaban un término para describir lo que finalmente se conocería como punto de código. Se establecieron en el término "rune", y la influencia posterior de Rob Pike sobre el lenguaje de programación Go ayudó a popularizar el término.

Sin embargo, el tipo de .NET Rune no es el equivalente del tipo Go rune . En Go, el rune tipo es un alias para int32. Un rune de Go está diseñado para representar un punto de código Unicode, pero puede ser cualquier valor de 32 bits, incluidos los puntos de código suplentes y los valores que no son puntos de código Unicode legales.

Para ver tipos similares en otros lenguajes de programación, consulte El tipo primitivo char de Rust o el tipo de SwiftUnicode.Scalar, ambos representan valores escalares Unicode. Proporcionan una funcionalidad similar a . El tipo de Rune NET y no permiten la creación de instancias de valores que no son valores escalares Unicode legales.