Partilhar via


Alterações de comportamento ao comparar cadeias de caracteres no .NET 5+

O .NET 5 introduz uma mudança comportamental de tempo de execução em que as APIs de globalização usam a UTI por padrão em todas as plataformas suportadas. Isso é um desvio das versões anteriores do .NET Core e do .NET Framework, que utilizam a funcionalidade NLS (National Language Support) do sistema operacional quando executado no Windows. Para obter mais informações sobre essas alterações, incluindo opções de compatibilidade que podem reverter a alteração de comportamento, consulte Globalização e UTI do .NET.

Razão para a alteração

Esta alteração foi introduzida para unificar. O comportamento de globalização da NET em todos os sistemas operacionais suportados. Ele também fornece a capacidade de os aplicativos agruparem suas próprias bibliotecas de globalização em vez de depender das bibliotecas internas do sistema operacional. Para obter mais informações, consulte a notificação de alteração de quebra.

Diferenças comportamentais

Se você usar funções como string.IndexOf(string) sem chamar a sobrecarga que usa um StringComparison argumento, você pode pretender executar uma pesquisa ordinal , mas, em vez disso, inadvertidamente assume uma dependência do comportamento específico da cultura. Como NLS e UTI implementam lógicas diferentes em seus comparadores linguísticos, os resultados de métodos como string.IndexOf(string) podem retornar valores inesperados.

Isso pode se manifestar mesmo em lugares onde você nem sempre espera que as instalações da globalização estejam ativas. Por exemplo, o código a seguir pode produzir uma resposta diferente dependendo do tempo de execução atual.

const string greeting = "Hel\0lo";
Console.WriteLine($"{greeting.IndexOf("\0")}");

// The snippet prints:
//
// '3' when running on .NET Core 2.x - 3.x (Windows)
// '0' when running on .NET 5 or later (Windows)
// '0' when running on .NET Core 2.x - 3.x or .NET 5 (non-Windows)
// '3' when running on .NET Core 2.x or .NET 5+ (in invariant mode)

string s = "Hello\r\nworld!";
int idx = s.IndexOf("\n");
Console.WriteLine(idx);

// The snippet prints:
//
// '6' when running on .NET Core 3.1
// '-1' when running on .NET 5 or .NET Core 3.1 (non-Windows OS)
// '-1' when running on .NET 5 (Windows 10 May 2019 Update or later)
// '6' when running on .NET 6+ (all Windows and non-Windows OSs)

Para obter mais informações, consulte APIs de globalização usam bibliotecas ICU no Windows.

Proteja-se contra comportamentos inesperados

Esta seção fornece duas opções para lidar com alterações de comportamento inesperadas no .NET 5.

Ativar analisadores de código

Os analisadores de código podem detetar sites de chamadas possivelmente com bugs. Para ajudar a se proteger contra comportamentos surpreendentes, recomendamos habilitar os analisadores de plataforma de compilador .NET (Roslyn) em seu projeto. Os analisadores ajudam a sinalizar o código que pode inadvertidamente estar usando um comparador linguístico quando um comparador ordinal provavelmente foi pretendido. As regras a seguir devem ajudar a sinalizar esses problemas:

Essas regras específicas não estão habilitadas por padrão. Para habilitá-los e mostrar quaisquer violações como erros de compilação, defina as seguintes propriedades em seu arquivo de projeto:

<PropertyGroup>
  <AnalysisMode>All</AnalysisMode>
  <WarningsAsErrors>$(WarningsAsErrors);CA1307;CA1309;CA1310</WarningsAsErrors>
</PropertyGroup>

O trecho a seguir mostra exemplos de código que produz os avisos ou erros relevantes do analisador de código.

//
// Potentially incorrect code - answer might vary based on locale.
//
string s = GetString();
// Produces analyzer warning CA1310 for string; CA1307 matches on char ','
int idx = s.IndexOf(",");
Console.WriteLine(idx);

//
// Corrected code - matches the literal substring ",".
//
string s = GetString();
int idx = s.IndexOf(",", StringComparison.Ordinal);
Console.WriteLine(idx);

