Warum ist die Länge dieser Zeichenfolge länger als die Anzahl der darin enthaltenen Zeichen?

145

Dieser Code:

string a = "abc";
string b = "A𠈓C";
Console.WriteLine("Length a = {0}", a.Length);
Console.WriteLine("Length b = {0}", b.Length);

Ausgänge:

Length a = 3
Length b = 4

Warum? Das einzige, was ich mir vorstellen kann, ist, dass das chinesische Schriftzeichen 2 Bytes lang ist und dass die .LengthMethode die Anzahl der Bytes zurückgibt.

weini37
quelle
10
Woher wusste ich, dass es ein Ersatzpaarproblem war, nur weil ich mir den Titel angesehen hatte? Ah, gutes altes System. Globalisierung ist dein Verbündeter!
Chris Cirefice
9
es ist 4 Bytes lang in UTF-16, nicht 2
phuclv
Der Dezimalwert des 𠈓Zeichens ist 131603, und da Zeichen vorzeichenlose Bytes sind, bedeutet dies, dass Sie diesen Wert in 2 statt in 4 Zeichen erreichen können (maximaler vorzeichenloser 16-Bit-Wert beträgt 65535 (oder 65536 Variationen) und die Verwendung von 2 Zeichen zur Darstellung ermöglicht für eine maximale Anzahl von Variationen von nicht 65536 * 2 (131072), sondern 65536 * 65536 Variationen (4.294.967.296, effektiv ein 32-Bit-Wert)
GMasucci
3
@ GMAsucci: Es sind 2 Zeichen in UTF-16, aber 4 Bytes, weil ein UTF16-Zeichen 2 Bytes groß ist, sonst könnte es nicht 65536 Variationen speichern, sondern nur 256.
Kaiserludi
4
Ich empfehle, den großartigen Artikel "Das absolute Minimum, das jeder Softwareentwickler unbedingt
lesen.

Antworten:

232

Alle anderen geben die oberflächliche Antwort, aber es gibt auch eine tiefere Begründung: Die Anzahl der "Zeichen" ist eine schwer zu definierende Frage und kann überraschend teuer zu berechnen sein, während eine Längeneigenschaft schnell sein sollte.

Warum ist es schwer zu definieren? Nun, es gibt ein paar Optionen und keine ist wirklich gültiger als die andere:

  • Die Anzahl der Codeeinheiten (Bytes oder andere Datenblöcke mit fester Größe; C # und Windows verwenden normalerweise UTF-16, sodass die Anzahl der Zwei-Byte-Teile zurückgegeben wird) ist sicherlich relevant, da der Computer die Daten in dieser Form noch verarbeiten muss für viele Zwecke (das Schreiben in eine Datei kümmert sich beispielsweise eher um Bytes als um Zeichen)

  • Die Anzahl der Unicode-Codepunkte ist ziemlich einfach zu berechnen (obwohl O (n), weil Sie die Zeichenfolge nach Ersatzpaaren durchsuchen müssen) und kann für einen Texteditor von Bedeutung sein. Sie entspricht jedoch nicht der Anzahl der Zeichen auf dem Bildschirm gedruckt (Grapheme genannt). Beispielsweise können einige Buchstaben mit Akzent in zwei Formen dargestellt werden: ein einzelner Codepunkt oder zwei miteinander gepaarte Punkte, einer für den Buchstaben und einer mit der Aufschrift "Fügen Sie meinem Partnerbrief einen Akzent hinzu". Wäre das Paar zwei Zeichen oder eins? Sie können Zeichenfolgen normalisieren, um dies zu unterstützen, aber nicht alle gültigen Buchstaben haben eine einzige Codepunktdarstellung.

  • Selbst die Anzahl der Grapheme entspricht nicht der Länge einer gedruckten Zeichenfolge, was unter anderem von der Schriftart abhängt. Da einige Zeichen in vielen Schriftarten (Kerning) mit einer gewissen Überlappung gedruckt werden, wird die Länge einer Zeichenfolge auf dem Bildschirm angezeigt ist sowieso nicht unbedingt gleich der Summe der Länge der Grapheme!

  • Einige Unicode-Punkte sind nicht einmal Zeichen im herkömmlichen Sinne, sondern eine Art Kontrollmarker. Wie eine Bytereihenfolge-Markierung oder eine Anzeige von rechts nach links. Zählen diese?

Kurz gesagt, die Länge eines Strings ist tatsächlich eine lächerlich komplexe Frage, und die Berechnung kann sowohl CPU-Zeit als auch Datentabellen in Anspruch nehmen.

Was ist der Sinn? Warum sind diese Metriken wichtig? Nun, nur Sie können das für Ihren Fall beantworten, aber ich persönlich finde, dass sie im Allgemeinen irrelevant sind. Die Einschränkung der Dateneingabe erfolgt logischerweise eher durch Byte-Limits, da diese ohnehin übertragen oder gespeichert werden müssen. Das Begrenzen der Anzeigegröße wird besser von der Anzeigeseiten-Software durchgeführt. Wenn Sie 100 Pixel für die Nachricht haben, hängt die Anzahl der Zeichen von der Schriftart usw. ab, was der Datenschicht-Software ohnehin nicht bekannt ist. Angesichts der Komplexität des Unicode-Standards werden Sie wahrscheinlich ohnehin Fehler an den Randfällen haben, wenn Sie etwas anderes ausprobieren.

Es ist also eine schwierige Frage, die nicht häufig für allgemeine Zwecke verwendet wird. Die Anzahl der Codeeinheiten ist trivial zu berechnen - es ist nur die Länge des zugrunde liegenden Datenarrays - und in der Regel die aussagekräftigste / nützlichste mit einer einfachen Definition.

Das ist der Grund, warum bdie Länge 4über die oberflächliche Erklärung von "weil die Dokumentation dies sagt" hinausgeht.

Adam D. Ruppe
quelle
9
Im Wesentlichen ist '.Length' nicht das, was die meisten Programmierer denken. Vielleicht sollte es eine Reihe spezifischerer Eigenschaften (z. B. GlyphCount) und Länge geben, die als veraltet markiert sind!
Redcalx
8
@locster Ich stimme zu, denke aber nicht, dass Lengthes veraltet sein sollte, um die Analogie mit Arrays beizubehalten.
Kroltan
2
@locster Es sollte nicht veraltet sein. Die Python macht sehr viel Sinn und niemand stellt sie in Frage.
Simonzack
1
Ich denke. Länge macht sehr viel Sinn und ist eine natürliche Eigenschaft, solange Sie verstehen, was es ist und warum es so ist. Dann funktioniert es wie jedes andere Array (in einigen Sprachen wie D ist ein String buchstäblich ein Array, was die Sprache betrifft, und es funktioniert wirklich gut)
Adam D. Ruppe
4
Das ist nicht wahr (ein häufiges Missverständnis) - mit UTF-32 würde lengthInBytes / 4 die Anzahl der Codepunkte angeben , aber das entspricht nicht der Anzahl der "Zeichen" oder Grapheme. Betrachten Sie LATIN SMALL LETTER E, gefolgt von einer KOMBINIERENDEN DIAERESE ... die als einzelnes Zeichen gedruckt wird. Sie kann sogar auf einen einzelnen Codepunkt normalisiert werden, ist aber selbst in UTF-32 immer noch zwei Einheiten lang.
Adam D. Ruppe
62

Aus der Dokumentation der String.LengthImmobilie:

Die Length-Eigenschaft gibt die Anzahl der Char- Objekte in dieser Instanz zurück, nicht die Anzahl der Unicode-Zeichen. Der Grund dafür ist , dass ein Unicode - Zeichen kann von mehr als einer dargestellt werden Char . Verwenden Sie die System.Globalization.StringInfo Klasse Arbeit mit jedem Unicode - Zeichen anstelle eines jeden Char .

Kindermädchen
quelle
3
Java verhält sich genauso (druckt auch 4 für String b), da es die UTF-16-Darstellung in char-Arrays verwendet. Es ist ein 4-Byte-Zeichen in UTF-8.
Michael
32

Dein Charakter bei Index 1 in "A𠈓C"ist ein SurrogatePair

Der wichtigste Punkt, an den Sie sich erinnern sollten, ist, dass Ersatzpaare 32-Bit- Einzelzeichen darstellen.

Sie können diesen Code ausprobieren und er wird zurückgegeben True

Console.WriteLine(char.IsSurrogatePair("A𠈓C", 1));

Char.IsSurrogatePair-Methode (String, Int32)

trueWenn der Parameter s benachbarte Zeichen an den Positionen Index und Index + 1 enthält und der numerische Wert des Zeichens am Positionsindex von U + D800 bis U + DBFF reicht und der numerische Wert des Zeichens am Positionsindex + 1 von U reicht + DC00 bis U + DFFF; sonst , false.

Dies wird in der Eigenschaft String.Length näher erläutert :

Die Length-Eigenschaft gibt die Anzahl der Char-Objekte in dieser Instanz zurück, nicht die Anzahl der Unicode-Zeichen. Der Grund ist, dass ein Unicode-Zeichen durch mehr als ein Zeichen dargestellt werden kann. Verwenden Sie die System.Globalization.StringInfo-Klasse, um mit jedem Unicode-Zeichen anstelle jedes Zeichens zu arbeiten.

Habib
quelle
24

Wie die anderen Antworten gezeigt haben, werden sie, selbst wenn 3 sichtbare Zeichen vorhanden sind, mit 4 charObjekten dargestellt. Deshalb Lengthist das 4 und nicht 3.

MSDN gibt das an

Die Length-Eigenschaft gibt die Anzahl der Char-Objekte in dieser Instanz zurück, nicht die Anzahl der Unicode-Zeichen.

Wenn Sie jedoch wirklich die Anzahl der "Textelemente" und nicht die Anzahl der CharObjekte wissen möchten, können Sie die StringInfoKlasse verwenden.

var si = new StringInfo("A𠈓C");
Console.WriteLine(si.LengthInTextElements); // 3

Sie können auch jedes Textelement wie folgt auflisten

var enumerator = StringInfo.GetTextElementEnumerator("A𠈓C");
while(enumerator.MoveNext()){
    Console.WriteLine(enumerator.Current);
}

Bei Verwendung foreachder Zeichenfolge wird der mittlere "Buchstabe" in zwei charObjekte aufgeteilt, und das gedruckte Ergebnis entspricht nicht der Zeichenfolge.

dee-see
quelle
20

Dies liegt daran, dass die LengthEigenschaft die Anzahl der Zeichenobjekte und nicht die Anzahl der Unicode-Zeichen zurückgibt . In Ihrem Fall wird eines der Unicode-Zeichen durch mehr als ein Zeichenobjekt (SurrogatePair) dargestellt.

Die Length-Eigenschaft gibt die Anzahl der Char-Objekte in dieser Instanz zurück, nicht die Anzahl der Unicode-Zeichen. Der Grund ist, dass ein Unicode-Zeichen durch mehr als ein Zeichen dargestellt werden kann. Verwenden Sie die System.Globalization.StringInfo-Klasse, um mit jedem Unicode-Zeichen anstelle jedes Zeichens zu arbeiten.

Yuval Itzchakov
quelle
1
Sie haben eine mehrdeutige Verwendung von "Zeichen" in dieser Antwort. Ich schlage vor, mindestens die erste durch eine genaue Terminologie zu ersetzen.
Leichtigkeitsrennen im Orbit
1
Danke dir. Die Mehrdeutigkeit wurde behoben.
Yuval Itzchakov
10

Wie andere sagten, ist es nicht die Anzahl der Zeichen in der Zeichenfolge, sondern die Anzahl der Char-Objekte. Das Zeichen 𠈓 ist der Codepunkt U + 20213. Da der Wert außerhalb des Bereichs des 16-Bit-Zeichentyps liegt, wird er in UTF-16 als Ersatzpaar codiert D840 DE13.

Der Weg, um die Länge in Zeichen zu erhalten, wurde in den anderen Antworten erwähnt. Es sollte jedoch mit Vorsicht verwendet werden, da es viele Möglichkeiten gibt, ein Zeichen in Unicode darzustellen. "à" kann 1 zusammengesetztes Zeichen oder 2 Zeichen (a + diakritische Zeichen) sein. Eine Normalisierung kann erforderlich sein, wie im Fall von Twitter .

Sie sollten dies lesen. Das absolute Minimum, das jeder Softwareentwickler unbedingt
über Unicode und Zeichensätze wissen muss (keine Ausreden!)

phuclv
quelle
6

Dies liegt daran, dass length()nur Unicode-Codepunkte verwendet werden, die nicht größer als sind U+FFFF. Dieser Satz von Codepunkten wird als BMP ( Basic Multilingual Plane ) bezeichnet und verwendet nur 2 Byte.

Unicode-Codepunkte außerhalb von BMPwerden in UTF-16 mit 4-Byte-Ersatzpaaren dargestellt.

Verwenden Sie, um die Anzahl der Zeichen (3) korrekt zu zählen StringInfo

StringInfo b = new StringInfo("A𠈓C");
Console.WriteLine(string.Format("Length 2 = {0}", b.LengthInTextElements));
Pier-Alexandre Bouchard
quelle
6

Okay, in .Net und C # sind alle Zeichenfolgen als UTF-16LE codiert . A stringwird als Folge von Zeichen gespeichert. Jedes charkapselt die Speicherung von 2 Bytes oder 16 Bits.

Was wir "auf Papier oder Bildschirm" als einen einzelnen Buchstaben, ein Zeichen, eine Glyphe, ein Symbol oder ein Interpunktionszeichen sehen, kann als ein einzelnes Textelement betrachtet werden. Wie in Unicode Standard Annex # 29 UNICODE TEXT SEGMENTATION beschrieben , wird jedes Textelement durch einen oder mehrere Codepunkte dargestellt. Eine vollständige Liste der Codes finden Sie hier .

Jeder Codepunkt muss für die interne Darstellung durch einen Computer binär codiert werden. Wie angegeben, charspeichert jeder 2 Bytes. Codepunkte an oder unter U+FFFFkönnen in einem einzigen gespeichert werden char. Die obigen U+FFFFCodepunkte werden als Ersatzpaar gespeichert, wobei zwei Zeichen verwendet werden, um einen einzelnen Codepunkt darzustellen.

Wenn wir wissen, was wir jetzt ableiten können, kann ein Textelement als eines char, als Ersatzpaar aus zwei Zeichen oder, wenn das Textelement durch mehrere Codepunkte dargestellt wird, als Kombination aus einzelnen Zeichen und Ersatzpaaren gespeichert werden. Als ob das nicht kompliziert genug wäre, können einige Textelemente durch verschiedene Kombinationen von Codepunkten dargestellt werden, wie in Unicode Standard Annex # 15, UNICODE NORMALIZATION FORMS beschrieben .


Zwischenspiel

Zeichenfolgen, die beim Rendern gleich aussehen, können also aus einer anderen Kombination von Zeichen bestehen. Ein ordinaler (Byte für Byte) Vergleich zweier solcher Zeichenfolgen würde einen Unterschied feststellen. Dies kann unerwartet oder unerwünscht sein.

Sie können .NET-Zeichenfolgen neu codieren. so dass sie das gleiche Normalisierungsformular verwenden. Nach der Normalisierung werden zwei Zeichenfolgen mit denselben Textelementen auf dieselbe Weise codiert. Verwenden Sie dazu die Funktion string.Normalize . Denken Sie jedoch daran, dass einige verschiedene Textelemente einander ähnlich sehen. : -s


Was bedeutet das alles in Bezug auf die Frage? Das '𠈓'Textelement wird durch die einzelne Code Point U + 20213 cjk Unified Ideographs-Erweiterung b dargestellt . Dies bedeutet, dass es nicht als einzelnes charZeichen codiert werden kann und als Ersatzpaar mit zwei Zeichen codiert werden muss. Deshalb string bist man charlänger so string a.

Wenn Sie die Anzahl der Textelemente in a zuverlässig zählen müssen (siehe Einschränkung) string, sollten Sie die System.Globalization.StringInfoKlasse wie folgt verwenden.

using System.Globalization;

string a = "abc";
string b = "A𠈓C";

Console.WriteLine("Length a = {0}", new StringInfo(a).LengthInTextElements);
Console.WriteLine("Length b = {0}", new StringInfo(b).LengthInTextElements);

die Ausgabe geben,

"Length a = 3"
"Length b = 3"

wie erwartet.


Vorbehalt

Die .NET-Implementierung der Unicode-Textsegmentierung in den Klassen StringInfound TextElementEnumeratorsollte im Allgemeinen nützlich sein und in den meisten Fällen eine Antwort liefern, die der Aufrufer erwartet. Wie in Unicode Standard Annex # 29 angegeben, "kann das Ziel der Übereinstimmung der Benutzerwahrnehmungen nicht immer genau erreicht werden, da der Text allein nicht immer genügend Informationen enthält, um Grenzen eindeutig zu bestimmen."

Jodrell
quelle
Ich denke, Ihre Antwort ist möglicherweise verwirrend. In diesem Fall ist 𠈓 nur ein einzelner Codepunkt, aber da sein Codepunkt 0xFFFF überschreitet, muss er unter Verwendung eines Ersatzpaars als 2 Codeeinheiten dargestellt werden. Grapheme ist ein weiteres Konzept, das auf einem Codepunkt aufbaut, wobei ein Graphem durch einen einzelnen Codepunkt oder mehrere Codepunkte dargestellt werden kann, wie dies in Hangul in Korea oder in vielen lateinischen Sprachen der Fall ist.
nhahtdh
@nhahtdh, ich stimme zu, meine Antwort war falsch. Ich habe es umgeschrieben und hoffe, dass es jetzt mehr Klarheit schafft.
Jodrell