如何从.NET字符串中获取Unicode代码点的数组?

21
我有一些字符范围限制的列表需要检查一个字符串,并且在.NET中,char类型是UTF-16,因此有些字符会变成奇怪的(代理)对。因此,在枚举string中的所有char时,我获取不到32位Unicode代码点,有些高值的比较将失败。

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

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

6个回答

24
您在询问有关“代码点”的内容。在UTF-16(C#的char)中,只有两种可能性:
  1. 字符来自基本多文种平面,并由单个代码单元编码。
  2. 字符位于BMP之外,并使用代理高低对代码单元进行编码。
因此,假设字符串有效,这将返回给定字符串的代码数组:
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时,它们被分解为一个音符头、组合茎、组合旗帜和组合重音-断音号,所有这些都是代理对:

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

请注意,leppie的解决方案是不正确的。这个问题涉及到代码点而不是文本元素。一个文本元素是由多个代码点组成的单个字形。例如,在上面的例子中,字符串中的ñ由拉丁小写字母n和一个组合波浪符̃◌表示。Leppie的解决方案会丢弃任何不能被规范化为单个代码点的组合字符。

1
我会使用 var codePoint = Char.ConvertToUtf32(...); if(codePoint > 0xFFFF) i++; 而不是 Char.IsHighSurrogate - CodesInChaos
@CodesInChaos:我相信这是等价的。只有当第一个字符是高代理项时,你才能得到一个在0xFFFF以上的码位,但如果我错了,请告诉我。 - Daniel A.A. Pelsmaeker
它是等价的。那只是一种风格上的建议。 - CodesInChaos
您可能还想在此处添加您的_Devanagari音节“ni”示例,即由两个代码点组成的单个文本元素,不会在任何规范化形式下合并为单个代码点。波浪符n,ñ,可以通过(适当的)规范化变成一个代码点。 - Jeppe Stig Nielsen
2
@JeppeStigNielsen 我反而添加了一个单个文本元素的示例,其中包含两个代码点,它们都是代理对,并在规范化下扩展为四个代码点的代理对。 - Daniel A.A. Pelsmaeker

7

这个答案不正确。请查看 @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();
}

注意:规范化是处理复合字符的必要步骤。


3
您的解决方案舍弃了任何修饰符号,并且处理的是文本元素而不是代码点。例如,对ExtractScalars("El Ni\u006E\u0303o")的结果进行字符串转换后得到的是"El Nino"而不是"El Niño" - Daniel A.A. Pelsmaeker
我意识到您可能会对我的ConvertToUtf32重载使用方式感到困惑。是的,现在已经解决了,但那不是问题所在。问题在于代理对和组合字符、文本元素和代码点之间的区别。您的代码确实处理了代理对。 - Daniel A.A. Pelsmaeker
@leppie 只有一些基本字符和组合字符的组合在规范化为FormC时才会变成单个代码点。因此,这个答案仍然是不正确的。当您想要一个代码点序列时,使用TextElement并不是正确的方法。 - CodesInChaos
2
是的,我正在研究这个问题。例如,天城体音节“ni”是一个可组合字符\u0928\u093F,在规范化时不会转变为一个代码点。另外,如果你有一个拉丁字符带有多个修饰符(例如^~),也不会被规范化为单个代码点。你必须接受你的代码处理的是_文本元素_(代表单个字形的代码点组合),通过执行ConvertToUtf32(e, 0)来舍弃除第一个以外的所有代码点。没有办法使你的代码能够使用文本元素来处理代码点。 - Daniel A.A. Pelsmaeker
1
另一种策略是这样的:var bytes = Encoding.UTF32.GetBytes(s); var ints = new int[bytes.Length / 4]; for (var idx = 0; idx < ints.Length; ++idx) { ints[idx] = BitConverter.ToInt32(bytes, 4 * idx); }。当然,您仍然可以首先规范化s。如果您想要奇怪的字节序,可以使用new UTF32Encoding(...) - Jeppe Stig Nielsen
显示剩余5条评论

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;
  }

}

BitConverter 使用本机字节序,而 Encoding.UTF32 使用小端字节序。因此,在大端系统上将无法正常工作。 - CodesInChaos
1
我只想说,我在leppie的答案下以评论的形式发布了与你提交的答案几乎相同的解决方案,并且提到了字节序问题,而这是在你提交答案之前的六秒钟内完成的。 - Jeppe Stig Nielsen
@JeppeStigNielsen:显然,伟大的思想是相似的 :) - Nicholas Carey

1

我采用了尼古拉斯(和杰普)建议的相同方法,只是更简短:

    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();

这与被接受的答案产生了相同的输出。谢谢! - Arundale Ramanathan

1

这个解决方案与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;
}

这会产生与被接受的答案相同的输出,即使对于ZWJ序列也是如此。谢谢! - Arundale Ramanathan

0

这里另一个解决方案

    public static int[] GetCodePoints(string input)
    {
        var cp_lst = new ArrayList();
        for (var i = 0; i < input.Length; i += char.IsSurrogatePair(input, i) ? 2 : 1) {
            int codepoint = char.ConvertToUtf32(input, i);
            cp_lst.Add(codepoint);
            //Console.WriteLine(codepoint);
        }
        return (int[]) cp_lst.ToArray(typeof(int));
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接