13

我正在使用此代码生成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

4 回答 4

40

U+10FFFC 是一个 Unicode 码点,但string' 的接口并没有直接暴露一系列 Unicode 码点。它的接口公开了一系列 UTF-16 代码单元。这是一个非常低级的文本视图。不幸的是,如此低级的文本视图被嫁接到了最明显和最直观的界面上......我会尽量不要过多地抱怨我不喜欢这种设计,只是说没关系多么不幸,这只是你必须忍受的(悲伤的)事实。

首先,我会建议使用char.ConvertFromUtf32来获取您的初始字符串。更简单,更具可读性:

var s = char.ConvertFromUtf32(0x10FFFC);

所以,这个字符串Length不是 1,因为正如我所说,接口处理 UTF-16 代码单元,而不是 Unicode 代码点。U+10FFFC 使用两个 UTF-16 代码单元,所以s.Length2。所有高于 U+FFFF 的代码点都需要两个 UTF-16 代码单元来表示。

您应该注意,ConvertFromUtf32不返回char:char是 UTF-16 代码单元,而不是 Unicode 代码点。为了能够返回所有 Unicode 代码点,该方法不能返回单个char. 有时它需要返回两个,这就是为什么它使它成为一个字符串。有时您会发现一些处理ints 而不是charbecause的 APIint也可用于处理所有代码点(这就是ConvertFromUtf32作为参数的内容,以及ConvertToUtf32作为结果产生的内容)。

stringimplements IEnumerable<char>,这意味着当您迭代 a 时,string每次迭代都会获得一个 UTF-16 代码单元。这就是为什么迭代你的字符串并将其打印出来会产生一些带有两个“东西”的损坏输出。这些是构成 U+10FFFC 表示的两个 UTF-16 代码单元。他们被称为“代理人”。第一个是高/领先替代品,第二个是低/落后替代品。当您单独打印它们时,它们不会产生有意义的输出,因为单独的代理在 UTF-16 中甚至无效,并且它们也不被视为 Unicode 字符。

当您将这两个代理项附加到循环中的字符串时,您可以有效地重建代理项对,并在稍后打印该对,因为其中一个会为您提供正确的输出。

在咆哮的前面,请注意没有人抱怨您在该循环中使用了格式错误的 UTF-16 序列。它创建了一个带有唯一代理项的字符串,但一切都像什么都没发生一样继续进行:该string类型甚至不是格式良好的UTF-16 代码单元序列的类型,而是任何UTF-16 代码单元序列的类型。

char结构提供了处理代理项的静态方法: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 return char.ConvertFromUtf32(char.ConvertToUtf32(s, i));

使用该版本,以下内容按原样工作:

foreach(string codePoint in s.AsCodePoints())
{
     Console.WriteLine(codePoint);
}
于 2013-05-29T16:48:49.857 回答
1

正如 Martinho 已经发布的那样,以这种方式使用此私有代码点创建字符串要容易得多:

var s = char.ConvertFromUtf32(0x10FFFC);

但是遍历该字符串的两个 char 元素是没有意义的:

foreach(var ch in s)
{
    Console.WriteLine(ch);
}

做什么的?您将只获得编码代码点的高和低代理。请记住,char 是 16 位类型,因此它只能保存 0xFFFF 的最大值。您的代码点不适合 16 位类型,实际上对于最高代码点,您需要 21 位 (0x10FFFF),因此下一个更宽的类型将只是 32 位类型。这两个 char 元素不是字符,而是代理对。0x10FFFC 的值被编码到两个代理项中。

于 2014-06-17T12:20:54.560 回答
0

而@R。Martinho Fernandes 的回答是正确的,他的AsCodePoints扩展方法有两个问题:

  1. 它将抛出一个ArgumentException无效代码点(高代理没有低代理,反之亦然)。
  2. 如果您只有 int 代码点,则不能使用采用或(例如)的char静态方法。(char)(string, int)char.IsNumber()

我将代码分成两种方法,一种类似于原始方法,但在无效代码点上返回Unicode 替换字符。第二种方法返回一个包含更多有用字段的 struct IEnumerable:

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

public struct CodePointIndex {
    public int Index;
    public bool Valid;
    public bool IsSurrogatePair;

    public static CodePointIndex Create(int index, bool valid, bool isSurrogatePair) {
        return new CodePointIndex {
            Index = index,
            Valid = valid,
            IsSurrogatePair = isSurrogatePair,
        };
    }
}

CC0

在法律可能的范围内,将 CC0 与本作品相关联的人已放弃本作品的所有版权和相关或邻接权。

于 2016-05-26T08:18:26.357 回答
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); } }
}
于 2016-11-28T10:33:47.640 回答