¿Por qué la longitud de esta cadena es más larga que la cantidad de caracteres que contiene?

145

Este código:

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

salidas:

Length a = 3
Length b = 4

¿Por qué? Lo único que podría imaginar es que el carácter chino tiene 2 bytes de longitud y que el .Lengthmétodo devuelve el recuento de bytes.

weini37
fuente
10
¿Cómo supe que era un problema de pareja sustituta solo por mirar el título? Ah, buen sistema. ¡La globalización es tu aliada!
Chris Cirefice
9
tiene 4 bytes de longitud en UTF-16, no 2
phuclv
el valor decimal del carácter 𠈓es 131603, y como los caracteres son bytes sin signo, eso significa que puede lograr ese valor en 2 caracteres en lugar de 4 (el valor máximo de 16 bits sin signo es 65535 (o 65536 variaciones) y usar 2 caracteres para representarlo permite para un número máximo de variaciones no 65536 * 2 (131072) sino más bien 65536 * 65536 variaciones (4,294,967,296, efectivamente un valor de 32 bits)
GMasucci
3
@GMAsucci: son 2 caracteres en UTF-16, pero 4 bytes, porque un carácter UTF16 tiene un tamaño de 2 bytes, de lo contrario no podría almacenar 65536 variaciones, sino solo 256.
Kaiserludi
44
Recomiendo leer el excelente artículo 'El mínimo absoluto que todo desarrollador de software debe saber absolutamente, positivamente sobre los conjuntos de caracteres y Unicode (¡Sin excusas!)' Joelonsoftware.com/articles/Unicode.html
ItsMe

Respuestas:

232

Todos los demás están dando la respuesta superficial, pero también hay una razón más profunda: el número de "caracteres" es una pregunta difícil de definir y puede ser sorprendentemente costoso de calcular, mientras que una propiedad de longitud debería ser rápida.

¿Por qué es difícil de definir? Bueno, hay algunas opciones y ninguna es realmente más válida que otra:

  • El número de unidades de código (bytes u otro fragmento de datos de tamaño fijo; C # y Windows generalmente usan UTF-16, por lo que devuelve el número de piezas de dos bytes) es ciertamente relevante, ya que la computadora aún necesita manejar los datos de esa forma para muchos propósitos (escribir en un archivo, por ejemplo, se preocupa por los bytes en lugar de los caracteres)

  • El número de puntos de código Unicode es bastante fácil de calcular (aunque O (n) porque debe escanear la cadena en busca de pares sustitutos) y puede ser importante para un editor de texto ... pero en realidad no es lo mismo que el número de caracteres impreso en pantalla (llamados grafemas). Por ejemplo, algunas letras acentuadas se pueden representar de dos formas: un único punto de código o dos puntos emparejados, uno que representa la letra y otro que dice "agregar un acento a mi letra asociada". ¿Sería la pareja dos personajes o uno? Puede normalizar cadenas para ayudar con esto, pero no todas las letras válidas tienen una única representación de punto de código.

  • Incluso la cantidad de grafemas no es la misma que la longitud de una cadena impresa, que depende de la fuente, entre otros factores, y dado que algunos caracteres se imprimen con cierta superposición en muchas fuentes (interletraje), la longitud de una cadena en la pantalla ¡no es necesariamente igual a la suma de la longitud de los grafemas de todos modos!

  • Algunos puntos Unicode ni siquiera son caracteres en el sentido tradicional, sino más bien algún tipo de marcador de control. Como un marcador de orden de bytes o un indicador de derecha a izquierda. ¿Cuentan estos?

En resumen, la longitud de una cadena es en realidad una pregunta ridículamente compleja y calcularla puede llevar mucho tiempo de CPU, así como tablas de datos.

Además, ¿cuál es el punto? ¿Por qué son importantes estas métricas? Bueno, solo usted puede responder eso para su caso, pero personalmente, considero que generalmente son irrelevantes. La limitación de la entrada de datos que encuentro se realiza de manera más lógica por los límites de bytes, ya que eso es lo que debe transferirse o almacenarse de todos modos. La limitación del tamaño de la pantalla se realiza mejor con el software del lado de la pantalla: si tiene 100 píxeles para el mensaje, la cantidad de caracteres que ajuste dependerá de la fuente, etc. Finalmente, dada la complejidad del estándar Unicode, es probable que tenga errores en los casos extremos de todos modos si intenta algo más.

Por lo tanto, es una pregunta difícil con poco uso general. El número de unidades de código es trivial de calcular, es solo la longitud de la matriz de datos subyacente, y el más significativo / útil como regla general, con una definición simple.

Es por eso que btiene una extensión 4más allá de la explicación superficial de "porque la documentación lo dice".

Adam D. Ruppe
fuente
9
Esencialmente, '.Longitud' no es lo que la mayoría de los programadores piensan que es. Tal vez debería haber un conjunto de propiedades más específicas (por ejemplo, GlyphCount) y Longitud marcadas como Obsoleto.
Redcalx
8
@locster Estoy de acuerdo, pero no creo que Lengthdeba ser obsoleto, para mantener la analogía con las matrices.
Kroltan
2
@locster No debería ser obsoleto. El python one tiene mucho sentido y nadie lo cuestiona.
simonzack
1
Creo que .Length tiene mucho sentido y es una propiedad natural, siempre y cuando comprenda qué es y por qué es así. Luego funciona como cualquier otra matriz (en algunos idiomas como D, una cadena literalmente es una matriz en lo que respecta al idioma y funciona realmente bien)
Adam D. Ruppe
44
Eso no es cierto (un error común): con UTF-32, lengthInBytes / 4 daría la cantidad de puntos de código , pero eso no es lo mismo que la cantidad de "caracteres" o grafemas. Considere LETRA PEQUEÑA LATINA E seguida de una DIAERESIS COMBINADA ... que se imprime como un solo carácter, incluso se puede normalizar a un único punto de código, pero aún tiene dos unidades de largo, incluso en UTF-32.
Adam D. Ruppe
62

De la documentación de la String.Lengthpropiedad:

La propiedad Longitud devuelve el número de objetos Char en esta instancia, no el número de caracteres Unicode. La razón es que un personaje Unicode puede estar representado por más de un Char . Use la clase System.Globalization.StringInfo para trabajar con cada personaje Unicode en lugar de cada Char .

niñera
fuente
3
Java se comporta de la misma manera (también imprime 4 para String b), ya que utiliza la representación UTF-16 en matrices de caracteres. Es un carácter de 4 bytes en UTF-8.
Michael
32

Tu personaje en el índice 1 en "A𠈓C"es un par sustituto

El punto clave a recordar es que los pares sustitutos representan caracteres individuales de 32 bits .

Puedes probar este código y volverá True

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

Método Char.IsSurrogatePair (String, Int32)

truesi el parámetro s incluye caracteres adyacentes en las posiciones índice e índice + 1 , y el valor numérico del carácter en el índice de posición varía de U + D800 a U + DBFF, y el valor numérico del carácter en el índice de posición + 1 varía de U + DC00 a U + DFFF; de lo contrario, false.

Esto se explica con más detalle en la propiedad String.Length :

La propiedad Longitud devuelve el número de objetos Char en esta instancia, no el número de caracteres Unicode. La razón es que un personaje Unicode puede estar representado por más de un Char. Use la clase System.Globalization.StringInfo para trabajar con cada personaje Unicode en lugar de cada Char.

Habib
fuente
24

Como han señalado las otras respuestas, incluso si hay 3 caracteres visibles, se representan con 4 charobjetos. Es por eso que Lengthes 4 y no 3.

MSDN afirma que

La propiedad Longitud devuelve el número de objetos Char en esta instancia, no el número de caracteres Unicode.

Sin embargo, si lo que realmente quiere saber es la cantidad de "elementos de texto" y no la cantidad de Charobjetos, puede usar la StringInfoclase.

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

También puede enumerar cada elemento de texto de esta manera

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

El uso foreachen la cadena dividirá la "letra" del medio en dos charobjetos y el resultado impreso no se corresponderá con la cadena.

dee-see
fuente
20

Esto se debe a que la Lengthpropiedad devuelve el número de objetos char , no el número de caracteres unicode. En su caso, uno de los caracteres Unicode está representado por más de un objeto char (SurrogatePair).

La propiedad Longitud devuelve el número de objetos Char en esta instancia, no el número de caracteres Unicode. La razón es que un personaje Unicode puede estar representado por más de un Char. Use la clase System.Globalization.StringInfo para trabajar con cada personaje Unicode en lugar de cada Char.

Yuval Itzchakov
fuente
1
Tienes un uso ambiguo de "personaje" en esta respuesta. Sugiero reemplazar al menos el primero con una terminología precisa.
ligereza corre en órbita el
1
Gracias. Se corrigió la ambigüedad.
Yuval Itzchakov
10

Como otros dijeron, no es el número de caracteres en la cadena sino el número de objetos Char. El carácter 𠈓 es el punto de código U + 20213. Dado que el valor está fuera del rango del tipo char de 16 bits, está codificado en UTF-16 como el par sustituto D840 DE13.

La forma de obtener la longitud en caracteres se mencionó en las otras respuestas. Sin embargo, debe usarse con cuidado ya que puede haber muchas formas de representar un personaje en Unicode. "à" puede tener 1 carácter compuesto o 2 caracteres (a + diacríticos). La normalización puede ser necesaria como en el caso de twitter .

Debería leer esto
El mínimo absoluto que todo desarrollador de software debe saber absolutamente, positivamente sobre los conjuntos de caracteres y Unicode (¡sin excusas!)

phuclv
fuente
6

Esto se debe a que length()solo funciona para puntos de código Unicode que no son más grandes que U+FFFF. Este conjunto de puntos de código se conoce como el Plano Multilingüe Básico (BMP) y usa solo 2 bytes.

Los puntos de código Unicode fuera del BMPestán representados en UTF-16 usando pares sustitutos de 4 bytes.

Para contar correctamente el número de caracteres (3), use StringInfo

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

De acuerdo, en .Net y C # todas las cadenas están codificadas como UTF-16LE . A stringse almacena como una secuencia de caracteres. Cada uno charencapsula el almacenamiento de 2 bytes o 16 bits.

Lo que vemos "en papel o pantalla" como una sola letra, carácter, glifo, símbolo o signo de puntuación puede considerarse como un único Elemento de texto. Como se describe en el Anexo Estándar Unicode # 29 SEGMENTACIÓN DE TEXTO DE UNICODE , cada Elemento de Texto está representado por uno o más Puntos de Código. Puede encontrar una lista exhaustiva de códigos aquí .

Cada punto de código debe codificarse en binario para la representación interna de una computadora. Como se indicó, cada uno charalmacena 2 bytes. Los puntos de código en o debajo U+FFFFse pueden almacenar en un solo char. Los puntos de código anteriores U+FFFFse almacenan como un par sustituto, utilizando dos caracteres para representar un único punto de código.

Dado lo que ahora sabemos que podemos deducir, un elemento de texto puede almacenarse como uno char, como un par sustituto de dos caracteres o, si el elemento de texto está representado por múltiples puntos de código, alguna combinación de caracteres individuales y pares sustitutos. Como si eso no fuera lo suficientemente complicado, algunos Elementos de texto pueden representarse mediante diferentes combinaciones de Puntos de código como se describe en el Anexo estándar Unicode # 15, FORMAS DE NORMALIZACIÓN DE UNICODE .


Interludio

Por lo tanto, las cadenas que se ven iguales cuando se procesan en realidad pueden estar formadas por una combinación diferente de caracteres. Una comparación ordinal (byte por byte) de dos cadenas de este tipo detectaría una diferencia, esto puede ser inesperado o indeseable.

Puede volver a codificar cadenas .Net. para que usen el mismo formulario de normalización. Una vez normalizado, dos cadenas con los mismos elementos de texto se codificarán de la misma manera. Para hacer esto, use la función string.Normalize . Sin embargo, recuerde que algunos elementos de texto diferentes se parecen entre sí. : -s


Entonces, ¿qué significa todo esto en relación con la pregunta? El elemento de texto '𠈓'está representado por la única extensión de ideogramas unificados Code Point U + 20213 cjk b . Esto significa que no puede codificarse como único chary debe codificarse como Par sustituto, utilizando dos caracteres. Es por eso que string bes uno charmás largo que string a.

Si necesita contar de manera confiable (ver advertencia) el número de elementos de texto en un string, debe usar la System.Globalization.StringInfoclase de esta manera.

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

dando la salida,

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

como se esperaba.


Consideración

La implementación .Net de la segmentación de texto Unicode en las clases StringInfoy TextElementEnumeratordebería ser generalmente útil y, en la mayoría de los casos, producirá una respuesta que la persona que llama espera. Sin embargo, como se indica en el Anexo estándar 29 de Unicode, "El objetivo de hacer coincidir las percepciones de los usuarios no siempre se puede cumplir exactamente porque el texto por sí solo no siempre contiene suficiente información para decidir inequívocamente los límites".

Jodrell
fuente
Creo que tu respuesta es potencialmente confusa. En este caso, 𠈓 es solo un punto de código único, pero dado que su punto de código excede 0xFFFF, debe representarse como 2 unidades de código utilizando un par sustituto. Grapheme es otro concepto construido sobre el punto de código, donde un grafema puede ser representado por un solo punto de código o múltiples puntos de código, como se ve en el Hangul coreano o en muchos idiomas basados ​​en el latín.
nhahtdh
@nhahtdh, estoy de acuerdo, mi respuesta fue errónea. Lo he reescrito y espero que ahora cree una mayor claridad.
Jodrell