//
// Corrected code (alternative) - searches for the literal ',' character.
//
string s = GetString();
int idx = s.IndexOf(',');
Console.WriteLine(idx);

Da mesma forma, ao instanciar uma coleção ordenada de cadeias de caracteres ou classificar uma coleção baseada em cadeia de caracteres existente, especifique um comparador explícito.

//
// Potentially incorrect code - behavior might vary based on locale.
//
SortedSet<string> mySet = new SortedSet<string>();
List<string> list = GetListOfStrings();
list.Sort();

//
// Corrected code - uses ordinal sorting; doesn't vary by locale.
//
SortedSet<string> mySet = new SortedSet<string>(StringComparer.Ordinal);
List<string> list = GetListOfStrings();
list.Sort(StringComparer.Ordinal);

Reverter para comportamentos NLS

Para reverter aplicativos .NET 5+ de volta para comportamentos NLS mais antigos quando executados no Windows, siga as etapas em Globalização e UTI do .NET. Essa opção de compatibilidade em todo o aplicativo deve ser definida no nível do aplicativo. Bibliotecas individuais não podem aceitar ou recusar esse comportamento.

Gorjeta

É altamente recomendável habilitar as regras de análise de código CA1307, CA1309 e CA1310 para ajudar a melhorar a higiene do código e descobrir quaisquer bugs latentes existentes. Para obter mais informações, consulte Habilitar analisadores de código.

APIs afetadas

A maioria dos aplicativos .NET não encontrará nenhum comportamento inesperado devido às alterações no .NET 5. No entanto, devido ao número de APIs afetadas e como essas APIs são fundamentais para o ecossistema .NET mais amplo, você deve estar ciente do potencial do .NET 5 para introduzir comportamentos indesejados ou expor bugs latentes que já existem em seu aplicativo.

As APIs afetadas incluem:

Nota

Esta não é uma lista exaustiva de APIs afetadas.

Todas as APIs acima usam pesquisa e comparação de cadeias de caracteres linguísticas usando a cultura atual do thread, por padrão. As diferenças entre pesquisa e comparação linguística e ordinal são destacadas na pesquisa e comparação ordinal vs. linguística.

Como a ICU implementa comparações de cadeia de caracteres linguísticas de forma diferente do NLS, os aplicativos baseados no Windows que atualizam para o .NET 5 de uma versão anterior do .NET Core ou do .NET Framework e que chamam uma das APIs afetadas podem notar que as APIs começam a exibir comportamentos diferentes.

Exceções

  • Se uma API aceitar um explícito StringComparison ou CultureInfo parâmetro, esse parâmetro substituirá o comportamento padrão da API.
  • System.String Os membros em que o primeiro parâmetro é do tipo char (por exemplo, String.IndexOf(Char)) usam a pesquisa ordinal, a menos que o chamador passe um argumento explícito StringComparison que especifique CurrentCulture[IgnoreCase] ou InvariantCulture[IgnoreCase].

Para obter uma análise mais detalhada do comportamento padrão de cada String API, consulte a seção Pesquisa padrão e tipos de comparação.

Pesquisa e comparação ordinal vs. linguística

A pesquisa e comparação ordinal (também conhecida como não-linguística) decompõe uma cadeia de caracteres em seus elementos individuais char e executa uma pesquisa ou comparação char-by-char. Por exemplo, as cadeias de "dog" caracteres e "dog" comparam como iguais sob um Ordinal comparador, uma vez que as duas cadeias consistem exatamente na mesma sequência de caracteres. No entanto, "dog" e "Dog" comparar como não igual sob um Ordinal comparador, porque eles não consistem exatamente na mesma sequência de caracteres. Ou seja, o ponto U+0044 de código de maiúsculas 'D'ocorre antes do ponto U+0064de código de minúsculas'd', resultando em "Dog" classificação antes "dog"de .

Um OrdinalIgnoreCase comparador ainda opera em uma base char-by-char, mas elimina diferenças de maiúsculas e minúsculas durante a execução da operação. Sob um OrdinalIgnoreCase comparador, os pares 'd' char e 'D' comparam como iguais, assim como os pares 'á' char e 'Á'. Mas o char 'a' sem acento se compara como não igual ao char 'á'acentuado.

