Mudanças de comportamento ao comparar strings no .NET 5+
O .NET 5 apresenta uma alteração comportamental em runtime em que as APIs de globalização usam a ICU por padrão em todas as plataformas com suporte. Essa é uma saída das versões anteriores do .NET Core e do .NET Framework, que utilizam a funcionalidade nls (suporte à linguagem nacional) do sistema operacional ao ser executado no Windows. Para obter mais informações sobre essas alterações, incluindo comutadores de compatibilidade que podem reverter a alteração de comportamento, consulte a globalização e ICU do .NET.
Motivo da alteração
Essa alteração foi introduzida para unificar o comportamento de globalização do .NET em todos os sistemas operacionais com suporte. Ele também fornece a capacidade dos aplicativos de agrupar suas próprias bibliotecas de globalização em vez de depender das bibliotecas internas do sistema operacional. Para obter mais informações, confira a notificação de alteração interruptiva.
Diferenças de comportamento
Se você usar funções como string.IndexOf(string)
sem chamar a sobrecarga que usa um argumento StringComparison, talvez você pretenda executar uma pesquisa ordinal, mas, em vez disso, usa inadvertidamente uma dependência do comportamento específico da cultura. Como o NLS e a ICU implementam uma lógica diferente em seus comparadores linguísticos, os resultados de métodos como string.IndexOf(string)
podem retornar valores inesperados.
Isso pode se manifestar mesmo em locais onde você nem sempre espera que as instalações de globalização estejam ativas. Por exemplo, o código a seguir pode produzir uma resposta diferente dependendo do runtime 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 As APIs de globalização usam bibliotecas de ICU no Windows.
Proteger contra comportamento inesperado
Esta seção fornece duas opções para lidar com alterações de comportamento inesperadas no .NET 5.
Habilitar analisadores de código
Os analisadores de código podem detectar sites de chamadas com possíveis bugs. Para ajudar a proteger contra quaisquer comportamentos surpreendentes, recomendamos habilitar analisadores da plataforma de compilador .NET (Roslyn) em seu projeto. Os analisadores ajudam a sinalizar o código que pode estar usando inadvertidamente um comparador linguístico quando um comparador ordinal provavelmente foi pretendido. As regras a seguir devem ajudar a sinalizar esses problemas:
- CA1307: Especificar StringComparison para garantir a clareza
- CA1309: Usar StringComparison ordinal
- CA1310: Especificar StringComparison para garantir a exatidão
Essas regras específicas não são habilitadas por padrão. Para habilitá-las e mostrar quaisquer violações como erros de build, defina as seguintes propriedades em seu arquivo de projeto:
<PropertyGroup>
<AnalysisMode>All</AnalysisMode>
<WarningsAsErrors>$(WarningsAsErrors);CA1307;CA1309;CA1310</WarningsAsErrors>
</PropertyGroup>
O snippet a seguir mostra exemplos de código que produzem 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 classificada 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 os aplicativos .NET 5+ de volta para comportamentos mais antigos do NLS durante a execução no Windows, siga as etapas na Globalização de .NET e ICU. 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.
Dica
É 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 à importância dessas APIs 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:
- System.String.Compare
- System.String.EndsWith
- System.String.IndexOf
- System.String.StartsWith
- System.String.ToLower
- System.String.ToLowerInvariant
- System.String.ToUpper
- System.String.ToUpperInvariant
- System.Globalization.TextInfo (a maioria dos membros)
- System.Globalization.CompareInfo (a maioria dos membros)
- System.Array.Sort (ao classificar matrizes de cadeias de caracteres)
- System.Collections.Generic.List<T>.Sort() (quando os elementos da lista são cadeias de caracteres)
- System.Collections.Generic.SortedDictionary<TKey,TValue> (quando as chaves são cadeias de caracteres)
- System.Collections.Generic.SortedList<TKey,TValue> (quando as chaves são cadeias de caracteres)
- System.Collections.Generic.SortedSet<T> (quando o conjunto contém cadeias de caracteres)
Observação
Esta não é uma lista completa 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 linguística e ordinal e comparação são citadas na Pesquisa ordinal vs. linguística e comparação.
Como o ICU implementa comparações de cadeia de caracteres linguísticas de maneira diferente do NLS, os aplicativos baseados no Windows que atualizam para o .NET 5 de uma versão anterior do .NET Core ou .NET Framework e que chamam uma das APIs afetadas podem perceber que as APIs começam a exibir comportamentos diferentes.
Exceções
- Se uma API aceitar um parâmetro
StringComparison
ouCultureInfo
explícito, esse parâmetro substituirá o comportamento padrão da API. - Os membros
System.String
em que o primeiro parâmetro é do tipochar
(por exemplo, String.IndexOf(Char)) usam pesquisa ordinal, a menos que o chamador passe um argumento explícitoStringComparison
que especificaCurrentCulture[IgnoreCase]
ouInvariantCulture[IgnoreCase]
.
Para obter uma análise mais detalhada do comportamento padrão de cada API String, consulte a seção Padrão de tipos de pesquisa e 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 entre cada caractere. Por exemplo, as cadeias de caracteres "dog"
e "dog"
são comparadas como igual em um comparador Ordinal
, pois as duas cadeias de caracteres consistem exatamente na mesma sequência de caracteres. No entanto, "dog"
e "Dog"
são comparadas como diferentes em um comparador Ordinal
, porque elas não consistem exatamente na mesma sequência de caracteres. Ou seja, o ponto de código U+0044
de 'D'
maiúsculo ocorre antes do ponto de código U+0064
de 'd'
minúsculo, resultando na classificação de "Dog"
antes de "dog"
.
Um comparador OrdinalIgnoreCase
ainda opera caractere por caractere, mas elimina diferenças de maiúsculas e minúsculas durante a execução da operação. Em um comparador OrdinalIgnoreCase
, os pares de caracteres 'd'
e 'D'
são comparados como iguais, assim como os pares de caracteres 'á'
e 'Á'
. Mas o caractere não acentuado 'a'
é comparado como diferente do caractere acentuado 'á'
.
Alguns exemplos disso são fornecidos na tabela a seguir:
Cadeia de caracteres 1 | Cadeia de caracteres 2 | Comparação Ordinal |
Comparação OrdinalIgnoreCase |
---|---|---|---|
"dog" |
"dog" |
equal | equal |
"dog" |
"Dog" |
diferente de | equal |
"resume" |
"résumé" |
diferente de | diferente de |
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 não acentuado
'e'
seguido por um caractere modificador'\u0301'
de acento correspondente.
Isso significa que as quatro cadeias de caracteres a seguir são exibidas como "résumé"
, mesmo que suas partes constituintes sejam diferentes. As cadeias de caracteres usam uma combinação de caracteres literais 'é'
ou caracteres literais não codificados 'e'
, além do modificador '\u0301'
de acento correspondente.
"r\u00E9sum\u00E9"
"r\u00E9sume\u0301"
"re\u0301sum\u00E9"
"re\u0301sume\u0301"
Em um comparador ordinal, nenhuma dessas cadeias de caracteres é comparada como igual à outra. Isso ocorre porque todas elas contêm sequências de caracteres subjacentes diferentes, embora quando são renderizadas para a tela, todas elas têm a mesma aparência.
Ao executar uma operação string.IndexOf(..., StringComparison.Ordinal)
, o runtime procura uma correspondência de substring exata. 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'
Rotinas de pesquisa e comparação ordinais nunca são afetadas pela configuração de cultura do thread atual.
Rotinas de pesquisa linguística e comparação decompõem uma cadeia de caracteres em elementos de ordenação e executam pesquisas ou comparações nesses elementos. Não há necessariamente um mapeamento individual entre os caracteres de uma cadeia de caracteres e seus elementos de ordenação constituintes. Por exemplo, uma cadeia de caracteres de comprimento 2 pode consistir em apenas um único elemento de ordenação. Quando duas cadeias de caracteres são comparadas de forma linguística, o comparador verifica se os elementos de ordenação das duas cadeias de caracteres têm o mesmo significado semântico, mesmo que os caracteres literais da cadeia de caracteres 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 ordenação.
String | Como elementos de ordenação |
---|---|
"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 ordenação corresponde vagamente ao que os leitores pensariam como um único caractere ou cluster de caracteres. Ele é conceitualmente semelhante a um cluster de grafema, mas abrange um conjunto um pouco maior.
Em um comparador linguístico, correspondências exatas não são necessárias. Em vez disso, os elementos de ordenação são comparados com base em seu significado semântico. Por exemplo, um comparador linguístico trata as substrings "\u00E9"
e "e\u0301"
como iguais, pois ambas significam semanticamente "um e minúsculo com um modificador de acento agudo". Isso permite que o método IndexOf
corresponda à substring "e\u0301"
em uma cadeia de caracteres maior que contém a substring semanticamente equivalente "\u00E9"
, 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 caracteres de comprimentos diferentes podem ser comparadas como iguais se uma comparação linguística for usada. Os chamadores devem ter cuidado para não usar a lógica de caso especial que lida com o comprimento da cadeia de caracteres nesses cenários.
As rotinas de pesquisa e comparação com reconhecimento de cultura são uma forma especial de rotinas de pesquisa e comparação linguísticas. Em um comparador com reconhecimento de cultura, o conceito de um elemento de ordenação é estendido para incluir informações específicas à cultura especificada.
Por exemplo, no alfabeto húngaro, quando os dois caracteres <dz> aparecem consecutivos, eles são considerados a própria letra exclusiva distinta de <d> ou <z>. Isso significa que quando <dz> é visto em uma cadeia de caracteres, um comparador com reconhecimento de cultura húngaro o trata como um único elemento de ordenação.
String | Como elementos de ordenação | Comentários |
---|---|---|
"endz" |
"e" + "n" + "d" + "z" |
(usando um comparador linguístico padrão) |
"endz" |
"e" + "n" + "dz" |
(usando um comparador húngaro com reconhecimento de cultura) |
Ao usar um comparador com reconhecimento à cultura Húngara, isso significa que a cadeia de caracteres "endz"
não termina com a subcadeia de caracteres "z"
, pois <dz> e <z> são considerados elementos de ordenação com significados semânticos diferentes.
// 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'
Observação
- Comportamento: comparadores com reconhecimento linguístico e cultural podem sofrer ajustes comportamentais de tempos em tempos. A UTI e a instalação mais antiga do Windows NLS são atualizadas para considerar como os idiomas do mundo mudam. Para obter mais informações, consulte a rotatividade de dados de localidade (cultura) da postagem do blog. O comportamento do comparador Ordinal nunca será alterado, pois ele executa a pesquisa e a comparação bit a bit exatas. No entanto, o comportamento do comparador OrdinalIgnoreCase pode mudar à medida que o Unicode cresce para abranger mais conjuntos de caracteres e correções de funções em dados de comparação e semelhanças que podem incluir medidas.
- Uso: os comparadores
StringComparison.InvariantCulture
eStringComparison.InvariantCultureIgnoreCase
são comparadores linguísticos que não reconhecem a cultura. Ou seja, esses comparadores entendem conceitos como o caractere acentuado que tem várias representações subjacentes possíveis e que todas essas representações devem ser tratadas como iguais. Mas os comparadores linguísticos sem reconhecimento de cultura não conterão tratamento especial para <dz> tão distinto de <d> ou <z>, como mostrado acima. Eles também não vão diferenciar caracteres como o alemão Eszett (ß).
O .NET também oferece o modo de globalização invariável. Esse modo de aceitação desabilita caminhos de código que lidam com rotinas de pesquisa linguística e comparação. Nesse modo, todas as operações usam comportamentos Ordinal ou OrdinalIgnoreCase, independentemente do argumento CultureInfo
ou StringComparison
fornecido pelo chamador. Para obter mais informações, consulte as Opções de configuração do Runtime para globalização e Modo invariável de globalização do .NET Core.
Para obter mais informações, consulte Práticas recomendadas para a comparação de cadeias de caracteres no .NET.
Implicações de segurança
Se seu aplicativo usar 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 o seguinte 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 método string.IndexOf(string)
usa uma pesquisa linguística por padrão, é possível que uma string contenha um caractere literal '<'
ou '&'
e que a rotina string.IndexOf(string)
retorne -1
, indicando que a substring de pesquisa não foi encontrado. As regras de análise de código CA1307 e CA1309 sinalizam esses sites de chamadas e alertam o desenvolvedor de que há um problema em potencial.
Tipos de pesquisa e comparação padrão
A tabela a seguir lista os tipos de pesquisa e comparação padrão para várias APIs semelhantes a cadeias de caracteres e cadeias de caracteres. Se o chamador fornecer um parâmetro CultureInfo
ou StringComparison
explícito, esse parâmetro será respeitado em relação a qualquer padrão.
API | Comportamento padrão | Comentários |
---|---|---|
string.Compare |
CurrentCulture | |
string.CompareTo |
CurrentCulture | |
string.Contains |
Ordinal | |
string.EndsWith |
Ordinal | (quando o primeiro parâmetro é char ) |
string.EndsWith |
CurrentCulture | (quando o primeiro parâmetro é string ) |
string.Equals |
Ordinal | |
string.GetHashCode |
Ordinal | |
string.IndexOf |
Ordinal | (quando o primeiro parâmetro é char ) |
string.IndexOf |
CurrentCulture | (quando o primeiro parâmetro é string ) |
string.IndexOfAny |
Ordinal | |
string.LastIndexOf |
Ordinal | (quando o primeiro parâmetro é char ) |
string.LastIndexOf |
CurrentCulture | (quando o primeiro parâmetro é string ) |
string.LastIndexOfAny |
Ordinal | |
string.Replace |
Ordinal | |
string.Split |
Ordinal | |
string.StartsWith |
Ordinal | (quando o primeiro parâmetro é char ) |
string.StartsWith |
CurrentCulture | (quando o primeiro parâmetro é string ) |
string.ToLower |
CurrentCulture | |
string.ToLowerInvariant |
InvariantCulture | |
string.ToUpper |
CurrentCulture | |
string.ToUpperInvariant |
InvariantCulture | |
string.Trim |
Ordinal | |
string.TrimEnd |
Ordinal | |
string.TrimStart |
Ordinal | |
string == string |
Ordinal | |
string != string |
Ordinal |
Ao contrário das APIs string
, todas as APIs MemoryExtensions
executam pesquisas ordinais e comparações por padrão, com as seguintes exceções.
API | Comportamento padrão | Comentários |
---|---|---|
MemoryExtensions.ToLower |
CurrentCulture | (quando passado um argumento nulo CultureInfo ) |
MemoryExtensions.ToLowerInvariant |
InvariantCulture | |
MemoryExtensions.ToUpper |
CurrentCulture | (quando passado um argumento nulo CultureInfo ) |
MemoryExtensions.ToUpperInvariant |
InvariantCulture |
Uma consequência é que, ao converter o código de consumo string
em consumo ReadOnlySpan<char>
, alterações comportamentais podem ser inadvertidamente introduzidas. Abaixo, 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