我正在使用此代码生成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!");
}
}
但是,由于这种非常大的字符分裂,这不起作用。如何修改它以使其工作?
答案 0 :(得分:36)
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
。有时它需要返回两个,这就是为什么它使它成为一个字符串。有时您会发现一些API在int
而不是char
处理,因为int
也可用于处理所有代码点(这是ConvertFromUtf32
作为参数的内容,以及ConvertToUtf32
生成结果)。
string
实现IEnumerable<char>
,这意味着当您遍历string
时,每次迭代会获得一个UTF-16代码单元。这就是为什么迭代你的字符串并将其打印出来会产生一些带有两个“东西”的破碎输出。这些是构成U + 10FFFC表示的两个UTF-16代码单元。他们被称为“代理人”。第一个是高/领导代理,第二个是低/跟踪代理。当您单独打印它们时,它们不会产生有意义的输出,因为单独的代理在UTF-16中甚至不是有效的,并且它们也不被视为Unicode字符。
当您将这两个代理项附加到循环中的字符串时,您可以有效地重建代理项对,然后打印该对作为一个可以获得正确的输出。
在咆哮的前面,请注意在该循环中你没有抱怨你使用了格式错误的UTF-16序列。它创建了一个带有单独代理的字符串,然而一切都继续进行,好像什么也没发生:string
类型甚至不是格式良好的 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行更改为:
yield return char.ConvertFromUtf32(char.ConvertToUtf32(s, i));
使用该版本,以下工作原样:
foreach(string codePoint in s.AsCodePoints())
{
Console.WriteLine(codePoint);
}
答案 1 :(得分:0)
正如Martinho已经发布的那样,用这个私有代码点创建字符串要容易得多:
var s = char.ConvertFromUtf32(0x10FFFC);
但循环遍历该字符串的两个char元素是毫无意义的:
foreach(var ch in s)
{
Console.WriteLine(ch);
}
为什么?您将获得编码代码点的高和低代理。请记住,char是16位类型,因此它只能保存最大值0xFFFF。您的代码点不适合16位类型,实际上对于最高代码点,您需要21位(0x10FFFF),因此下一个更宽的类型将只是32位类型。两个char元素不是字符,而是代理对。 0x10FFFC的值被编码到两个代理中。
答案 2 :(得分:0)
而@R。 Martinho Fernandes的回答是正确的,他的AsCodePoints
扩展方法有两个问题:
ArgumentException
(没有低代理的高代理,反之亦然)。char
或(char)
的{{1}}静态方法(例如(string, int)
)。我已将代码拆分为两个方法,一个类似于原始方法但在无效代码点上返回Unicode Replacement Character。第二种方法返回一个带有更多有用字段的结构IEnumerable:
char.IsNumber()
StringCodePointExtensions.cs
public static class StringCodePointExtensions {
const char ReplacementCharacter = '\ufffd';
public static IEnumerable<CodePointIndex> 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<int> CodePointInts(this string s) {
return s
.CodePointIndexes()
.Select(
cpi => {
if (cpi.Valid) {
return char.ConvertToUtf32(s, cpi.Index);
} else {
return (int)ReplacementCharacter;
}
});
}
}
:
CodePointIndex.cs
在法律允许的范围内,将CC0与此作品相关联的人已放弃对此作品的所有版权及相关或相邻权利。 子>
答案 3 :(得分:0)
在C#字符串中枚举UTF32字符的另一种方法是使用System.Globalization.StringInfo.GetTextElementEnumerator
方法,如下面的代码所示。
public static class StringExtensions
{
public static System.Collections.Generic.IEnumerable<UTF32Char> 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); } }
}