您如何从.NET字符串中获取一系列Unicode代码点?

时间:2009-03-26 20:03:46

标签: c# string unicode char astral-plane

我有一个字符范围限制列表,我需要检查字符串,但.NET中的char类型是UTF-16,因此某些字符变为古怪(代理)对。因此,当枚举char中的所有string时,我没有获得32位Unicode代码点,而某些高值比较失败。

我非常了解Unicode,如果有必要,我可以自己解析字节,但我正在寻找一个C#/ .NET Framework BCL解决方案。所以...

如何将string转换为32位Unicode代码点的数组(int[])?

5 个答案:

答案 0 :(得分:19)

您在询问代码点。在UTF-16(C#的char)中,只有两种可能性:

  1. 该字符来自基本多语言平面,由单个代码单元编码。
  2. 该字符位于 BMP 之外,并使用代理高低对代码单元进行编码
  3. 因此,假设字符串有效,则返回给定字符串的代码 points 数组:

    public static int[] ToCodePoints(string str)
    {
        if (str == null)
            throw new ArgumentNullException("str");
    
        var codePoints = new List<int>(str.Length);
        for (int i = 0; i < str.Length; i++)
        {
            codePoints.Add(Char.ConvertToUtf32(str, i));
            if (Char.IsHighSurrogate(str[i]))
                i += 1;
        }
    
        return codePoints.ToArray();
    }
    

    代理对和组合字符ñ的示例:

    ToCodePoints("\U0001F300 El Ni\u006E\u0303o");                        //  El Niño
    // { 0x1f300, 0x20, 0x45, 0x6c, 0x20, 0x4e, 0x69, 0x6e, 0x303, 0x6f } //    E l   N i n ̃◌ o
    

    这是另一个例子。这两个代码点代表一个带有断音重音的第32个音符,两个代理对:

    ToCodePoints("\U0001D162\U0001D181");              // 
    // { 0x1d162, 0x1d181 }                            //  ◌
    

    C-normalized时,它们会被分解为一个符头,结合词干,组合旗帜和组合口音 - staccato,所有代理对:

    ToCodePoints("\U0001D162\U0001D181".Normalize());  // 
    // { 0x1d158, 0x1d165, 0x1d170, 0x1d181 }          //    ◌
    

    请注意leppie's solution不正确。问题是关于代码点,而不是文本元素。文本元素是代码点的组合,它们一起形成单个字素。例如,在上面的示例中,字符串中的ñ由拉丁文小写n表示,后跟组合波浪号̃◌。 Leppie的解决方案会丢弃任何无法归一化为单个代码点的组合字符。

答案 1 :(得分:8)

这个答案不正确。有关正确的答案,请参阅@ Virtlink的答案。

static int[] ExtractScalars(string s)
{
  if (!s.IsNormalized())
  {
    s = s.Normalize();
  }

  List<int> chars = new List<int>((s.Length * 3) / 2);

  var ee = StringInfo.GetTextElementEnumerator(s);

  while (ee.MoveNext())
  {
    string e = ee.GetTextElement();
    chars.Add(char.ConvertToUtf32(e, 0));
  }

  return chars.ToArray();
}

备注:处理复合字符需要规范化。

答案 2 :(得分:4)

看起来它应该比这复杂得多:

public static IEnumerable<int> Utf32CodePoints( this IEnumerable<char> s )
{
  bool      useBigEndian = !BitConverter.IsLittleEndian;
  Encoding  utf32        = new UTF32Encoding( useBigEndian , false , true ) ;
  byte[]    octets       = utf32.GetBytes( s ) ;

  for ( int i = 0 ; i < octets.Length ; i+=4 )
  {
    int codePoint = BitConverter.ToInt32(octets,i);
    yield return codePoint;
  }

}

答案 3 :(得分:0)

我想出了Nicholas(和Jeppe)建议的same approach,只是更短:

    public static IEnumerable<int> GetCodePoints(this string s) {
        var utf32 = new UTF32Encoding(!BitConverter.IsLittleEndian, false, true);
        var bytes = utf32.GetBytes(s);
        return Enumerable.Range(0, bytes.Length / 4).Select(i => BitConverter.ToInt32(bytes, i * 4));
    }

枚举就是我所需要的,但是获取数组是微不足道的:

int[] codePoints = myString.GetCodePoints().ToArray();

答案 4 :(得分:0)

此解决方案产生的结果与the solution by Daniel A.A. Pelsmaeker相同,但要短一些:

public static int[] ToCodePoints(string s)
{
    byte[] utf32bytes = Encoding.UTF32.GetBytes(s);
    int[] codepoints = new int[utf32bytes.Length / 4];
    Buffer.BlockCopy(utf32bytes, 0, codepoints, 0, utf32bytes.Length);
    return codepoints;
}