var s = Encoding.UTF8.GetString(new byte[] {0xF4,0x8F,0xBF,0xBC});
我知道这是私人使用的,但它显示一个单一的字符,我期望显示它.操作这个unicode字符时出现问题.
如果我以后这样做:
foreach(var ch in s) { Console.WriteLine(ch); }
而不是打印单个字符,它打印两个字符(即字符串显然由两个字符组成).如果我改变我的循环,将这些字符添加回一个空字符串,如下所示:
string tmp=""; foreach(var ch in s) { Console.WriteLine(ch); tmp += ch; }
最后,tmp将只打印一个字符.
这里究竟发生了什么?我以为char包含一个unicode字符,我从来不必担心一个字符有多少个字节,除非我正在转换为字节.我的真实用例是,我需要能够检测到在字符串中使用非常大的unicode字符.目前我有这样的事情:
foreach(var ch in s) { if(ch>=0x100000 && ch<=0x10FFFF) { Console.WriteLine("special character!"); } }
然而,由于这种分割非常大的角色,这不行.我该如何修改这项工作?
解决方法
首先,我会建议您使用char.ConvertFromUtf32
获取您的初始字符串.更简单,更易读:
var s = char.ConvertFromUtf32(0x10FFFC);
所以,这个字符串的长度不是1,因为正如我所说,接口处理UTF-16代码单元,而不是Unicode代码点. U 10FFFC使用两个UTF-16代码单元,所以s.Length是2. U FFFF以上的所有代码点都需要两个UTF-16代码单元来表示.
您应该注意ConvertFromUtf32不返回char:char是UTF-16代码单元,而不是Unicode代码点.为了能够返回所有Unicode代码点,该方法不能返回单个字符.有时它需要返回两个,这就是为什么它使它成为一个字符串.有时你会发现一些API处理int而不是char,因为int可以用于处理所有的代码点(ConvertFromUtf32作为参数,ConvertToUtf32是什么样的结果).
字符串实现IEnumerable< char>,这意味着当您迭代字符串时,每次迭代可获得一个UTF-16代码单元.这就是为什么迭代你的字符串并打印出来,会产生一些破坏的输出,其中有两个“东西”.那些是组成U 10FFFC表示的两个UTF-16代码单元.他们被称为“代理人”.第一个是高/中等代价,第二个是低/低代价.当您单独打印它们时,它们不会产生有意义的输出,因为单个代理在UTF-16中甚至无效,并且它们也不被视为Unicode字符.
当您将这两个代理附加到循环中的字符串时,您可以有效地重建代理对,并在以后打印该对,从而获得正确的输出.
在咆哮的前面,请注意,没有人抱怨在该循环中使用了畸形的UTF-16序列.它创建一个具有孤立代理的字符串,但是一切都没有发生:字符串类型甚至不是UTF-16格式的单元序列,而是UTF-16代码单元序列的类型.
The char
structure提供静态方法来处理代理:IsHighSurrogate,IsLowSurrogate,IsSurrogatePair,ConvertToUtf32和ConvertFromUtf32.如果您希望可以编写一个迭代器来迭代Unicode字符而不是UTF-16代码单元:
static IEnumerable<int> AsCodePoints(this string s) { for(int i = 0; i < s.Length; ++i) { yield return char.ConvertToUtf32(s,i); if(char.IsHighSurrogate(s,i)) i++; } }
那么你可以像下面这样迭代:
foreach(int codePoint in s.AsCodePoints()) { // do stuff. codePoint will be an int will value 0x10FFFC in your example }
如果您希望将每个代码点作为字符串,而不是将返回类型更改为IEnumerable< string>而收益行为:
yield return char.ConvertFromUtf32(char.ConvertToUtf32(s,i));
使用该版本,以下工作原样是:
foreach(string codePoint in s.AsCodePoints()) { Console.WriteLine(codePoint); }