Dlaczego długość tego ciągu jest większa niż liczba zawartych w nim znaków?

145

Ten kod:

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

wyjścia:

Length a = 3
Length b = 4

Czemu? Jedyne, co mogłem sobie wyobrazić, to to, że chiński znak ma 2 bajty i że .Lengthmetoda zwraca liczbę bajtów.

weini37
źródło
10
Skąd wiedziałem, że to problem z parą zastępczą, patrząc tylko na tytuł. Ach, dobry stary System. Globalizacja jest Twoim sprzymierzeńcem!
Chris Cirefice
9
ma 4 bajty długości w UTF-16, a nie 2
phuclv
wartość dziesiętna znaku 𠈓to 131603, a ponieważ znaki są bajtami bez znaku, oznacza to, że można uzyskać tę wartość w 2 znakach zamiast 4 (maksymalna wartość 16-bitowa bez znaku to 65535 (lub 65536 wariacji) i użycie 2 znaków do reprezentacji pozwala dla maksymalnej liczby zmian wynoszącej nie 65536 * 2 (131072), ale raczej 65536 * 65536 wariacji (4294967296, w rzeczywistości wartość 32-bitowa)
GMasucci
3
@GMAsucci: To 2 znaki w UTF-16, ale 4 bajty, ponieważ znak UTF16 ma rozmiar 2 bajtów, w przeciwnym razie nie mógłby przechowywać 65536 odmian, ale tylko 256.
Kaiserludi
4
Polecam przeczytanie świetnego artykułu „Absolutne minimum każdy programista absolutnie, pozytywnie musi wiedzieć o Unicode i zestawach
ItsMe

Odpowiedzi:

232

Wszyscy inni dają powierzchowną odpowiedź, ale jest też głębsze uzasadnienie: liczba „znaków” jest trudnym do zdefiniowania pytaniem i może być zaskakująco kosztowna do obliczenia, podczas gdy właściwość length powinna być szybka.

Dlaczego trudno to zdefiniować? Cóż, jest kilka opcji i żadna nie jest tak naprawdę ważniejsza niż inna:

  • Liczba jednostek kodu (bajtów lub innych fragmentów danych o stałym rozmiarze; C # i Windows zwykle używają UTF-16, więc zwraca liczbę elementów dwubajtowych) jest z pewnością istotna, ponieważ komputer nadal musi radzić sobie z danymi w tej formie do wielu celów (np. zapis do pliku dba o bajty, a nie o znaki)

  • Liczba punktów kodowych Unicode jest dość łatwa do obliczenia (chociaż O (n), ponieważ musisz zeskanować ciąg w poszukiwaniu par zastępczych) i może mieć znaczenie dla edytora tekstu ... ale w rzeczywistości nie jest tym samym, co liczba znaków wydrukowane na ekranie (zwane grafemami). Na przykład, niektóre litery akcentowane mogą być reprezentowane w dwóch formach: jeden kod lub dwa punkty połączone razem, jeden reprezentujący literę i jeden mówiący „dodaj akcent do mojej litery partnera”. Czy para byłaby dwiema postaciami czy jedną? Aby w tym pomóc, możesz znormalizować ciągi znaków, ale nie wszystkie prawidłowe litery mają jedną reprezentację punktu kodowego.

  • Nawet liczba grafemów nie jest taka sama jak długość drukowanego ciągu, która zależy między innymi od czcionki, a ponieważ niektóre znaki są drukowane z pewnym nakładaniem się w wielu czcionkach (kerning), długość ciągu na ekranie i tak niekoniecznie jest równa sumie długości grafemów!

  • Niektóre punkty Unicode nie są nawet znakami w tradycyjnym sensie, ale raczej rodzajem znacznika kontrolnego. Podobnie jak znacznik kolejności bajtów lub wskaźnik od prawej do lewej. Czy to się liczy?

Krótko mówiąc, długość łańcucha jest w rzeczywistości absurdalnie złożonym pytaniem, a obliczenie jej może zająć dużo czasu procesora, a także tabele danych.

Co więcej, o co chodzi? Dlaczego te dane mają znaczenie? Cóż, tylko ty możesz odpowiedzieć na to pytanie w swoim przypadku, ale osobiście uważam, że są one generalnie nieistotne. Uważam, że ograniczanie wprowadzania danych jest bardziej logiczne dzięki ograniczeniom bajtów, ponieważ i tak trzeba je przesłać lub przechowywać. Ograniczanie rozmiaru wyświetlacza jest lepiej wykonywane przez oprogramowanie po stronie wyświetlacza - jeśli masz 100 pikseli dla wiadomości, to ile znaków zmieścisz zależy od czcionki itp., Które i tak nie są znane oprogramowaniu warstwy danych. Wreszcie, biorąc pod uwagę złożoność standardu Unicode, prawdopodobnie i tak będziesz mieć błędy w skrajnych przypadkach, jeśli spróbujesz czegoś innego.

Jest to więc trudne pytanie, przy niewielkim zastosowaniu ogólnym. Liczba jednostek kodu jest łatwa do obliczenia - jest to tylko długość podstawowej tablicy danych - i co do zasady jest najbardziej znacząca / użyteczna, z prostą definicją.

Dlatego bma długość 4wykraczającą poza powierzchowne wyjaśnienie „ponieważ tak mówi dokumentacja”.

Adam D. Ruppe
źródło
9
Zasadniczo „.Length” nie jest tym, co myśli większość programistów. Może powinien istnieć zestaw bardziej szczegółowych właściwości (np. GlyphCount) i Length oznaczone jako Obsolete!
redcalx
8
@locster Zgadzam się, ale nie uważam, że Lengthpowinno być przestarzałe, aby zachować analogię z tablicami.
Kroltan
2
@locster Nie powinno być przestarzałe. Python ma dużo sensu i nikt go nie kwestionuje.
simonzack
1
Myślę, że długość ma dużo sensu i jest naturalną właściwością, o ile rozumiesz, co to jest i dlaczego tak jest. Wtedy działa jak każda inna tablica (w niektórych językach, takich jak D, łańcuch dosłownie jest tablicą, jeśli chodzi o język i działa naprawdę dobrze)
Adam D. Ruppe
4
To nieprawda (powszechne nieporozumienie) - w przypadku UTF-32 parametr lengthInBytes / 4 dawałby liczbę punktów kodowych , ale nie jest to to samo, co liczba „znaków” lub grafemów. Rozważmy ŁACIŃSKĄ MAŁĄ LITERĘ E, po której następuje ŁĄCZONA DIAEREZA ... która jest drukowana jako pojedynczy znak, może być nawet znormalizowana do pojedynczego punktu kodowego, ale wciąż ma dwie jednostki długości, nawet w UTF-32.
Adam D. Ruppe,
62

Z dokumentacji o String.Lengthnieruchomości:

Właściwość Length zwraca liczbę obiektów Char w tym wystąpieniu, a nie liczbę znaków Unicode. Powodem jest to, że znak Unicode może być reprezentowany przez więcej niż jeden znak Char . Użyj klasy System.Globalization.StringInfo, aby pracować z każdym znakiem Unicode zamiast z każdym Char .

niania
źródło
3
Java zachowuje się w ten sam sposób (również wypisuje 4 dla String b), ponieważ używa reprezentacji UTF-16 w tablicach char. Jest to 4-bajtowy znak w UTF-8.
Michael
32

Twoja postać w indeksie 1 w "A𠈓C"to SurrogatePair

Kluczową kwestią do zapamiętania jest to, że pary zastępcze reprezentują 32-bitowe pojedyncze znaki.

Możesz wypróbować ten kod, a on zwróci True

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

Char.IsSurrogatePair, metoda (String, Int32)

truejeśli parametr s zawiera sąsiednie znaki w pozycjach indeks i indeks + 1 , a numeryczna wartość znaku na pozycji zawiera się w przedziale od U + D800 do U + DBFF, a numeryczna wartość znaku o indeksie pozycji + 1 waha się od U + DC00 do U + DFFF; w przeciwnym razie false.

Jest to dokładniej wyjaśnione we właściwości String.Length :

Właściwość Length zwraca liczbę obiektów Char w tym wystąpieniu, a nie liczbę znaków Unicode. Powodem jest to, że znak Unicode może być reprezentowany przez więcej niż jeden znak Char. Użyj klasy System.Globalization.StringInfo, aby pracować z każdym znakiem Unicode zamiast z każdym Char.

Habib
źródło
24

Jak wskazywały inne odpowiedzi, nawet jeśli widoczne są 3 znaki, są one przedstawiane za pomocą 4 charobiektów. Dlatego właśnieLength jest 4, a nie 3.

MSDN stwierdza, że

Właściwość Length zwraca liczbę obiektów Char w tym wystąpieniu, a nie liczbę znaków Unicode.

Jednakże, jeśli naprawdę chcesz wiedzieć, ile "elementów tekstowych", a nie liczba Charobiektów, możesz użyć tej StringInfoklasy.

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

Możesz także wyliczyć każdy element tekstowy w ten sposób

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

Użycie foreachna łańcuchu podzieli środkową „literę” na dwa charobiekty, a wydrukowany wynik nie będzie odpowiadał napisowi.

dee-see
źródło
20

Dzieje się tak, ponieważ Lengthwłaściwość zwraca liczbę obiektów char , a nie liczbę znaków Unicode. W twoim przypadku jeden ze znaków Unicode jest reprezentowany przez więcej niż jeden obiekt char (SurrogatePair).

Właściwość Length zwraca liczbę obiektów Char w tym wystąpieniu, a nie liczbę znaków Unicode. Powodem jest to, że znak Unicode może być reprezentowany przez więcej niż jeden znak Char. Użyj klasy System.Globalization.StringInfo, aby pracować z każdym znakiem Unicode zamiast z każdym Char.

Yuval Itzchakov
źródło
1
W tej odpowiedzi masz niejednoznaczne użycie słowa „znak”. Proponuję przynajmniej tę pierwszą zastąpić precyzyjną terminologią.
Wyścigi lekkości na orbicie
1
Dziękuję Ci. Naprawiono niejednoznaczność.
Yuval Itzchakov
10

Jak powiedzieli inni, nie jest to liczba znaków w ciągu, ale liczba obiektów Char. Znak 𠈓 to punkt kodowy U + 20213. Ponieważ wartość jest poza zakresem 16-bitowych znaków, jest zakodowana w UTF-16 jako para zastępcza D840 DE13.

Sposób uzyskania długości znaków został wymieniony w innych odpowiedziach. Jednak należy go używać ostrożnie, ponieważ może istnieć wiele sposobów reprezentowania znaku w Unicode. „à” może składać się z 1 złożonego znaku lub 2 znaków (a + znaki diakrytyczne). Może być potrzebna normalizacja, tak jak w przypadku Twittera .

Powinieneś przeczytać to
Absolutne minimum Każdy programista Absolutnie, pozytywnie musi wiedzieć o Unicode i zestawach znaków (bez wymówek!)

phuclv
źródło
6

Dzieje się tak, ponieważ length()działa tylko dla punktów kodowych Unicode, które nie są większe niż U+FFFF. Ten zestaw punktów kodowych jest znany jako podstawowa płaszczyzna wielojęzyczna (BMP) i wykorzystuje tylko 2 bajty.

Punkty kodu Unicode poza BMPznakami są reprezentowane w UTF-16 przy użyciu 4-bajtowych par zastępczych.

Aby poprawnie policzyć liczbę znaków (3), użyj StringInfo

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

Okay, w .Net i C # wszystkie ciągi są kodowane jako UTF-16LE . A stringjest przechowywany jako sekwencja znaków. Każdy charhermetyzuje pamięć 2 bajtów lub 16 bitów.

To, co widzimy „na papierze lub ekranie” jako pojedyncza litera, znak, glif, symbol lub znak interpunkcyjny, można traktować jako pojedynczy element tekstowy. Jak opisano w załączniku nr 29 do standardu Unicode, SEGMENTACJA TEKSTU UNICODE , każdy element tekstowy jest reprezentowany przez jeden lub więcej punktów kodowych. Pełną listę kodów można znaleźć tutaj .

Każdy punkt kodowy musi zostać zakodowany w postaci binarnej w celu wewnętrznej reprezentacji przez komputer. Jak wspomniano, każdy charprzechowuje 2 bajty. Punkty kodowe na lub poniżej U+FFFFmogą być przechowywane w jednym char. Powyższe punkty kodowe U+FFFFsą przechowywane jako para zastępcza, przy użyciu dwóch znaków reprezentujących pojedynczy punkt kodowy.

Biorąc pod uwagę to, co teraz wiemy, że możemy wydedukować, element tekstowy może być przechowywany jako jeden char, jako para zastępcza dwóch znaków lub, jeśli element tekstowy jest reprezentowany przez wiele punktów kodowych, pewna kombinacja pojedynczych znaków i par zastępczych. Jakby to nie było wystarczająco skomplikowane, niektóre elementy tekstowe mogą być reprezentowane przez różne kombinacje punktów kodowych, jak opisano w Załączniku nr 15 do normy Unicode, FORMY NORMALIZACJI UNICODE .


Interludium

Tak więc ciągi, które wyglądają tak samo po wyrenderowaniu, mogą w rzeczywistości składać się z innej kombinacji znaków. Porządkowe (bajt po bajcie) porównanie dwóch takich ciągów wykryłoby różnicę, może to być nieoczekiwane lub niepożądane.

Możesz ponownie zakodować ciągi .Net. aby używali tego samego formularza normalizacji. Po znormalizowaniu dwa ciągi z tymi samymi elementami tekstowymi zostaną zakodowane w ten sam sposób. Aby to zrobić, użyj funkcji string.Normalize . Pamiętaj jednak, że niektóre różne elementy tekstowe wyglądają podobnie do siebie. : -s


Więc co to wszystko oznacza w odniesieniu do pytania? Element tekstowy '𠈓'jest reprezentowany przez pojedyncze rozszerzenie ujednoliconych ideogramów U + 20213 cjk b . Oznacza to, że nie może być zakodowany jako pojedynczy chari musi być zakodowany jako para zastępcza, przy użyciu dwóch znaków. Dlatego string bjest o jeden chardłużej string a.

Jeśli potrzebujesz rzetelnie (patrz zastrzeżenie) policzyć liczbę elementów tekstowych w a string, powinieneś użyć takiej System.Globalization.StringInfoklasy.

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

dając wyjście,

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

zgodnie z oczekiwaniami.


Caveat

Implementacja .Net segmentacji tekstu Unicode w klasach StringInfoi TextElementEnumeratorpowinna być ogólnie użyteczna i, w większości przypadków, przyniesie odpowiedź, której oczekuje wywołanie. Jednak, jak stwierdzono w Załączniku nr 29 standardu Unicode, „Cel dopasowania percepcji użytkownika nie zawsze może zostać dokładnie osiągnięty, ponieważ sam tekst nie zawsze zawiera wystarczającą ilość informacji, aby jednoznacznie określić granice”.

Jodrell
źródło
Myślę, że twoja odpowiedź jest potencjalnie myląca. W tym przypadku 𠈓 jest tylko pojedynczym punktem kodowym, ale ponieważ jego punkt kodowy przekracza wartość 0xFFFF, musi być reprezentowany jako 2 jednostki kodu przy użyciu pary zastępczej. Grafem to kolejna koncepcja zbudowana na podstawie punktu kodowego, w której grafem może być reprezentowany przez pojedynczy punkt kodowy lub wiele punktów kodowych, jak widać w koreańskim Hangul lub wielu językach opartych na łacinie.
nhahtdh
@nhahtdh, zgadzam się, moja odpowiedź była błędna. Przepisałem to i mam nadzieję, że teraz zapewnia większą przejrzystość.
Jodrell