Почему длина этой строки превышает количество символов в ней?


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
Как я узнал, что это была проблема суррогатной пары, просто глядя на название. Ах, хорошая система. Глобализация - ваш союзник!
Крис Сирфице

9
в UTF-16 длина 4 байта, а не 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
Я рекомендую прочитать отличную статью «Абсолютный минимум для каждого разработчика программного обеспечения. Абсолютно, положительно должен знать о Unicode и наборах символов (без извинений!)» Joelonsoftware.com/articles/Unicode.html
ItsMe

Ответы:


232

Все остальные дают поверхностный ответ, но есть и более глубокое обоснование: количество «символов» является трудным для определения вопросом и может быть удивительно дорогим для вычисления, тогда как свойство длины должно быть быстрым.

Почему это трудно определить? Ну, есть несколько вариантов, и ни один из них не является более действительным, чем другой:

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

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

  • Даже количество графем не совпадает с длиной напечатанной строки, которая зависит от шрифта среди других факторов, и поскольку некоторые символы печатаются с некоторым перекрытием во многих шрифтах (кернинг), длина строки на экране в любом случае не обязательно равна сумме длины графем!

  • Некоторые точки Unicode - это даже не символы в традиционном смысле, а какой-то контрольный маркер. Как маркер порядка байтов или индикатор справа налево. Это считается?

Короче говоря, длина строки на самом деле является смехотворно сложным вопросом, и для ее вычисления может потребоваться много процессорного времени, а также таблицы данных.

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

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

Вот почему bесть длина 4за пределами поверхностного объяснения «потому что в документации так сказано».


9
По сути, «.Length» - это не то, что думает большинство программистов. Возможно, должен быть набор более специфических свойств (например, GlyphCount) и Length, помеченных как устаревшие!
Redcalx

8
@locster Я согласен, но не думаю, что он Lengthдолжен быть устаревшим, чтобы поддерживать аналогию с массивами.
Кролтан

2
@locster Это не должно быть устаревшим. Python один имеет большой смысл, и никто не сомневается в этом.
simonzack

1
Я думаю. Длина имеет большой смысл и является естественным свойством, если вы понимаете, что это такое и почему это так. Затем он работает как любой другой массив (в некоторых языках, таких как D, строка в буквальном смысле является массивом, если говорить о языке, и работает очень хорошо)
Адам Д. Руппе

4
Это не так (распространенное заблуждение) - с UTF-32, lengthInBytes / 4 даст количество кодовых точек , но это не то же самое, что число «символов» или графем. Рассмотрим LATIN SMALL LETTER E, за которым следует КОМБИНИРУЮЩИЙ ДИАРЕЗ ..., который печатается как один символ, его можно даже нормализовать до одной кодовой точки, но он по-прежнему длиной в две единицы, даже в UTF-32.
Адам Д. Руппе

62

Из документации о String.Lengthнедвижимости:

Свойство Length возвращает количество объектов Char в этом экземпляре, а не количество символов Unicode. Причина в том, что символ Unicode может быть представлен более чем одним символом . Используйте класс System.Globalization.StringInfo для работы с каждым символом Unicode вместо каждого символа Char .


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

32

Ваш персонаж с индексом 1 в "A𠈓C"является SurrogatePair

Следует помнить, что суррогатные пары представляют 32-битные одиночные символы.

Вы можете попробовать этот код, и он вернется True

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

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

trueесли параметр s включает соседние символы в позициях index и index + 1 , а числовое значение символа в позиции index варьируется от U + D800 до U + DBFF, а числовое значение символа в позиции index + 1 варьируется от U + DC00 через U + DFFF; в противном случае false.

Это дополнительно объясняется в свойстве String.Length :

Свойство Length возвращает количество объектов Char в этом экземпляре, а не количество символов Unicode. Причина в том, что символ Unicode может быть представлен более чем одним символом. Используйте класс System.Globalization.StringInfo для работы с каждым символом Unicode вместо каждого символа Char.


24

