为什么这个字符串的长度比其中字符的数量还要长?

150

这段代码:

string a = "abc";
string b = "AC";
Console.WriteLine("Length a = {0}", a.Length);
Console.WriteLine("Length b = {0}", b.Length);

输出:

Length a = 3
Length b = 4

为什么?我唯一能想象的是中文字符长度为2个字节,而.Length方法返回的是字节数。


11
从标题上我怎么知道这是代理对问题?啊,好的老 System.Globalization 就是你的盟友! - Chris Cirefice
10
它在UTF-16中长度为4个字节,而不是2个。 - phuclv
3
这是UTF-16中的2个字符,但它们占用4个字节。因为UTF-16字符大小为2个字节,否则将无法存储65536种变体,只能存储256种。 - Kaiserludi
@KhouriGiordano:不,C#中的“string”类型使用UTF-16。 - Harry Johnston
4
我推荐阅读这篇伟大的文章《关于Unicode和字符集,每个软件开发者绝对必须知道的最低限度(没有借口!)》http://www.joelonsoftware.com/articles/Unicode.html - ItsMe
显示剩余6条评论
8个回答

238

其他人都只回答了表面问题,但实际上还有更深层次的原因:计算“字符”数量是一个难以定义的问题,并且可能会非常耗费计算资源,而长度属性应该更快速。

