本文提供了此 API 参考文档的补充说明。
Rune 实例表示 Unicode 标量值,即除代理范围 (U+D800..U+DFFF) 之外的任何代码点。 该类型的构造函数和转换运算符验证输入,因此使用者可以调用 API(假设基础 Rune 实例格式良好)。
如果对 Unicode 标量值、码点、代理范围和良好格式化等术语不熟悉,请参阅 .NET 中的字符编码简介。
何时使用 Rune 类型
如果你的代码符合以下条件,请考虑使用Rune
类型:
- 调用需要 Unicode 标量值的 API
- 显式处理代理对
需要 Unicode 标量值的 API
如果代码遍历 char
实例中的 string
或 ReadOnlySpan<char>
,那么某些 char
方法在 char
实例(位于代理范围内的实例)上将无法正常工作。 例如,以下 API 需要标量值 char
才能正常工作:
- Char.GetNumericValue
- Char.GetUnicodeCategory
- Char.IsDigit
- Char.IsLetter
- Char.IsLetterOrDigit
- Char.IsLower
- Char.IsNumber
- Char.IsPunctuation
- Char.IsSymbol
- Char.IsUpper
以下示例展示了如果任一 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.IsSurrogate
- Char.IsSurrogatePair
- Char.IsHighSurrogate
- Char.IsLowSurrogate
- Char.ConvertFromUtf32
- Char.ConvertToUtf32
例如,以下方法具有处理代理 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.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
和Rune
之间存在一些API差异。 例如:
- 没有
Rune
等效于 Char.IsSurrogate(Char),因为根据定义,Rune
实例永远不能是代理码位。 - 这 Rune.GetUnicodeCategory 并不总是返回与 Char.GetUnicodeCategory 相同的结果。 它确实返回的值与CharUnicodeInfo.GetUnicodeCategory相同。 有关详细信息,请参阅“备注”。Char.GetUnicodeCategory
将 Rune
转换为 UTF-8 或 UTF-16
由于 a Rune
是 Unicode 标量值,因此可以转换为 UTF-8、UTF-16 或 UTF-32 编码。 该 Rune
类型具有对转换为 UTF-8 和 UTF-16 的内置支持。
将Rune.EncodeToUtf16实例转换为Rune
char
实例。 若要查询通过将 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方法返回写入的实际元素数。 如果目标缓冲区太短而无法包含结果,则会引发异常。 对于希望避免异常的调用方,还提供了不引发异常的 TryEncodeToUtf8 和 TryEncodeToUtf16 方法。
.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 标量值的值。