Чому довжина цього рядка більше, ніж кількість символів у ньому?


145

Цей код:

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

Виходи:

Length a = 3
Length b = 4

Чому? Єдине, що я міг собі уявити, - це те, що китайський символ має 2 байти і .Lengthметод повертає кількість байтів.


10
Як я дізнався, що це проблема сурогатної пари лише з погляду на заголовок. Ах, добра система. Глобалізація - ваш союзник!
Chris Cirefice

9
це 4 байти в UTF-16, а не 2
phuclv

десяткове значення знаку символу 𠈓- 131603, і ​​оскільки символи є непідписаними байтами, це означає, що ви можете досягти цього значення в 2 символах, а не 4 (непідписане 16-бітове значення максимуму - 65535 (або 65536 варіацій), а використовувати 2 символи для його представлення дозволяє для максимальної кількості варіацій не 65536 * 2 (131072), а 65536 * 65536 варіацій (4,294,967,296, фактично значення 32 біт)
GMasucci

3
@GMAsucci: це 2 символи в UTF-16, але 4 байти, оскільки символ UTF16 має 2 байти, інакше він не може зберігати 65536 варіацій, а лише 256.
Kaiserludi

4
Рекомендую прочитати чудову статтю «Абсолютний мінімум для кожного розробника програмного забезпечення абсолютно, позитивно повинен знати про набори юнікодів та символів (без виправдань!)» Joelonsoftware.com/articles/Unicode.html
ItsMe

Відповіді:


232

Усі інші дають поверхневу відповідь, але є і більш глибоке обгрунтування: кількість "символів" є складним для визначення питання і його можна напрочуд дорого обчислити, тоді як властивість довжини має бути швидкою.

Чому це важко визначити? Ну, є кілька варіантів, і жоден не є дійсно більш достовірним, ніж інший:

  • Кількість одиниць коду (байти чи інший фрагмент даних фіксованого розміру; C # та Windows зазвичай використовують UTF-16, тому він повертає кількість двобайтових фрагментів), безумовно, є актуальним, оскільки комп'ютер все ще повинен обробляти дані в цій формі для багатьох цілей (наприклад, запис у файл переймається байтами, а не символами)

  • Кількість кодових точок Unicode досить легко обчислити (хоча O (n), оскільки ви повинні сканувати рядок для сурогатних пар) і може мати значення для текстового редактора ...., але насправді це не те саме, що кількість символів друкується на екрані (називається графемами). Наприклад, деякі літери з наголосом можуть бути представлені у двох формах: одна кодова точка або дві точки, сполучені разом, одна представляє букву, а одна приказка "додайте акцент моєму листу партнера". Пара буде двома символами чи одним? Ви можете нормалізувати рядки, щоб допомогти з цим, але не всі дійсні букви мають одне представлення кодової точки.

  • Навіть кількість графем не є такою ж, як довжина друкованого рядка, що залежить від шрифту серед інших факторів, і оскільки деякі символи друкуються з деяким перекриттям у багатьох шрифтах (кернінг), довжина рядка на екрані не обов'язково дорівнює сумі довжини графем у будь-якому випадку!

  • Деякі точки Unicode - це навіть не символи в традиційному розумінні, а скоріше якийсь маркер управління. Як маркер порядку байтів або індикатор справа наліво. Чи рахуються ці?

Коротше кажучи, довжина рядка - це насправді смішно складне питання, і його обчислення може зайняти багато часу процесора, а також таблиць даних.

Більше того, який сенс? Чому ці показники мають значення? Ну, тільки ви можете відповісти на це у вашій справі, але особисто я вважаю, що вони взагалі не мають значення. Обмеження введення даних, які я вважаю, більш логічно робиться за допомогою байтових обмежень, оскільки це все одно потрібно перенести або зберегти. Обмеження розміру дисплея краще здійснювати за допомогою програмного забезпечення на стороні дисплея - якщо у вас 100 пікселів для повідомлення, кількість символів, які ви підходите, залежить від шрифту тощо. Нарешті, враховуючи складність стандарту Unicode, ви, мабуть, матимете помилки у крайових випадках, якщо ви спробуєте щось інше.

Тож це складне питання з не великим загальним призначенням. Кількість одиниць коду обчислюється тривіально - це лише довжина базового масиву даних - і найбільш змістовна / корисна, як загальне правило, з простим визначенням.

Ось чому bмає довжину, що 4перевищує поверхневе пояснення "тому що документація так говорить".


9
По суті ".Length" - це не те, що вважає це більшість кодерів. Можливо, має бути набір більш конкретних властивостей (наприклад, GlyphCount) та довжина, позначені як застарілі!
redcalx

8
@locster Я згоден, але не думаю, що Lengthслід підтримувати аналогію з масивами.
Кролтан

2
@locster Це не повинно бути застарілим. Пітон має багато сенсу, і ніхто цього не ставить під сумнів.
simonzack

1
Я думаю, що. Length має багато сенсу і є природною властивістю, якщо ви розумієте, що це таке і чому саме так. Тоді він працює як і будь-який інший масив (у деяких мовах, як D, рядок буквально є масивом, що стосується мови, і він працює дуже добре)
Адам Д. Руппе,

4
Це неправда (поширена помилка) - з UTF-32, lengthInBytes / 4 дасть кількість кодових точок , але це не те, що кількість "символів" або графем. Розглянемо ЛАТИННИЙ МАЛИЙ ПІСЛЕННЯ Е з подальшим поєднанням ДІАЕРЕЗУ ..., який друкується як один символ, він навіть може бути нормалізований до однієї кодової точки, але це ще два одиниці, навіть у UTF-32.
Адам Д. Руппе

62

З документації на String.Lengthмайно:

Властивість Length повертає в цьому випадку кількість об'єктів Char , а не кількість символів Unicode. Причина полягає в тому, що символ Unicode може бути представлений більш ніж один Чар . Використовуйте клас System.Globalization.StringInfo для роботи з кожним символом Unicode замість кожного Char .


3
Java поводиться так само (також друкуючи 4 для String b), оскільки використовує представлення UTF-16 у масивах char. Це 4-байтний символ у UTF-8.
Майкл

32

Ваш персонаж в індексі 1 у "A𠈓C"- це SurrogatePair

Ключовим моментом, який слід пам’ятати, є те, що сурогатні пари представляють 32-бітні одиночні символи.

Ви можете спробувати цей код, і він повернеться True

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

Метод Char.IsSurrogatePair (String, Int32)

trueякщо параметр s включає суміжні символи в індексі позицій та індексі + 1 , а числове значення символу в індексі позиції коливається від U + D800 до U + DBFF, а числове значення символу в індексі позиції + 1 становить від U + DC00 через U + DFFF; в іншому випадку false.

Це далі пояснено у властивості String.Length :

Властивість Length повертає в цьому випадку кількість об'єктів Char, а не кількість символів Unicode. Причина полягає в тому, що символ Unicode може бути представлений декількома знаками. Використовуйте клас System.Globalization.StringInfo для роботи з кожним символом Unicode замість кожного Char.


24

Як вказували інші відповіді, навіть якщо є 3 видимих ​​символи, вони представлені 4 charпредметами. Ось чому Lengthце 4, а не 3.

MSDN стверджує, що

Властивість Length повертає в цьому випадку кількість об'єктів Char, а не кількість символів Unicode.

Однак якщо ви дійсно хочете знати, це кількість "текстових елементів", а не кількість Charоб'єктів, якими ви можете скористатись StringInfoкласом.

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

Ви також можете перерахувати кожен текстовий елемент, як цей

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

Використання foreachрядка розділить середню "літеру" на два charоб'єкти, і друкований результат не відповідатиме рядку.


20

Це тому, що Lengthвластивість повертає кількість об'єктів char , а не кількість символів unicode. У вашому випадку один з символів Unicode представлений більш ніж одним об'єктом char (SurrogatePair).

Властивість Length повертає в цьому випадку кількість об'єктів Char, а не кількість символів Unicode. Причина полягає в тому, що символ Unicode може бути представлений декількома знаками. Використовуйте клас System.Globalization.StringInfo для роботи з кожним символом Unicode замість кожного Char.


1
Ви маєте неоднозначне використання "символу" у цій відповіді. Я пропоную замінити хоча б перший на точну термінологію.
Гонки легкості по орбіті

1
Дякую. Виправлена ​​неоднозначність.
Юваль Ітчаков

10

Як говорили інші, це не кількість символів у рядку, а кількість об'єктів Char. Символ 𠈓 - кодова точка U + 20213. Оскільки значення знаходиться поза діапазоном 16-бітного типу char, воно кодується в UTF-16 як сурогатна параD840 DE13 .

Спосіб отримання довжини в символах був згаданий в інших відповідях. Однак слід користуватися обережно, оскільки у Unicode може бути багато способів представити персонаж. "à" може мати 1 складений символ або 2 символи (a + діакритики). Нормалізація може знадобитися, як у випадку з твіттером .

Ви повинні прочитати цей
абсолютний мінімум, кожен розробник програмного забезпечення повинен абсолютно позитивно знати про набори юнікодів та символів (без виправдань!)


6

Це тому, що length()працює лише для кодових точок Unicode, які не перевищують U+FFFF. Цей набір кодових точок відомий як Основна багатомовна площина (BMP) і використовує лише 2 байти.

Кодові точки Unicode, що знаходяться поза межами BMP, представлені в UTF-16, використовуючи 4 байт сурогатних пар.

Щоб правильно порахувати кількість символів (3), використовуйте StringInfo

StringInfo b = new StringInfo("A𠈓C");
Console.WriteLine(string.Format("Length 2 = {0}", b.LengthInTextElements));

6

Гаразд, у .Net та C # всі рядки кодуються як UTF-16LE . А stringзберігається як послідовність символів. Кожна charкапсула зберігає 2 байти або 16 біт.

Те, що ми бачимо «на папері чи екрані» як окрему букву, символ, гліф, символ або розділовий знак, можна розглядати як єдиний текстовий елемент. Як описано в стандартному додатку Unicode № 29 СЕГМЕНТАЦІЯ ТЕКСТУ UNICODE , кожен текстовий елемент представлений однією або кількома кодовими точками. Вичерпний список кодів можна знайти тут .

Кожну кодову точку потрібно закодувати у двійковий код для внутрішнього представлення комп'ютером. Як зазначено, кожен charзберігає 2 байти. Кодові точки в і нижче U+FFFFможна зберігати в одній char. Кодові точки вище U+FFFFзберігаються у вигляді сурогатної пари, використовуючи дві символи для представлення однієї кодової точки.

З огляду на те, що ми знаємо, що ми можемо зробити, текстовий елемент може бути збережений як один char, як сурогатний пара з двох знаків або, якщо текстовий елемент представлений кількома кодовими точками, деякою комбінацією одиничних знаків і сурогатних пар. Як би це не було досить складним, деякі текстові елементи можуть бути представлені різними комбінаціями точок коду, як описано в, стандартному додатку Unicode, № 15, НОРМАЛІЗАЦІЙНИЙ ФОРМ UNICODE .


Інтермедія

Таким чином, рядки, які виглядають однаково при рендерингу, насправді можуть складатися з різної комбінації символів. Порядкове (байт у байті) порівняння двох таких рядків виявило б різницю, це може бути несподівано або небажано.

Ви можете перекодувати рядки .Net. щоб вони використовували ту саму Форму нормалізації. Після нормалізації два рядки з однаковими текстовими елементами будуть закодовані однаково. Для цього використовуйте функцію string.Normalize . Однак пам’ятайте, деякі різні текстові елементи схожі між собою. : -s


Отже, що це все означає стосовно питання? Текстовий елемент '𠈓'представлений єдиним розширенням уніфікованих ідеографів cjk U + 20213 cjk b . Це означає, що він не може бути кодований як єдиний charі повинен бути кодований як сурогатний пара, використовуючи дві символи. Ось чому string bодин charдовший string a.

Якщо вам потрібно надійно (див. Застереження) підрахувати кількість текстових елементів у a, stringви повинні використовувати такий System.Globalization.StringInfoклас.

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

даючи вихід,

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

як і очікувалося.


Caveat

Реалізація тексту тексту Unicode у класах StringInfoта TextElementEnumeratorкласах має бути загалом корисною та в більшості випадків дасть відповідь, яку очікує абонент. Однак, як зазначено у стандартному додатку Unicode, доданий №29, "мета співставлення сприйняття користувачів не завжди може бути точно досягнута, оскільки сам по собі текст не завжди містить достатньо інформації для однозначного визначення меж".


Я думаю, що ваша відповідь потенційно бентежить. У цьому випадку only є лише однією кодовою точкою, але оскільки її кодова точка перевищує 0xFFFF, вона повинна бути представлена ​​у вигляді 2-х одиниць коду за допомогою сурогатної пари. Grapheme - це ще одна концепція, побудована на вершині кодової точки, де графема може бути представлена ​​однією кодовою точкою або декількома кодовими точками, як це видно в корейській Hangul або багатьох мовах, що базуються на латинській мові.
nhahtdh

@nhahtdh, я згоден, моя відповідь була помилковою. Я її переписав і, сподіваюся, це тепер створює більшу чіткість.
Джодрелл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.