System.Text.Rune 结构

本文提供了此 API 参考文档的补充说明。

Rune实例表示 Unicode 标量值,这意味着排除代理项范围(U+D800.)的任何代码点。U+DFFF)。 该类型的构造函数和转换运算符验证输入,因此使用者可以调用 API(假设基础 Rune 实例格式良好)。

如果不熟悉 Unicode 标量值、代码点、代理项范围和格式正确的术语,请参阅 .NET 中的字符编码简介。

何时使用 Rune 类型

如果代码有以下几点, Rune 请考虑使用类型:

  • 调用需要 Unicode 标量值的 API
  • 显式处理代理项对

需要 Unicode 标量值的 API

如果代码循环访问char某个或 a ReadOnlySpan<char>中的实例,则某些char方法在代理项范围内的实例上无法正常工作charstring。 例如,以下 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

显式处理代理项对的代码

如果代码调用对代理项代码点显式操作的 API,请考虑使用 Rune 类型,例如以下方法:

例如,以下方法具有处理代理项 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 匹配项
  • 在已知字符值上拆分字符串

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 实例表示 Unicode 标量值,因此遵循 Unicode 文本分段准则的 组件可以用作 Rune 计算显示字符的构建基块。

StringInfo 类型可用于对显示字符进行计数,但在除 .NET 5+ 以外的 .NET 实现的所有方案中,该类型不会正确计数。

有关详细信息,请参阅 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
    

如果输入不表示有效的 Unicode 标量值,则所有构造函数都会引发 ArgumentException

Rune.TryCreate 一些方法可用于不希望在失败时引发异常的调用方。

Rune 实例也可以从现有输入序列中读取。 例如,给定一个 ReadOnlySpan<char> 表示 UTF-16 数据的方法返回 Rune.DecodeFromUtf16 输入范围开头的第一个 Rune 实例。 该方法 Rune.DecodeFromUtf8 同样运行,接受 ReadOnlySpan<byte> 表示 UTF-8 数据的参数。 有等效的方法可从范围末尾读取,而不是范围开头。

查询 a Rune

若要获取实例的 Rune 整数代码点值,请使用 Rune.Value 该属性。

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

该类型上 char 提供的许多静态 API 也可用于该 Rune 类型。 例如, Rune.IsWhiteSpace 等效 Rune.GetUnicodeCategoryChar.IsWhiteSpaceChar.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;
}

之间存在一些 API 差异charRune 例如:

Rune转换为 UTF-8 或 UTF-16

由于 a Rune 是 Unicode 标量值,因此可以转换为 UTF-8、UTF-16 或 UTF-32 编码。 该 Rune 类型具有对转换为 UTF-8 和 UTF-16 的内置支持。

Rune.EncodeToUtf16实例转换为Runechar实例。 若要查询将实例转换为 Rune UTF-16 而生成的实例数char,请使用该Rune.Utf16SequenceLength属性。 UTF-8 转换存在类似的方法。

以下示例将 Rune 实例转换为 char 数组。 代码假定变量中有rune一个Rune实例:

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

由于 a 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.EncodeToUtf16Rune.EncodeToUtf8方法返回写入的实际元素数。 如果目标缓冲区太短而无法包含结果,则会引发异常。 对于想要避免异常的调用方,也存在非引发 TryEncodeToUtf8TryEncodeToUtf16 方法。

.NET 中的 Rune 与其他语言

Unicode Standard 中未定义术语“rune”。 该术语可追溯到 UTF-8 的创建。 Rob Pike 和 Ken Thompson 正在寻找一个术语来描述最终被称为代码点的内容。 他们解决了术语“rune”,罗布·派克后来对 Go 编程语言的影响有助于普及这个词。

但是,.NET Rune 类型与 Go rune 类型不相等。 在 Go 中,类型 rune别名 int32。 Go rune 旨在表示 Unicode 码位,但它可以是任何 32 位值,包括代理代码点和不是合法 Unicode 码位的值。

有关其他编程语言中的类似类型,请参阅 Rust 的基元 char 类型Swift Unicode.Scalar 的类型,这两种类型都表示 Unicode 标量值。 它们提供的功能类似于 .NET 的类型 Rune ,不允许实例化非合法 Unicode 标量值的值。