使用.Net的大于2字节的unicode字符

我正在使用此代码生成U+10FFFC

 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!"); } } 

但是,由于这种非常大的字符分裂,这不起作用。 如何修改它以使其工作?

U + 10FFFC是一个Unicode代码点,但string的接口不直接公开Unicode代码点序列。 它的接口公开了一系列UTF-16代码单元。 这是一个非常低级别的文本视图。 非常不幸的是,这种低级别的文本视图被嫁接到最明显和最直观的界面上……我会尽量不去嘲笑我不喜欢这个设计,只是说不管怎样多么不幸,这只是一个(悲伤)事实,你必须忍受。

首先,我建议使用char.ConvertFromUtf32来获取您的初始字符串。 更简单,更可读:

 var s = char.ConvertFromUtf32(0x10FFFC); 

所以,这个字符串的Length不是1,因为正如我所说,接口处理的是UTF-16代码单元,而不是Unicode代码点。 U + 10FFFC使用两个UTF-16代码单元,因此s.Length为2. U + FFFF以上的所有代码点都需要两个UTF-16代码单元来表示它们。

您应该注意ConvertFromUtf32不返回charchar是UTF-16代码单元,而不是Unicode代码点。 为了能够返回所有Unicode代码点,该方法不能返回单个char 。 有时它需要返回两个,这就是为什么它使它成为一个字符串。 有时你会发现一些处理int而不是char API,因为int也可以用来处理所有代码点(这就是ConvertFromUtf32作为参数所采用的,以及ConvertToUtf32产生的结果)。

string实现了IEnumerable ,这意味着当你遍历一个string ,每次迭代都会获得一个UTF-16代码单元。 这就是为什么迭代你的字符串并将其打印出来会产生一些带有两个“东西”的破碎输出。 这些是构成U + 10FFFC表示的两个UTF-16代码单元。 他们被称为“代理人”。 第一个是高/领导代理,第二个是低/跟踪代理。 当您单独打印它们时,它们不会产生有意义的输出,因为单独的代理在UTF-16中甚至不是有效的,并且它们也不被视为Unicode字符。

当您将这两个代理项附加到循环中的字符串时,您可以有效地重建代理项对,并在以后打印该对,从而获得正确的输出。

在咆哮的前面,请注意在该循环中你没有抱怨你使用了格式错误的UTF-16序列。 它创建了一个带有单独代理的字符串,然而一切都继续进行,好像什么也没发生: string类型甚至不是格式良好的 UTF-16代码单元序列的类型,而是任何 UTF-16代码单元序列的类型。

char结构提供静态方法来处理代理: IsHighSurrogateIsLowSurrogateIsSurrogatePairConvertToUtf32ConvertFromUtf32 。 如果需要,可以编写迭代器来迭代Unicode字符而不是UTF-16代码单元:

 static IEnumerable 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 ,并将yield行更改为:

 yield return char.ConvertFromUtf32(char.ConvertToUtf32(s, i)); 

使用该版本,以下工作原样:

 foreach(string codePoint in s.AsCodePoints()) { Console.WriteLine(codePoint); } 

正如Martinho已经发布的那样,使用这个私有代码点创建字符串要容易得多:

 var s = char.ConvertFromUtf32(0x10FFFC); 

但是循环遍历该字符串的两个char元素是毫无意义的:

 foreach(var ch in s) { Console.WriteLine(ch); } 

做什么的? 您将获得编码代码点的高低代理。 请记住,char是16位类型,因此它只能保存最大值0xFFFF。 您的代码点不适合16位类型,实际上对于最高代码点,您需要21位(0x10FFFF),因此下一个更宽的类型将只是32位类型。 两个char元素不是字符,而是代理对。 0x10FFFC的值被编码到两个代理中。

而@R。 Martinho Fernandes的回答是正确的,他的AsCodePoints扩展方法有两个问题:

  1. 它会在无效的代码点上抛出ArgumentException (没有低代理的高代理,反之亦然)。
  2. 如果只有int代码点,则不能使用带有(char)(string, int) char静态方法(例如char.IsNumber() )。

我已经将代码拆分为两个方法,一个类似于原始方法但在无效代码点上返回Unicode替换字符 。 第二个方法返回一个带有更多有用字段的结构IEnumerable:

StringCodePointExtensions.cs

 public static class StringCodePointExtensions { const char ReplacementCharacter = '\ufffd'; public static IEnumerable CodePointIndexes(this string s) { for (int i = 0; i < s.Length; i++) { if (char.IsHighSurrogate(s, i)) { if (i + 1 < s.Length && char.IsLowSurrogate(s, i + 1)) { yield return CodePointIndex.Create(i, true, true); i++; continue; } else { // High surrogate without low surrogate yield return CodePointIndex.Create(i, false, false); continue; } } else if (char.IsLowSurrogate(s, i)) { // Low surrogate without high surrogate yield return CodePointIndex.Create(i, false, false); continue; } yield return CodePointIndex.Create(i, true, false); } } public static IEnumerable CodePointInts(this string s) { return s .CodePointIndexes() .Select( cpi => { if (cpi.Valid) { return char.ConvertToUtf32(s, cpi.Index); } else { return (int)ReplacementCharacter; } }); } } 

CodePointIndex.cs

 public struct CodePointIndex { public int Index; public bool Valid; public bool IsSurrogatePair; public static CodePointIndex Create(int index, bool valid, bool isSurrogatePair) { return new CodePointIndex { Index = index, Valid = valid, IsSurrogatePair = isSurrogatePair, }; } } 

CC0

在法律允许的范围内,将CC0与此作品相关联的人已放弃对此作品的所有版权及相关或相邻权利。

枚举C#字符串中的UTF32字符的另一种方法是使用System.Globalization.StringInfo.GetTextElementEnumerator方法,如下面的代码所示。

 public static class StringExtensions { public static System.Collections.Generic.IEnumerable GetUTF32Chars(this string s) { var tee = System.Globalization.StringInfo.GetTextElementEnumerator(s); while (tee.MoveNext()) { yield return new UTF32Char(s, tee.ElementIndex); } } } public struct UTF32Char { private string s; private int index; public UTF32Char(string s, int index) { this.s = s; this.index = index; } public override string ToString() { return char.ConvertFromUtf32(this.UTF32Code); } public int UTF32Code { get { return char.ConvertToUtf32(s, index); } } public double NumericValue { get { return char.GetNumericValue(s, index); } } public UnicodeCategory UnicodeCategory { get { return char.GetUnicodeCategory(s, index); } } public bool IsControl { get { return char.IsControl(s, index); } } public bool IsDigit { get { return char.IsDigit(s, index); } } public bool IsLetter { get { return char.IsLetter(s, index); } } public bool IsLetterOrDigit { get { return char.IsLetterOrDigit(s, index); } } public bool IsLower { get { return char.IsLower(s, index); } } public bool IsNumber { get { return char.IsNumber(s, index); } } public bool IsPunctuation { get { return char.IsPunctuation(s, index); } } public bool IsSeparator { get { return char.IsSeparator(s, index); } } public bool IsSurrogatePair { get { return char.IsSurrogatePair(s, index); } } public bool IsSymbol { get { return char.IsSymbol(s, index); } } public bool IsUpper { get { return char.IsUpper(s, index); } } public bool IsWhiteSpace { get { return char.IsWhiteSpace(s, index); } } }