Alguns exemplos disso são fornecidos na tabela a seguir:

String 1 String 2 Ordinal comparação OrdinalIgnoreCase comparação
"dog" "dog" igual a igual a
"dog" "Dog" não igual igual a
"resume" "résumé" não igual não igual

O Unicode também permite que as cadeias de caracteres tenham várias representações diferentes na memória. Por exemplo, um e-agudo (é) pode ser representado de duas maneiras possíveis:

  • Um único caractere literal 'é' (também escrito como '\u00E9').
  • Um caractere literal sem acento 'e' seguido por um caractere '\u0301'modificador de acento combinado.

Isso significa que as quatro cordas a seguir são exibidas como "résumé", mesmo que suas peças constituintes sejam diferentes. As cadeias de caracteres usam uma combinação de caracteres literais 'é' ou caracteres literais não acentuados 'e' mais o modificador '\u0301'de acento combinado.

  • "r\u00E9sum\u00E9"
  • "r\u00E9sume\u0301"
  • "re\u0301sum\u00E9"
  • "re\u0301sume\u0301"

Sob um comparador ordinal, nenhuma dessas cadeias de caracteres se compara como igual entre si. Isso ocorre porque todos eles contêm diferentes sequências de caracteres subjacentes, mesmo que, quando são renderizados na tela, todos pareçam iguais.

Ao executar uma string.IndexOf(..., StringComparison.Ordinal) operação, o tempo de execução procura uma correspondência exata de substring. Os resultados são os seguintes.