Как указывали другие ответы, даже если есть 3 видимых символа, они представлены 4 charобъектами. Вот почему Length4, а не 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 представлен несколькими объектами char (SurrogatePair).

Свойство Length возвращает количество объектов Char в этом экземпляре, а не количество символов Unicode. Причина в том, что символ Unicode может быть представлен более чем одним символом. Используйте класс System.Globalization.StringInfo для работы с каждым символом Unicode вместо каждого символа Char.


1
В этом ответе вы неоднозначно используете «характер». Я предлагаю заменить хотя бы первый с точной терминологией.
Гонки легкости на орбите

1
Спасибо. Исправлена ​​неоднозначность.
Ювал Ицчаков

10

Как говорили другие, это не количество символов в строке, а количество объектов Char. Символ 𠈓 является кодовой точкой U + 20213. Поскольку значение находится вне диапазона 16-битного типа символа, оно кодируется в UTF-16 как суррогатная пара D840 DE13.

Способ получения длины в символах был упомянут в других ответах. Однако это следует использовать с осторожностью, так как может быть много способов представления символа в Юникоде. «а» может быть 1 составным символом или 2 символами (диакритические знаки +). Нормализация может быть необходима, как в случае с твиттером .

Вы должны прочитать это
Абсолютный минимум Каждый разработчик программного обеспечения Абсолютно, положительно должен знать о Unicode и наборах символов (никаких оправданий!)


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 . A stringхранится как последовательность символов. Каждый charинкапсулирует хранилище в 2 байта или 16 бит.

То, что мы видим «на бумаге или экране» как одну букву, символ, глиф, символ или знак пунктуации, можно рассматривать как отдельный элемент текста. Как описано в Стандартном приложении Unicode № 29 СЕГМЕНТАЦИЯ ТЕКСТА ЮНИКОДА , каждый текстовый элемент представлен одной или несколькими кодовыми точками. Исчерпывающий список кодов можно найти здесь .

Каждую кодовую точку необходимо закодировать в двоичный файл для внутреннего представления компьютером. Как указано, каждый charхранит 2 байта. Кодовые точки на или ниже U+FFFFмогут быть сохранены в одном char. Вышеуказанные кодовые точки U+FFFFхранятся в виде суррогатной пары с использованием двух символов для представления единой кодовой точки.

Учитывая то, что мы теперь знаем, что мы можем сделать вывод, текстовый элемент может быть сохранен как один char, как суррогатная пара из двух символов или, если текстовый элемент представлен несколькими кодовыми точками, как некоторая комбинация отдельных символов и суррогатных пар. Как будто это не было достаточно сложно, некоторые текстовые элементы могут быть представлены различными комбинациями кодовых точек, как описано в Стандартном приложении № 15 к Unicode, ФОРМЫ НОРМАЛИЗАЦИИ ЮНИКОДА .


интерлюдия

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

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


Итак, что все это значит в отношении вопроса? Текстовый элемент '𠈓'представлен единым расширением унифицированных идеограмм кодовой точки 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"

как и ожидалось.


Предостережение

Реализация .Net в Unicode Text Сегментации в StringInfoи TextElementEnumeratorклассах должна быть в целом полезной и, в большинстве случаев, даст ответ , что предпологает абонент. Однако, как указано в Приложении № 29 к стандарту Unicode, «цель сопоставления восприятия пользователя не всегда может быть достигнута именно потому, что один только текст не всегда содержит достаточно информации, чтобы однозначно определить границы».


Я думаю, что ваш ответ потенциально сбивает с толку. В этом случае 𠈓 является только одной кодовой точкой, но поскольку ее кодовая точка превышает 0xFFFF, она должна быть представлена ​​в виде 2 кодовых единиц с использованием суррогатной пары. Графема - это другая концепция, построенная на вершине кодовой точки, где графема может быть представлена ​​одной кодовой точкой или несколькими кодовыми точками, как это видно на корейском языке хангыль или на многих латинских языках.
nhahtdh

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