将Unicode代理对转换为文字字符串。

18

我试图从一个字符串中读取一个高Unicode字符并将其复制到另一个字符串中。为了简洁起见,我将简化我的代码如下所示:

public static void UnicodeTest()
{
    var highUnicodeChar = ""; //Not the standard A

    var result1 = highUnicodeChar; //this works
    var result2 = highUnicodeChar[0].ToString(); // returns \ud835
}

当我直接将highUnicodeChar分配给result1时,它会保留其字面值。当我尝试通过索引访问它时,它返回\ud835。据我了解,这是用于表示UTF-32字符的UTF-16字符代理对。我相当确定这个问题与尝试隐式转换charstring有关。

最终,我希望result2产生与result1相同的值。我该怎么做?

2个回答

32
Unicode中,有代码点。它们长度为21位。你的字符数学粗体大写字母A的代码点是U+1D400。
在Unicode编码中,你有代码单元。这是编码的自然单位:UTF-88位UTF-1616位等。一个或多个代码单元编码一个代码点。
在UTF-16中,形成一个单一代码点的两个代码单元称为代理对。代理对用于编码大于16位的任何代码点,即U+10000及以上。
在.NET中,这变得有点棘手,因为.NET Char表示单个UTF-16代码单元,而.NET String是代码单元的集合。
所以你的代码点(U+1D400)无法适应16位并需要代理对,这意味着你的字符串中有两个代码单元:
var highUnicodeChar = "";
char a = highUnicodeChar[0]; // code unit 0xD835
char b = highUnicodeChar[1]; // code unit 0xDC00

当您像这样索引字符串时,实际上只获取了代理对的一半。

您可以使用IsSurrogatePair来测试代理对。例如:

string GetFullCodePointAtIndex(string s, int idx) =>
    s.Substring(idx, char.IsSurrogatePair(s, idx) ? 2 : 1);

重要的是要注意,在Unicode中变量编码的兔子洞并不止于代码点。一个“字形簇”是大多数人在被问及时最终会称之为“字符”的“可见物”。一个字形簇由一个或多个代码点组成:一个基本字符和零个或多个组合字符。组合字符的一个例子是umlaut或其他各种想要添加的装饰/修饰符。请参阅此答案,了解组合字符可以做什么的可怕示例。
要测试是否为组合字符,您可以使用GetUnicodeCategory检查封闭标记、非间距标记或间距标记。

太棒了!这个解决方案正是我所寻找的,而且解释得非常好。 - hargle
“零个或多个组合字符”的示例可以在https://dev59.com/X3I-5IYBdhLWcg3wq6do上看到,也有一些工具可以生成这样的字符,例如https://lingojam.com/GlitchTextGenerator。 - Ismael Miguel
1
“代码点长度为21位” - 虽然21位是表示任何代码点所需的数据量,但实际上说这是“代码点的长度”并没有太大意义。我认为这种表示在重要的地方都没有使用;对于直接访问代码点,您实际上会使用UTF-32或者将代码点存储在64位甚至128位中,以实现内存统一性。此外:变音符通常不作为组合字符实现,因为大多数组合已经分配了单个代码点。 - leftaroundabout
@leftaroundabout 当区分Unicode和其编码之一时,我发现“21位”概念是让人们摆脱“UTF32=代码点”的错误理解的好方法。 - Cory Nelson

9

看起来你想从用户角度提取出第一个“原子”字符(即第一个Unicode字形簇),该字符包括代理对的两个半部分。你可以使用StringInfo.GetTextElementEnumerator()来实现这一点,将string拆分成原子块,然后取出第一个。

首先,定义以下扩展方法:

public static class TextExtensions
{
    public static IEnumerable<string> TextElements(this string s)
    {
        // StringInfo.GetTextElementEnumerator is a .Net 1.1 class that doesn't implement IEnumerable<string>, so convert
        if (s == null)
            yield break;
        var enumerator = StringInfo.GetTextElementEnumerator(s);
        while (enumerator.MoveNext())
            yield return enumerator.GetTextElement();
    }
}

现在,您可以做到以下事情:
var result2 = highUnicodeChar.TextElements().FirstOrDefault() ?? "";

请注意,StringInfo.GetTextElementEnumerator() 方法也会对 Unicode 组合字符 进行分组,因此字符串 Ĥ=T̂+V̂ 的第一个字形簇将是 而不是 H
示例代码片段请查看此处

+1 这是唯一合理的方法。不幸的是,.NET API(与大多数其他语言相同)并没有完全鼓励这种方法。应该为.NET编写一个linter,将string内非常随机的char访问标记为错误。 - Konrad Rudolph

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