Por que o comprimento dessa cadeia é maior que o número de caracteres nela?

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

saídas:

Length a = 3
Length b = 4

Por quê? A única coisa que eu poderia imaginar é que o caractere chinês tenha 2 bytes e que o .Lengthmétodo retorne a contagem de bytes.

weini37
fonte
10
Como eu sabia que era um problema de pares substitutos só de olhar para o título. Ah, bom sistema. A globalização é sua aliada!
Chris Cirefice
9
é de 4 bytes em UTF-16, não 2
phuclv
o valor decimal do caractere 𠈓é 131603 e, como os caracteres são bytes não assinados, isso significa que você pode atingir esse valor em 2 caracteres em vez de 4 (o valor não assinado de 16 bits no máximo é 65535 (ou 65536 variações) e o uso de 2 caracteres para representá-lo permite para um número máximo de variações não de 65536 * 2 (131072), mas sim de variações de 65536 * 65536 (4.294.967.296, efetivamente um valor de 32 bits) #
GMasucci
3
@GMAsucci: É 2 caracteres em UTF16, mas 4 bytes, porque um personagem UTF16 é de 2 bytes de tamanho, caso contrário não poderia armazenar 65536 variações, mas apenas 256.
Kaiserludi
4
Eu recomendo a leitura do excelente artigo 'O mínimo absoluto que todo desenvolvedor de software precisa saber absolutamente sobre o conjunto de caracteres e Unicode (sem desculpas!)' Joelonsoftware.com/articles/Unicode.html
ItsMe

Respostas:

232

Todo mundo está dando uma resposta superficial, mas também há uma lógica mais profunda: o número de "caracteres" é uma pergunta difícil de definir e pode ser surpreendentemente cara de calcular, enquanto uma propriedade length deve ser rápida.

Por que é difícil de definir? Bem, existem algumas opções e nenhuma é realmente mais válida que outra:

  • O número de unidades de código (bytes ou outro bloco de dados de tamanho fixo; C # e Windows geralmente usam UTF-16 para retornar o número de partes de dois bytes) certamente é relevante, pois o computador ainda precisa lidar com os dados dessa forma para muitos propósitos (gravar em um arquivo, por exemplo, se preocupa com bytes em vez de caracteres)

  • O número de pontos de código Unicode é bastante fácil de calcular (embora O (n) porque você precise escanear a string em busca de pares substitutos) e possa ser importante para um editor de texto ... mas na verdade não é a mesma coisa que o número de caracteres impresso na tela (chamado grafemas). Por exemplo, algumas letras acentuadas podem ser representadas de duas formas: um único ponto de código ou dois pontos emparelhados, um representando a letra e um dizendo "adicione um acento à carta do meu parceiro". O par teria dois caracteres ou um? Você pode normalizar seqüências de caracteres para ajudar nisso, mas nem todas as letras válidas têm uma única representação de ponto de código.

  • Mesmo o número de grafemas não é o mesmo que o comprimento de uma string impressa, que depende da fonte entre outros fatores, e como alguns caracteres são impressos com alguma sobreposição em muitas fontes (kerning), o comprimento de uma string na tela não é necessariamente igual à soma do comprimento dos grafemas!

  • Alguns pontos Unicode nem são caracteres no sentido tradicional, mas algum tipo de marcador de controle. Como um marcador de ordem de bytes ou um indicador da direita para a esquerda. Isso conta?

Em resumo, o comprimento de uma string é na verdade uma pergunta ridiculamente complexa e o cálculo pode levar muito tempo da CPU, bem como tabelas de dados.

Além disso, qual é o objetivo? Por que essas métricas são importantes? Bem, só você pode responder isso no seu caso, mas pessoalmente, acho que eles geralmente são irrelevantes. A limitação da entrada de dados que eu acho mais lógica é feita pelos limites de bytes, pois é isso que precisa ser transferido ou armazenado de qualquer maneira. A limitação do tamanho da tela é melhor realizada pelo software do lado da tela - se você tiver 100 pixels para a mensagem, quantos caracteres caberão dependerão da fonte etc., que não é conhecida pelo software da camada de dados. Finalmente, dada a complexidade do padrão unicode, você provavelmente terá bugs nos casos extremos de qualquer maneira, se tentar qualquer outra coisa.

Portanto, é uma pergunta difícil, com pouco uso de propósito geral. O número de unidades de código é trivial para calcular - é apenas o comprimento da matriz de dados subjacente - e o mais significativo / útil como regra geral, com uma definição simples.

É por isso bque o comprimento está 4além da explicação superficial de "porque a documentação diz isso".

Adam D. Ruppe
fonte
9
Essencialmente, '.Length' não é o que a maioria dos programadores pensa que é. Talvez deva haver um conjunto de propriedades mais específicas (por exemplo, GlyphCount) e Comprimento marcados como Obsoleto!
Redcalx #
8
@locster Concordo, mas não acho que Lengthdeva ser obsoleto, manter a analogia com matrizes.
Kroltan
2
@locster Não deve ser obsoleto. O python faz muito sentido e ninguém questiona.
simonzack
1
Eu acho que .Length faz muito sentido e é uma propriedade natural, desde que você entenda o que é e por que é assim. Em seguida, ele funciona como qualquer outro array (em algumas línguas como o D, uma string literal é um array, tanto quanto a língua está em causa e ele funciona muito bem)
Adam D. Ruppe
4
Isso não é verdade (um equívoco comum) - com UTF-32, lengthInBytes / 4 daria o número de pontos de código , mas não é o mesmo que o número de "caracteres" ou grafemas. Considere LATINA PEQUENA LETRA E seguida de uma DIAERESE COMBINADA ... que imprime como um único caractere, ela pode até ser normalizada em um único ponto de código, mas ainda tem duas unidades, mesmo em UTF-32.
Adam D. Ruppe
62

A partir da documentação da String.Lengthpropriedade:

A propriedade Length retorna o número de objetos Char nessa instância, não o número de caracteres Unicode. A razão é que um caractere Unicode pode ser representado por mais de um Char . Use o System.Globalization.StringInfo classe para trabalhar uns com os caracteres Unicode em vez de cada Char .

babá
fonte
3
O Java se comporta da mesma maneira (também imprime 4 para String b), pois usa a representação UTF-16 em matrizes de caracteres. É um caractere de 4 bytes em UTF-8.
Michael Michael
32

Seu personagem no índice 1 em "A𠈓C"é um SurrogatePair

O ponto principal a lembrar é que os pares substitutos representam caracteres únicos de 32 bits .

Você pode tentar esse código e ele retornará True

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

Método Char.IsSurrogatePair (String, Int32)

truese o parâmetro s incluir caracteres adjacentes no índice de posições e índice + 1 , e o valor numérico do caractere no índice de posição varia de U + D800 a U + DBFF, e o valor numérico do caractere no índice de posição + 1 varia de U + DC00 a U + DFFF; caso contrário false,.

Isso é explicado mais detalhadamente na propriedade String.Length :

A propriedade Length retorna o número de objetos Char nessa instância, não o número de caracteres Unicode. O motivo é que um caractere Unicode pode ser representado por mais de um caractere. Use a classe System.Globalization.StringInfo para trabalhar com cada caractere Unicode em vez de cada caractere.

Habib
fonte
24

Como as outras respostas apontaram, mesmo se houver 3 caracteres visíveis, eles são representados com 4 charobjetos. É por isso que Lengthé 4 e não 3.

MSDN afirma que

A propriedade Length retorna o número de objetos Char nessa instância, não o número de caracteres Unicode.

No entanto, se você realmente deseja saber o número de "elementos de texto" e não o número de Charobjetos, você pode usar a StringInfoclasse.

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

Você também pode enumerar cada elemento de texto como este

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

O uso foreachda string dividirá a "letra" do meio em dois charobjetos e o resultado impresso não corresponderá à string.

dee-see
fonte
20

Isso ocorre porque a Lengthpropriedade retorna o número de objetos de caracteres , não o número de caracteres unicode. No seu caso, um dos caracteres Unicode é representado por mais de um objeto char (SurrogatePair).

A propriedade Length retorna o número de objetos Char nessa instância, não o número de caracteres Unicode. O motivo é que um caractere Unicode pode ser representado por mais de um caractere. Use a classe System.Globalization.StringInfo para trabalhar com cada caractere Unicode em vez de cada caractere.

Yuval Itzchakov
fonte
1
Você tem um uso ambíguo de "personagem" nesta resposta. Sugiro substituir pelo menos o primeiro por uma terminologia precisa.
Lightness Races in Orbit
1
Obrigado. Corrigida a ambiguidade.
Yuval Itzchakov
10

Como outros disseram, não é o número de caracteres na string, mas o número de objetos Char. O caractere 𠈓 é o ponto de código U + 20213. Como o valor está fora do intervalo do tipo de caractere de 16 bits, ele é codificado em UTF-16 como o par substituto D840 DE13.

A maneira de obter o tamanho dos caracteres foi mencionada nas outras respostas. No entanto, deve ser usado com cuidado, pois pode haver muitas maneiras de representar um caractere no Unicode. "à" pode ter 1 caractere composto ou 2 caracteres (a + diacríticos). A normalização pode ser necessária, como no caso do twitter .

Você deve ler isto
O mínimo absoluto que todo desenvolvedor de software deve saber absolutamente, positivamente sobre Unicode e conjuntos de caracteres (sem desculpas!)

phuclv
fonte
6

Isso ocorre porque length()funciona apenas para pontos de código Unicode que não são maiores que U+FFFF. Esse conjunto de pontos de código é conhecido como Plano Multilíngue Básico (BMP) e usa apenas 2 bytes.

Os pontos de código Unicode fora do BMPsão representados no UTF-16 usando pares substitutos de 4 bytes.

Para contar corretamente o 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
fonte
6

Ok, em .Net e C # todas as strings são codificadas como UTF-16LE . A stringé armazenado como uma sequência de caracteres. Cada um charencapsula o armazenamento de 2 bytes ou 16 bits.

O que vemos "no papel ou na tela" como uma única letra, caractere, glifo, símbolo ou sinal de pontuação pode ser considerado um único Elemento de Texto. Conforme descrito no Anexo UNICODE nº 29 SEGMENTAÇÃO DE TEXTO DO UNICODE , cada elemento de texto é representado por um ou mais pontos de código. Uma lista exaustiva de códigos pode ser encontrada aqui .

Cada ponto de código precisa ser codificado em binário para representação interna por um computador. Como indicado, cada um chararmazena 2 bytes. Os pontos de código iguais ou inferiores U+FFFFpodem ser armazenados em um único char. Os pontos de código acima U+FFFFsão armazenados como um par substituto, usando dois caracteres para representar um único ponto de código.

Dado o que sabemos agora que podemos deduzir, um Elemento de Texto pode ser armazenado como um char, como um Par Substituto de dois caracteres ou, se o Elemento de Texto for representado por vários Pontos de Código, alguma combinação de caracteres únicos e Pares Substitutos. Como se isso não fosse suficientemente complicado, alguns Elementos de Texto podem ser representados por diferentes combinações de Pontos de Código, conforme descrito no Anexo Padrão 15 do Unicode, FORMULÁRIOS DE NORMALIZAÇÃO DO UNICODE .


Interlúdio

Portanto, as strings com a mesma aparência quando renderizadas podem realmente ser compostas de uma combinação diferente de caracteres. Uma comparação ordinal (byte a byte) de duas dessas seqüências detectaria uma diferença, isso pode ser inesperado ou indesejável.

Você pode recodificar as seqüências .Net. para que eles usem o mesmo formulário de normalização. Uma vez normalizado, duas seqüências com os mesmos elementos de texto serão codificadas da mesma maneira. Para fazer isso, use a função string.Normalize . No entanto, lembre-se, alguns elementos de texto diferentes se parecem. : -s


Então, o que tudo isso significa em relação à pergunta? O elemento Text '𠈓'é representado pela única extensão de ideogramas unificados Code Point U + 20213 cjk b . Isso significa que não pode ser codificado como um único chare deve ser codificado como Par Substituto, usando dois caracteres. É por isso que string bé charmais um isso string a.

Se você precisar contar de forma confiável (consulte a advertência) o número de Elementos de Texto em um, stringvocê deve usar a System.Globalization.StringInfoclasse como esta.

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 a saída,

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

como esperado.


Embargo

A implementação .Net da segmentação de texto Unicode nas classes StringInfoe TextElementEnumeratordeve ser geralmente útil e, na maioria dos casos, produzirá uma resposta que o chamador espera. No entanto, conforme declarado no Anexo Padrão 29 da Unicode, "O objetivo de corresponder às percepções do usuário nem sempre pode ser alcançado exatamente porque o texto por si só nem sempre contém informações suficientes para decidir inequivocamente os limites".

Jodrell
fonte
Eu acho que sua resposta é potencialmente confusa. Nesse caso, 𠈓 é apenas um ponto de código único, mas como seu ponto de código excede 0xFFFF, ele deve ser representado como duas unidades de código usando o par substituto. Grafema é outro conceito construído sobre o ponto de código, onde um grafema pode ser representado por um único ponto de código ou vários pontos de código, como visto no Hangul do coreano ou em muitos idiomas baseados em latim.
Nhahtdh 21/11/2014
@ nhahtdh, eu concordo, minha resposta foi errada. Eu o reescrevi e espero que agora crie maior clareza.
precisa