为什么此字符串的长度比其中的字符数长?


145

这段代码:

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

输出:

Length a = 3
Length b = 4

为什么?我唯一能想象的是汉字长2个字节,并且该.Length方法返回字节数。


10
仅从标题来看,我怎么知道这是一个代理对问题。啊,好的'ol System。全球化是您的盟友!
克里斯·西里菲斯

9
它在UTF-16中为4个字节长,而不是2个
phuclv 2014年

char的十进制值为𠈓131603,并且因为char是无符号字节,所以您可以用2个字符而不是4个字符来实现该值(无符号16位值的最大值为65535(或65536个变体),并使用2个字符表示它允许的最大变化数量不是65536 * 2(131072),而是65536 * 65536个变化(4,294,967,296,有效为32位值)
GMasucci 2014年

3
@GMAsucci:这是在UTF16 2个字符,但4个字节,因为UTF-16字符的大小是2个字节,否则它不能存储65536种变化,但只有256
Kaiserludi

4
我建议阅读伟大的文章“绝对绝对肯定每个软件开发人员绝对必须了解Unicode和字符集(无借口!)” joelonsoftware.com/articles/Unicode.html
ItsMe 2014年

Answers:


232

其他人都给出了表面的答案,但是还有一个更深层的理由:“字符”的数量是一个难以定义的问题,计算起来可能会令人惊讶地昂贵,而长度属性应该很快。

为什么很难定义?好吧,这里有几个选项,没有一个比另一个更有效:

  • 代码单元的数量(字节或其他固定大小的数据块; C#和Windows通常使用UTF-16,因此它返回两字节的数量)肯定是相关的,因为计算机仍需要处理该格式的数据有多种用途(例如,写入文件时,只关心字节而不是字符)

  • Unicode代码点的数量非常容易计算(尽管O(n),因为您必须扫描字符串以查找代理对),并且可能对文本编辑器很重要....但实际上与字符数不一样在屏幕上打印(称为字素)。例如,某些重音字母可以用两种形式表示:一个代码点或成对的两个点,一个代表字母,另一个说“在我的伴侣字母上加一个重音”。这对是两个字符还是一个?您可以规范化字符串以帮助解决此问题,但是并非所有有效字母都具有单个代码点表示形式。

  • 甚至字素的数量与打印字符串的长度也不相同,这取决于其他因素中的字体,并且由于某些字符以许多字体重叠打印(字距调整),因此屏幕上字符串的长度无论如何不一定等于字素长度的总和!

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

简而言之,字符串的长度实际上是一个荒谬的复杂问题,计算它会占用大量CPU时间以及数据表。

而且,有什么意义呢?为什么这些指标很重要?好吧,只有您能回答您的情况,但就我个人而言,我发现它们通常是无关紧要的。我发现限制数据输入在逻辑上是由字节限制完成的,因为无论如何这都是需要传输或存储的。最好通过显示端软件来限制显示大小-如果消息的像素为100,适合的字符数取决于字体等,这是数据层软件始终无法知道的。最后,考虑到unicode标准的复杂性,如果您尝试其他方法,那么无论如何都可能会遇到一些漏洞。

因此,没有太多通用用途是一个很难的问题。计算的代码单元数量微不足道-仅是基础数据数组的长度-且最简单的定义就是最有意义/最有用的一般规则。

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


9
本质上,“。Length”不是大多数编码人员认为的。也许应该有一组更具体的属性(例如GlyphCount)和Length标记为过时!
redcalx 2014年

8
@locster我同意,但不要认为Length应该过时,以与数组保持类比。
2014年

2
@locster不应过时。蟒蛇很有意义,没有人质疑它。
simonzack 2014年

1
我认为.Length很有道理,并且是自然属性,只要您了解它的含义和原因。然后,它就像任何其他数组一样工作(在某些语言中,如D,就语言而言,字符串从字面上看是一个数组,并且确实很好用)
Adam D. Ruppe 2014年

4
这不是真实的(一种常见的误解) -使用UTF-32,lengthInBytes / 4将给出的数码点,但是这是一样的“字符”或字形的数量。考虑一下拉丁文小写字母E,后跟一个COMBINING DIAERESIS ...,它以单个字符的形式打印,甚至可以归一化为单个代码点,但是即使在UTF-32中,它仍然长两个单位。
亚当·鲁珀

62

从属性的文档String.Length

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


3
Java的行为相同(也为打印4 String b),因为它在char数组中使用UTF-16表示形式。它是UTF-8中的4字节字符。
2014年

32

您在索引1中的角色"A𠈓C"代理对

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

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

Console.WriteLine(char.IsSurrogatePair("A𠈓C", 1));

Char.IsSurrogatePair方法(字符串,Int32)

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

这在String.Length属性中进一步说明:

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


24

正如其他答案所指出的那样,即使存在3个可见字符,它们也将由4个char对象表示。这就是为什么Length4不是3的原因。

MSDN指出

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

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

var si = new StringInfo("A𠈓C");
Console.WriteLine(si.LengthInTextElements); // 3

您也可以像这样枚举每个文本元素

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

foreach在字符串上使用会将中间的“字母”分成两个char对象,并且打印结果将与字符串不符。


20

那是因为该Length属性返回char对象的数量,而不是unicode字符的数量。在您的情况下,一个Unicode字符由多个char对象(SurrogatePair)表示。

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


1
您在此答案中使用了“字符”。我建议至少使用精确的术语替换第一个。
Lightness Races in Orbit

1
谢谢。修复了歧义。
Yuval Itzchakov 2014年

10

就像其他人所说的,这不是字符串中字符的数量,而是Char对象的数量。字符𠈓是代码点U + 20213。由于该值超出16位char类型的范围,因此它以UTF-16编码为替代对D840 DE13

其他答案中提到了获取字符长度的方法。但是,应谨慎使用,因为有很多方法可以用Unicode表示字符。“à”可以是1个组成字符或2个字符(a +变音符号)。像twitter一样,可能需要规范化。

您应该阅读
《绝对是每个软件开发人员的绝对最低要求》,绝对必须了解Unicode和字符集(无借口!)


6

这是因为length()仅适用于不大于的Unicode代码点U+FFFF。这组代码点被称为基本多语言平面(BMP),仅使用2个字节。

BMPUTF-16 外的Unicode代码点使用4字节代理对表示。

若要正确计算字符数(3),请使用 StringInfo

StringInfo b = new StringInfo("A𠈓C");
Console.WriteLine(string.Format("Length 2 = {0}", b.LengthInTextElements));

6

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

我们将“在纸上或屏幕上”看到的视为单个字母,字符,字形,符号或标点符号的东西视为单个文本元素。如Unicode标准附件#29 UNICODE TEXT SEGMENTATION中所述,每个Text元素由一个或多个Code Point表示。可以在此处找到详尽的代码列表。

每个代码点都需要编码为二进制,以供计算机内部表示。如上所述,每个char存储2个字节。等于或低于此的代码点U+FFFF可以存储为一个char。上面的代码点U+FFFF存储为代理对,使用两个字符表示单个代码点。

根据我们现在可以推断出的知识,文本元素可以存储为one char,两个字符的代理对,或者,如果文本元素由多个代码点表示,则可以将单个字符和代理对的某种组合存储。好像还不够复杂,某些文本元素可以用不同的代码点组合表示,如Unicode标准附件#15,UNICODE NORMALIZATION FORMS中所述


插曲

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

您可以重新编码.Net字符串。以便他们使用相同的规范化表格。标准化后,具有相同文本元素的两个字符串将以相同方式编码。为此,请使用string.Normalize函数。但是,请记住,某些不同的文本元素看起来彼此相似。:-s


那么,这与问题有关意味着什么呢?文本元素'𠈓'由单个代码点U + 20213 cjk统一表意文字扩展b表示。这意味着不能将其编码为单个char字符,而必须使用两个字符将其编码为代理对。这就是为什么string bchar更长的时间string a

如果您需要可靠地(请注意)计数a中文本元素的数量,string则应使用 System.Globalization.StringInfo此类。

using System.Globalization;

string a = "abc";
string b = "A𠈓C";

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

提供输出,

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

如预期的那样。


警告

StringInfoTextElementEnumerator类中的Unicode文本分段的.Net实现通常应该有用,并且在大多数情况下,将产生调用者期望的响应。但是,如Unicode标准附件#29中所述,“始终无法完全满足匹配用户感知的目标,因为仅文本本身并不总是包含足够的信息来明确地确定边界。”


我认为您的答案可能令人困惑。在这种情况下,𠈓只是一个代码点,但由于其代码点超过0xFFFF,因此必须使用代理对将其表示为2个代码单元。字素是建立在代码点之上的另一个概念,在这里,字素可以用一个或多个代码点表示,如韩文的韩文或许多基于拉丁语的语言所示。
nhahtdh 2014年

@nhahtdh,我同意,我的回答是错误的。我已经重写了它,希望现在可以使它更加清晰。
Jodrell 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.