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

时间:2013-05-29 14:24:12

标签: c# .net unicode char utf-16

我正在使用此代码生成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!");
    }
}

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

4 个答案:

答案 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不返回charchar是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提供了处理代理的静态方法:IsHighSurrogateIsLowSurrogateIsSurrogatePairConvertToUtf32ConvertFromUtf32。如果需要,可以编写迭代器来迭代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扩展方法有两个问题:

  1. 它会在无效代码点上抛出ArgumentException(没有低代理的高代理,反之亦然)。
  2. 如果您只有int代码点,则无法使用char(char)的{​​{1}}静态方法(例如(string, int))。
  3. 我已将代码拆分为两个方法,一个类似于原始方法但在无效代码点上返回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

    在法律允许的范围内,将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); } }
}