为什么这个问题难以定义?嗯,有几个选择,且都没有比另一个更合适:

  • 代码单元的数量(字节数或其他固定大小的数据块;C# 和 Windows 通常使用 UTF-16,因此返回两个字节的数量)肯定是相关的,因为计算机在许多目的中仍需要以该形式处理数据(比如写入文件就关心字节而不是字符)

  • Unicode 代码点的数量相对容易计算(虽然是 O(n) 因为你必须扫描字符串寻找代理对),并且可能对于文本编辑器来说很重要...但实际上并不等同于打印在屏幕上的字符数(称为 grapheme)。例如,一些带重音符号的字母可以用两种形式表示:单个代码点或成对出现的两个点,一个代表字母,另一个表示“给我的伙伴字母加一个重音符号”。那么这一对算两个字符还是一个字符呢?您可以通过规范化字符串来帮助解决这个问题,但并不是所有有效字母都有单个代码点表示。

  • 甚至 grapheme 的数量也不等于打印字符串的长度,这取决于字体等因素,而且由于某些字符在许多字体上具有一定重叠(字距),因此屏幕上的字符串长度不一定等于 grapheme 长度之和!

  • 有些 Unicode 点甚至不是传统意义上的字符,而是某种控制标记。比如字节顺序标记或从右到左的指示器。这些算吗?

总之,字符串长度实际上是一个极其复杂的问题,并计算它可能需要大量的 CPU 时间以及数据表。

此外,这些指标有什么意义?为什么这些指标很重要?好吧,只有您能够回答您的情况,但就我个人而言,它们通常是不相关的。我发现通过字节限制来限制数据输入更有逻辑性,因为这正是需要传输或存储的内容。限制显示大小最好由显示端软件完成 - 如果您有100个像素用于消息,则可以容纳多少字符取决于字体等因素,这些因素在数据层软件中是未知的。最后,鉴于Unicode标准的复杂性,如果尝试其他任何方法,您可能会在边缘情况下遇到错误。

因此,这是一个难以回答且没有很多通用用途的问题。代码单元数量很容易计算 - 它只是底层数据数组的长度 - 并且作为一般规则具有最有意义/有用的简单定义。

这就是为什么“b”超出“文档如此说”的表面解释而具有长度4的原因。


9
基本上,“.Length”并不是大多数程序员所认为的那样。也许应该有一组更具体的属性(例如“GlyphCount”),并将“Length”标记为过时! - redcalx
8
我同意,但我认为“Length”不应该被废除,以保持与数组的类比。 - Kroltan
2
@locster 它不应该过时。Python的版本很有意义,没有人会质疑它。 - simonzack
4
这并不是真的(一个常见的误解)- 对于UTF-32编码,lengthInBytes / 4可以得到代码点数,但这并不等同于“字符”或字形的数量。考虑后面跟着一个组合变音符号的拉丁小写字母e...它打印为一个单独的字符,甚至可以规范化为一个代码点,但在UTF-32中仍然有两个单位长度。 - Adam D. Ruppe
2
只是一个小补充:just可以有多个重音符号(或者一般来说,组合字符),比如ọ̵̌或ɘ̧̊̄。应该清楚地知道,你不能为所有可能的组合预定义Unicode代码点。 - Holger
显示剩余8条评论

65

根据 String.Length 属性的 documentation 文档:

Length属性返回此实例中Char对象的数量,而不是Unicode字符的数量。这是因为一个Unicode字符可能由多个Char表示。使用System.Globalization.StringInfo类来处理每个Unicode字符,而不是每个Char


3
Java的行为与此相同(对于String b也打印出4),因为它在字符数组中使用UTF-16表示法。在UTF-8中,它是一个4字节的字符。 - Michael

32

"AC"中索引为1的字符是一个SurrogatePair

需要记住的关键点是代理对表示32位单个字符。

您可以尝试此代码,它将返回True

Console.WriteLine(char.IsSurrogatePair("AC", 1));

Char.IsSurrogatePair方法(String,Int32)

如果s参数包括索引位置和索引+1处的相邻字符,并且位置index上的字符的数值范围从U+D800到U+DBFF,并且位置index+1处的字符的数值范围从U+DC00到U+DFFF,则返回true;否则返回false

这在String.Length属性中进一步解释:

Length属性返回此实例中Char对象的数量,而不是Unicode字符的数量。原因是一个Unicode字符可能由多个Char表示。使用System.Globalization.StringInfo类来处理每个Unicode字符,而不是每个Char。


24

正如其他答案所指出的那样,即使有3个可见字符,它们也用4个char对象表示。这就是为什么Length 是4而不是3。

MSDN说明:

Length属性返回此实例中Char对象的数量,而不是 Unicode 字符数。

但是,如果你真正想知道的是“文本元素”的数量而不是Char对象的数量,则可以使用StringInfo类。

var si = new StringInfo("AC");
Console.WriteLine(si.LengthInTextElements); // 3

你也可以像这样列举每个文本元素

var enumerator = StringInfo.GetTextElementEnumerator("AC");
while(enumerator.MoveNext()){
    Console.WriteLine(enumerator.Current);
}

在字符串上使用 foreach 会把中间的“字母”拆成两个 char 对象,并且打印出的结果与原字符串不一致。


20
由于Length属性返回的是字符对象而不是Unicode字符数,因此出现了这种情况。在您的情况下,一个Unicode字符由多个char对象(SurrogatePair)表示。

Length属性返回此实例中Char对象的数量,而不是Unicode字符的数量。原因是Unicode字符可能由多个Char表示。使用System.Globalization.StringInfo类来处理每个Unicode字符,而不是每个Char。


1
您在此答案中使用了“字符”这个术语有歧义。我建议至少将第一个术语替换为精确的术语。 - Lightness Races in Orbit
1
谢谢。已解决歧义问题。 - Yuval Itzchakov

10

正如其他人所说,它不是字符串中字符的数量,而是Char对象的数量。字符 ” 是码点U+20213。由于该值在16位char类型的范围之外,因此编码为UTF-16作为代理对D840 DE13

获取字符长度的方法在其他答案中已经提到。但是应谨慎使用,因为Unicode中有许多表示字符的方式。例如,“à”可能是1个组合字符或2个字符(a + 音标)。可能需要进行规范化,就像Twitter的情况一样。

您应该阅读这篇文章:
关于Unicode和字符集,每个软件开发人员绝对必须知道的最基本的知识(没有借口!)


7
这是因为length()仅适用于Unicode码点,其大小不超过U+FFFF。 这组码点称为基本多文种平面(BMP),仅使用2个字节。
BMP之外的Unicode码点使用4个字节的代理对在UTF-16中表示。
要正确计算字符数(3个),请使用StringInfo
StringInfo b = new StringInfo("AC");
Console.WriteLine(string.Format("Length 2 = {0}", b.LengthInTextElements));

6

好的,在.Net和C#中所有字符串都被编码为UTF-16LE。一个string被存储为一系列字符。每个char封装了2个字节或16位的存储。

我们在纸上或屏幕上看到的作为单个字母、字符、字形、符号或标点符号的东西可以被视为单个文本元素。如Unicode标准附录#29 UNICODE TEXT SEGMENTATION所述,每个文本元素由一个或多个代码点表示。可以在此处找到完整的代码列表。

每个码点都需要编码为二进制以便计算机进行内部表示。如前所述,每个char存储2个字节。在或等于U+FFFF的代码点可以存储在单个char中。在U+FFFF以上的代码点将作为代理对存储,使用两个字符表示一个单独的代码点。
根据我们现在所知道的,我们可以推断出,文本元素可以被存储为一个char,作为两个字符的代理对,或者,如果文本元素由多个代码点表示,则是单个字符和代理对的某种组合。如果还不够复杂,一些文本元素可以用不同的代码点组合来表示,如Unicode标准附录#15,Unicode规范化形式所述。

插曲

因此,看起来相同的字符串实际上可以由不同的字符组合而成。对这样两个字符串进行顺序(逐字节)比较会检测到差异,这可能是意外或不希望的。

您可以重新编码 .Net 字符串,使它们使用相同的规范化形式。一旦规范化,具有相同文本元素的两个字符串将以相同的方式编码。要做到这一点,请使用 string.Normalize 函数。但是,请记住,一些不同的文本元素看起来相似。:-s


因此,这一切与问题有何关系呢?文本元素''由单个代码点U+20213 cjk统一表意符号扩展b表示。这意味着它不能被编码为单个char,而必须使用代理对编码为两个字符。这就是为什么string bstring a多一个char的原因。
如果您需要可靠(请注意)地计算string中文本元素的数量,您应该像这样使用System.Globalization.StringInfo类。
using System.Globalization;

string a = "abc";
string b = "AC";

Console.WriteLine("Length a = {0}", new StringInfo(a).LengthInTextElements);
Console.WriteLine("Length b = {0}", new StringInfo(b).LengthInTextElements);

给出输出,

"Length a = 3"
"Length b = 3"

如预期。

注意事项

.Net实现的Unicode文本分割在StringInfoTextElementEnumerator类中应该是普遍有用的,在大多数情况下,会产生调用者期望的响应。然而,正如Unicode标准附录#29所述,“由于仅根据文本无法始终准确地匹配用户感知,因此目标不能总是完全达成。”


1
我认为你的回答可能会让人感到困惑。在这种情况下,虽然“ ”只是一个单一的代码点,但由于它的代码点超过了0xFFFF,因此必须使用代理对将其表示为2个代码单元。字形是建立在代码点之上的另一个概念,其中一个字形可以由单个代码点或多个代码点表示,如韩文的Hangul或许多基于拉丁语的语言中所见。 - nhahtdh
同意@nhahtdh的观点,我之前的回答是错误的。我已经重新写了一遍,希望现在更加清晰明了。 - Jodrell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接