2013-05-29 25 views
11

Używam tego kodu do generowania U+10FFFCUżywanie znaków Unicode większych niż 2 bajty z .Net

var s = Encoding.UTF8.GetString(new byte[] {0xF4,0x8F,0xBF,0xBC}); 

Wiem, że to dla prywatnego użytku i takich, ale nie wyświetla pojedynczy znak jak ja oczekiwać podczas wyświetlania. Problemy pojawiają się podczas manipulowania tą unikodową postacią.

Gdybym później to zrobić:

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

zamiast niego drukowanie tylko pojedynczy znak, drukuje dwa znaki (czyli ciąg jest najwyraźniej składa się z dwóch znaków). Gdybym zmieniać mój pętlę, aby dodać te znaki z powrotem do pustego łańcucha tak:

string tmp=""; 
foreach(var ch in s) 
{ 
    Console.WriteLine(ch); 
    tmp += ch; 
} 

Pod koniec tego tmp będzie drukować tylko jedną postać.

Co dokładnie się tutaj dzieje? Pomyślałem, że char zawiera jedną znak Unicode i nigdy nie musiałem się martwić o to, ile bajtów jest znaków, chyba że robię konwersję na bajty. Moim prawdziwym przypadkiem użycia jest potrzeba wykrycia, kiedy w łańcuchu używane są bardzo duże znaki Unicode. Obecnie mam coś takiego:

foreach(var ch in s) 
{ 
    if(ch>=0x100000 && ch<=0x10FFFF) 
    { 
     Console.WriteLine("special character!"); 
    } 
} 

Jednak z powodu tego podziału bardzo dużych znaków, to nie działa. Jak mogę to zmienić, aby działało?

Odpowiedz

29

U + 10FFFC to jeden kodowy kod Unicode, ale interfejs string nie odsłania bezpośrednio sekwencji punktów kodu Unicode. Jego interfejs udostępnia sekwencję jednostek kodowych UTF-16. To jest bardzo niski poziom tekstu. To dość niefortunne, że taki niskopoziomowy widok tekstu został przeszczepiony na najbardziej oczywisty i intuicyjny interfejs ... Postaram się nie wyrażać zbyt wiele na temat tego, jak nie podoba mi się ten projekt, i po prostu powiedzieć, że nie ma znaczenia jak nieszczęśliwy, to tylko (smutny) fakt, z którym musisz żyć.

Po pierwsze, zasugeruję użycie char.ConvertFromUtf32, aby uzyskać początkowy ciąg znaków. Znacznie prostsza, o wiele bardziej czytelny:

var s = char.ConvertFromUtf32(0x10FFFC); 

Tak, ten ciąg na Length nie jest 1, ponieważ, jak powiedziałem, zajmuje interfejsu w UTF-16 jednostek, a nie kodu Unicode punkty kodowe. U + 10FFFC używa dwóch jednostek kodowych UTF-16, więc s.Length ma wartość 2. Wszystkie punkty kodowe powyżej U + FFFF wymagają dwóch jednostek kodowych UTF-16 do ich reprezentacji.

Należy zauważyć, że ConvertFromUtf32 nie zwraca wartości char: char to jednostka kodowa UTF-16, a nie kodowy kod Unicode. Aby móc zwracać wszystkie punkty kodu Unicode, ta metoda nie może zwrócić pojedynczego char. Czasami trzeba zwrócić dwa, i dlatego robi to ciąg. Czasami znajdziesz kilka interfejsów API obsługujących int zamiast char, ponieważ int może być używany do obsługi wszystkich punktów kodowych (to jest to, co ConvertFromUtf32 bierze jako argument, a co ConvertToUtf32 tworzy jako wynik).

string realizuje IEnumerable<char>, co oznacza, że ​​podczas iteracji nad string dostać jeden UTF-16 jednostki kodu na iteracji. To dlatego iteracja twojego ciągu i wydrukowanie go daje pewne zepsute wyniki z dwoma "rzeczami" w nim. Są to dwie jednostki kodowe UTF-16, które składają się na reprezentację U + 10FFFC.Nazywa się je "surogatami". Pierwszy to surogat o wysokiej/ołowiu, a drugi to surogat nisko/szlakowy. Gdy drukujesz je pojedynczo, nie dają one znaczących wyników, ponieważ samotne surogaty nie są nawet poprawne w UTF-16 i nie są one również uważane za znaki Unicode.

