使用.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
不返回char
: char
是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
结构提供静态方法来处理代理: IsHighSurrogate
, IsLowSurrogate
, IsSurrogatePair
, ConvertToUtf32
和ConvertFromUtf32
。 如果需要,可以编写迭代器来迭代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
扩展方法有两个问题:
- 它会在无效的代码点上抛出
ArgumentException
(没有低代理的高代理,反之亦然)。 - 如果只有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与此作品相关联的人已放弃对此作品的所有版权及相关或相邻权利。
枚举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); } } }