Console.WriteLine("resume".IndexOf("e", StringComparison.Ordinal)); // prints '1'
Console.WriteLine("r\u00E9sum\u00E9".IndexOf("e", StringComparison.Ordinal)); // prints '-1'
Console.WriteLine("r\u00E9sume\u0301".IndexOf("e", StringComparison.Ordinal)); // prints '5'
Console.WriteLine("re\u0301sum\u00E9".IndexOf("e", StringComparison.Ordinal)); // prints '1'
Console.WriteLine("re\u0301sume\u0301".IndexOf("e", StringComparison.Ordinal)); // prints '1'
Console.WriteLine("resume".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '1'
Console.WriteLine("r\u00E9sum\u00E9".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '-1'
Console.WriteLine("r\u00E9sume\u0301".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '5'
Console.WriteLine("re\u0301sum\u00E9".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '1'
Console.WriteLine("re\u0301sume\u0301".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '1'

As rotinas de pesquisa e comparação ordinais nunca são afetadas pela configuração de cultura do thread atual.

As rotinas de pesquisa e comparação linguísticas decompõem uma cadeia de caracteres em elementos de agrupamento e realizam pesquisas ou comparações nesses elementos. Não há necessariamente um mapeamento 1:1 entre os caracteres de uma cadeia de caracteres e seus elementos de agrupamento constituintes. Por exemplo, uma cadeia de caracteres de comprimento 2 pode consistir em apenas um único elemento de agrupamento. Quando duas cadeias de caracteres são comparadas de uma forma com consciência linguística, o comparador verifica se os elementos de agrupamento das duas cadeias têm o mesmo significado semântico, mesmo que os caracteres literais da cadeia sejam diferentes.

Considere novamente a cadeia de caracteres "résumé" e suas quatro representações diferentes. A tabela a seguir mostra cada representação dividida em seus elementos de agrupamento.

String Como elementos de agrupamento
"r\u00E9sum\u00E9" "r" + "\u00E9" + "s" + "u" + "m" + "\u00E9"
"r\u00E9sume\u0301" "r" + "\u00E9" + "s" + "u" + "m" + "e\u0301"
"re\u0301sum\u00E9" "r" + "e\u0301" + "s" + "u" + "m" + "\u00E9"
"re\u0301sume\u0301" "r" + "e\u0301" + "s" + "u" + "m" + "e\u0301"

Um elemento de agrupamento corresponde vagamente ao que os leitores pensariam como um único caractere ou grupo de caracteres. É conceitualmente semelhante a um aglomerado de grafema, mas engloba um guarda-chuva um pouco maior.

Sob um comparador linguístico, correspondências exatas não são necessárias. Em vez disso, os elementos de agrupamento são comparados com base no seu significado semântico. Por exemplo, um comparador linguístico trata as subcadeias e "\u00E9" "e\u0301" como iguais, uma vez que ambas semanticamente significam "um e minúsculo com um modificador de acento agudo". Isso permite que o IndexOf método corresponda à substring "e\u0301" dentro de uma cadeia de caracteres maior que contém a substring "\u00E9"semanticamente equivalente, conforme mostrado no exemplo de código a seguir.

Console.WriteLine("r\u00E9sum\u00E9".IndexOf("e")); // prints '-1' (not found)
Console.WriteLine("r\u00E9sum\u00E9".IndexOf("\u00E9")); // prints '1'
Console.WriteLine("\u00E9".IndexOf("e\u0301")); // prints '0'

Como consequência disso, duas cadeias de comprimentos diferentes podem ser comparadas como iguais se for usada uma comparação linguística. Os chamadores devem tomar cuidado para não usar a lógica de casos especiais que lida com o comprimento da cadeia de caracteres nesses cenários.

As rotinas de pesquisa e comparação sensíveis à cultura são uma forma especial de pesquisa linguística e rotinas de comparação. Sob um comparador sensível à cultura, o conceito de um elemento de agrupamento é estendido para incluir informações específicas da cultura especificada.

Por exemplo, no alfabeto húngaro, quando os dois caracteres <dz> aparecem de um lado para o outro, eles são considerados sua própria letra única, distinta de <d> ou <z>. Isso significa que, quando <dz> é visto em uma corda, um comparador consciente da cultura húngara o trata como um único elemento de agrupamento.

String Como elementos de agrupamento Observações
"endz" "e" + "n" + "d" + "z" (utilizando um comparador linguístico normalizado)
"endz" "e" + "n" + "dz" (utilizando um comparador húngaro consciente da cultura)

Ao usar um comparador com consciência cultural húngara, isso significa que a cadeia de caracteres "endz" não termina com a subcadeia "z", já <que dz> e <z> são considerados elementos de agrupamento com significado semântico diferente.

// Set thread culture to Hungarian
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("hu-HU");
Console.WriteLine("endz".EndsWith("z")); // Prints 'False'

// Set thread culture to invariant culture
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Console.WriteLine("endz".EndsWith("z")); // Prints 'True'

Nota

  • Comportamento: Comparadores linguísticos e conscientes da cultura podem sofrer ajustes comportamentais de tempos em tempos. Tanto a UTI quanto o recurso NLS mais antigo do Windows são atualizados para levar em conta como os idiomas do mundo mudam. Para obter mais informações, consulte a postagem do blog Locale (culture) data churn. O comportamento do comparador ordinal nunca mudará, uma vez que ele executa pesquisa e comparação exatas bit a dia. No entanto, o comportamento do comparador OrdinalIgnoreCase pode mudar à medida que o Unicode cresce para abranger mais conjuntos de caracteres e corrige omissões nos dados de caixa existentes.
  • Uso: Os comparadores StringComparison.InvariantCulture e StringComparison.InvariantCultureIgnoreCase são comparadores linguísticos que não são sensíveis à cultura. Ou seja, esses comparadores entendem conceitos como o caráter acentuado é ter múltiplas representações subjacentes possíveis, e que todas essas representações devem ser tratadas igualmente. Mas comparadores linguísticos sem consciência cultural não conterão tratamento especial para <dz> como distinto de <d> ou <z>, como mostrado acima. Eles também não terão personagens especiais como o alemão Eszett (ß).

O .NET também oferece o modo de globalização invariante. Este modo opt-in desativa os caminhos de código que lidam com rotinas de pesquisa e comparação linguísticas. Nesse modo, todas as operações usam comportamentos Ordinal ou OrdinalIgnoreCase, independentemente do argumento ou StringComparison argumento fornecido CultureInfo pelo chamador. Para obter mais informações, consulte Opções de configuração de tempo de execução para globalização e Modo invariante de globalização do .NET Core.

Para obter mais informações, consulte Práticas recomendadas para comparar cadeias de caracteres no .NET.

Implicações para a segurança

Se seu aplicativo usa uma API afetada para filtragem, recomendamos habilitar as regras de análise de código CA1307 e CA1309 para ajudar a localizar locais onde uma pesquisa linguística pode ter sido usada inadvertidamente em vez de uma pesquisa ordinal. Padrões de código como os a seguir podem ser suscetíveis a explorações de segurança.

//
// THIS SAMPLE CODE IS INCORRECT.
// DO NOT USE IT IN PRODUCTION.
//
public bool ContainsHtmlSensitiveCharacters(string input)
{
    if (input.IndexOf("<") >= 0) { return true; }
    if (input.IndexOf("&") >= 0) { return true; }
    return false;
}

Como o string.IndexOf(string) método usa uma pesquisa linguística por padrão, é possível que uma cadeia de caracteres contenha um literal '<' ou '&' caractere e que a string.IndexOf(string) rotina retorne -1, indicando que a substring de pesquisa não foi encontrada. As regras de análise de código CA1307 e CA1309 sinalizam esses sites de chamada e alertam o desenvolvedor de que há um problema potencial.

Pesquisa padrão e tipos de comparação

A tabela a seguir lista os tipos padrão de pesquisa e comparação para várias APIs de cadeia de caracteres e semelhantes a cadeias de caracteres. Se o chamador fornecer um parâmetro ou StringComparison explícitoCultureInfo, esse parâmetro será honrado sobre qualquer padrão.

API Comportamento predefinido Observações
string.Compare Cultura Atual
string.CompareTo Cultura Atual
string.Contains Ordinal
string.EndsWith Ordinal (quando o primeiro parâmetro é um char)
string.EndsWith Cultura Atual (quando o primeiro parâmetro é um string)
string.Equals Ordinal
string.GetHashCode Ordinal
string.IndexOf Ordinal (quando o primeiro parâmetro é um char)
string.IndexOf Cultura Atual (quando o primeiro parâmetro é um string)
string.IndexOfAny Ordinal
string.LastIndexOf Ordinal (quando o primeiro parâmetro é um char)
string.LastIndexOf Cultura Atual (quando o primeiro parâmetro é um string)
string.LastIndexOfAny Ordinal
string.Replace Ordinal
string.Split Ordinal
string.StartsWith Ordinal (quando o primeiro parâmetro é um char)
string.StartsWith Cultura Atual (quando o primeiro parâmetro é um string)
string.ToLower Cultura Atual
string.ToLowerInvariant Cultura Invariante
string.ToUpper Cultura Atual
string.ToUpperInvariant Cultura Invariante
string.Trim Ordinal
string.TrimEnd Ordinal
string.TrimStart Ordinal
string == string Ordinal
string != string Ordinal

Ao contrário string das APIs, todas as MemoryExtensions APIs executam pesquisas ordinais e comparações por padrão, com as seguintes exceções.

API Comportamento predefinido Observações
MemoryExtensions.ToLower Cultura Atual (quando passado um argumento nulo CultureInfo )
MemoryExtensions.ToLowerInvariant Cultura Invariante
MemoryExtensions.ToUpper Cultura Atual (quando passado um argumento nulo CultureInfo )
MemoryExtensions.ToUpperInvariant Cultura Invariante

Uma consequência é que, ao converter o código de consumir string para consumir ReadOnlySpan<char>, mudanças comportamentais podem ser introduzidas inadvertidamente. Segue-se um exemplo disso.

string str = GetString();
if (str.StartsWith("Hello")) { /* do something */ } // this is a CULTURE-AWARE (linguistic) comparison

ReadOnlySpan<char> span = s.AsSpan();
if (span.StartsWith("Hello")) { /* do something */ } // this is an ORDINAL (non-linguistic) comparison

A maneira recomendada de resolver isso é passar um parâmetro explícito StringComparison para essas APIs. As regras de análise de código CA1307 e CA1309 podem ajudar com isso.

string str = GetString();
if (str.StartsWith("Hello", StringComparison.Ordinal)) { /* do something */ } // ordinal comparison

ReadOnlySpan<char> span = s.AsSpan();
if (span.StartsWith("Hello", StringComparison.Ordinal)) { /* do something */ } // ordinal comparison

Consulte também