Po dołączeniu tych dwóch surogatów do struny w pętli, skutecznie zrekonstruujesz zastępczą parę i wydrukujesz tę parę później jako jeden, aby uzyskać właściwe wyjście.

I na tyłku notowania, zauważ, że nic nie narzeka, że ​​użyłeś źle sformułowanej sekwencji UTF-16 w tej pętli. To tworzy ciąg z samotnym surogat, a mimo wszystko wykonuje tak, jakby nic się nie stało: typ string nie jest nawet rodzaj sensownych UTF-16 sekwencji jednostkowych kod, ale rodzaj dowolny UTF-16 sekwencja jednostek kodu.

The char structure zapewnia statyczne metody radzenia sobie z surogatów: IsHighSurrogate, IsLowSurrogate, IsSurrogatePair, ConvertToUtf32 i ConvertFromUtf32. Jeśli chcesz możesz napisać iterator że iteracje nad znaków Unicode zamiast UTF-16 jednostek kod:

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

Następnie można iterację jak:

foreach(int codePoint in s.AsCodePoints()) 
{ 
    // do stuff. codePoint will be an int will value 0x10FFFC in your example 
} 

Jeśli wolisz, aby każdy punkt kodu jako ciąg zamiast zmienić typ zwracany do IEnumerable<string> i linię ustąpić:

yield return char.ConvertFromUtf32(char.ConvertToUtf32(s, i)); 

z tą wersją, następujące prace jak jest:

foreach(string codePoint in s.AsCodePoints()) 
{ 
    Console.WriteLine(codePoint); 
} 
0

Jak pisał już przez Martinho, jest o wiele łatwiej stworzyć łańcuch z tej prywatnej punkt kodowy w ten sposób:

var s = char.ConvertFromUtf32(0x10FFFC); 

Ale pętli dwóch char elementów tego łańcucha nie ma sensu:

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

Po co? Otrzymasz tylko wysoki i niski surogat kodujący kodepoint. Pamiętaj, że char jest typem 16-bitowym, więc może przechowywać tylko maksymalną wartość 0xFFFF. Twój punkt kodowy nie pasuje do typu 16 bitowego, w rzeczy samej dla najwyższego codepoint potrzebujesz 21 bitów (0x10FFFF), więc następny szerszy typ będzie po prostu 32-bitowym typem. Dwa elementy char nie są znakami, ale zastępczą parą. Wartość 0x10FFFC jest zakodowana w dwóch surogatach.

0

Podczas gdy @R. Martinho Fernandes odpowiedź jest poprawna, jego AsCodePoints metodę rozszerzenia ma dwa problemy:

  1. To wygeneruje ArgumentException na nieprawidłowych punktów kodowych (wysoki surogat bez niskim surogat lub odwrotnie).
  2. Nie można użyć statycznych metod , które pobierają (char) lub (string, int) (np. char.IsNumber()), jeśli mają tylko punkty kodu int.

Podzielę kod na dwie metody, jedną podobną do oryginalnej, ale zwraca Unicode Replacement Character na nieprawidłowe punkty kodu.Druga metoda zwraca struct IEnumerable z bardziej przydatnych dziedzin:

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

w możliwie szerokim zakresie mocy prawa, osoba, która powiązanych CC0 w tej pracy zrzekł się wszelkich praw autorskich i pokrewnych lub sąsiadujących t o tę pracę.

0

Jeszcze inną alternatywą dla wyliczenia znaków UTF32 w łańcuchu C# jest użycie metody System.Globalization.StringInfo.GetTextElementEnumerator, jak w poniższym kodzie.

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); } } 
} 
+0

System.Globalization.StringInfo jest drogą do zrobienia. Reszta kodu jest nieprawidłowa. Zajrzyj na: https://msdn.microsoft.com/en-us/library/system.globalization.stringinfo(v=vs.110).aspx – X181

+0

Nie jest jasne, co masz na myśli. Czy jest jakiś problem z kodem z tej odpowiedzi? –