System.Text.Rune 结构

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

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

如果对 Unicode 标量值、码点、代理范围和良好格式化等术语不熟悉,请参阅 .NET 中的字符编码简介

何时使用 Rune 类型

如果你的代码符合以下条件,请考虑使用Rune类型:

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

需要 Unicode 标量值的 API

如果代码遍历 char 实例中的 stringReadOnlySpan<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

但对于基本多语言平面之外的语言,如奥萨奇语,它将无法正常工作:

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

该方法对奥萨奇文本返回错误结果的原因是,奥萨奇字母中的 char 实例是代理码点。 没有单个代理项代码点有足够的信息来确定它是否为字母。

如果将此代码更改为使用 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;
}

前面的代码正确地计算了奥塞奇字母的数量。

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 实例也可以从现有输入序列中读取。 例如,给定一个表示 UTF-16 数据的 ReadOnlySpan<char>Rune.DecodeFromUtf16 方法返回输入范围开头的第一个 Rune 实例。 Rune.DecodeFromUtf8 方法以相似的方式操作,接受一个表示 UTF-8 数据的 ReadOnlySpan<byte> 参数。 存在与从跨度末尾读取数据而非从跨度开头读取数据等效的方法。

查询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.IsWhiteSpaceRune.GetUnicodeCategory 等效于 Char.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;
}

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

Rune 转换为 UTF-8 或 UTF-16

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

Rune.EncodeToUtf16实例转换为Runechar实例。 若要查询通过将 char 实例转换为 UTF-16 后生成的 Rune 实例数,请使用 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 与其他语言的比较

“rune”一词在 Unicode 标准中并未定义。 该术语可追溯到 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 标量值的值。