Compartilhar via


Práticas recomendadas para comparar cadeias de caracteres no .NET

O .NET fornece amplo suporte para o desenvolvimento de aplicativos localizados e globalizados e facilita a aplicação das convenções da cultura atual ou de uma cultura específica ao executar operações comuns, como classificar e exibir cadeias de caracteres. Mas classificar ou comparar cadeias de caracteres nem sempre é uma operação sensível à cultura. Por exemplo, cadeias de caracteres que são usadas internamente por um aplicativo normalmente devem ser tratadas de forma idêntica em todas as culturas. Quando dados de cadeia de caracteres culturalmente independentes, como marcas XML, marcas HTML, nomes de usuário, caminhos de arquivo e nomes de objetos do sistema, são interpretados como se fossem sensíveis à cultura, o código do aplicativo pode estar sujeito a bugs sutis, desempenho ruim e, em alguns casos, problemas de segurança.

Este artigo examina os métodos de classificação, comparação e casing de cadeia de caracteres no .NET, apresenta recomendações para selecionar um método de tratamento de cadeia de caracteres apropriado e fornece informações adicionais sobre métodos de tratamento de cadeia de caracteres.

Recomendações para uso de cadeia de caracteres

Ao desenvolver com o .NET, siga estas recomendações ao comparar cadeias de caracteres.

Dica

Vários métodos relacionados à cadeia de caracteres executam a comparação. Os exemplos incluem String.Equals, String.Compare, String.IndexOfe String.StartsWith.

Evite as seguintes práticas ao comparar cadeias de caracteres:

  • Não use sobrecargas que não especificam explicita ou implicitamente as regras de comparação de cadeias de caracteres para operações de cadeia de caracteres.
  • Não use operações de cadeia de caracteres com base em StringComparison.InvariantCulture na maioria dos casos. Uma das poucas exceções é quando você mantém dados linguisticamente significativos, mas culturalmente independentes.
  • Não use uma sobrecarga dos métodos String.Compare ou CompareTo e teste para um valor retornado de zero para determinar se duas cadeias de caracteres são iguais.

Especificar comparações de cadeia de caracteres explicitamente

A maioria dos métodos de manipulação de cadeia de caracteres no .NET está sobrecarregada. Normalmente, uma ou mais sobrecargas aceitam configurações padrão, enquanto outras não aceitam padrões e definem a maneira precisa em que as cadeias de caracteres devem ser comparadas ou manipuladas. A maioria dos métodos que não dependem de padrões inclui um parâmetro de tipo StringComparison, que é uma enumeração que especifica explicitamente regras para comparação de cadeia de caracteres por cultura e caso. A tabela a seguir descreve os membros da StringComparison enumeração.

StringComparison membro Descrição
CurrentCulture Executa uma comparação que diferencia maiúsculas de minúsculas usando a cultura atual.
CurrentCultureIgnoreCase Executa uma comparação que não diferencia maiúsculas de minúsculas usando a cultura atual.
InvariantCulture Executa uma comparação que diferencia maiúsculas de minúsculas usando a cultura invariável.
InvariantCultureIgnoreCase Executa uma comparação que não diferencia maiúsculas de minúsculas usando a cultura invariável.
Ordinal Executa uma comparação ordinal.
OrdinalIgnoreCase Executa uma comparação ordinal que não diferencia maiúsculas de minúsculas.

Por exemplo, o IndexOf método, que retorna o índice de uma subcadeia de caracteres em um String objeto que corresponde a um caractere ou uma cadeia de caracteres, tem nove sobrecargas:

Recomendamos que você selecione uma sobrecarga que não use valores padrão, pelos seguintes motivos:

  • Algumas sobrecargas com parâmetros padrão (aqueles que pesquisam um Char na instância de cadeia de caracteres) realizam uma comparação ordinal, enquanto outras (aquelas que pesquisam uma cadeia de caracteres na instância de cadeia de caracteres) levam em consideração a cultura. É difícil lembrar qual método usa qual valor padrão e fácil confundir as sobrecargas.

  • A intenção do código que depende de valores padrão para chamadas de método não está clara. No exemplo a seguir, que depende de padrões, é difícil saber se o desenvolvedor realmente pretendia uma comparação ordinal ou linguística de duas cadeias de caracteres ou se uma diferença de caso entre url.Scheme e "https" pode fazer com que o teste de igualdade retorne false.

    Uri url = new("https://learn.microsoft.com/");
    
    // Incorrect
    if (string.Equals(url.Scheme, "https"))
    {
        // ...Code to handle HTTPS protocol.
    }
    
    Dim url As New Uri("https://learn.microsoft.com/")
    
    ' Incorrect
    If String.Equals(url.Scheme, "https") Then
        ' ...Code to handle HTTPS protocol.
    End If
    

Em geral, recomendamos que você chame um método que não dependa de padrões, pois ele torna a intenção do código inequívoca. Isso, por sua vez, torna o código mais legível e mais fácil de depurar e manter. O exemplo a seguir aborda as questões levantadas sobre o exemplo anterior. Isso deixa claro que a comparação ordinal é usada e que as diferenças no caso são ignoradas.

Uri url = new("https://learn.microsoft.com/");

// Correct
if (string.Equals(url.Scheme, "https", StringComparison.OrdinalIgnoreCase))
{
    // ...Code to handle HTTPS protocol.
}
Dim url As New Uri("https://learn.microsoft.com/")

' Incorrect
If String.Equals(url.Scheme, "https", StringComparison.OrdinalIgnoreCase) Then
    ' ...Code to handle HTTPS protocol.
End If

Os detalhes da comparação de cadeia de caracteres

A comparação de cadeia de caracteres é o centro de muitas operações relacionadas à cadeia de caracteres, especialmente a classificação e teste de igualdade. As cadeias de caracteres são classificadas em uma determinada ordem: se “my” aparece antes de “string” em uma lista classificada de cadeias de caracteres, “my” deve comparar menos que ou igual a “string”. Além disso, a comparação define implicitamente a igualdade. A operação de comparação retorna zero para cadeias de caracteres que considera iguais. Uma boa interpretação é que nenhuma cadeia de caracteres é menor que a outra. As operações mais significativas que envolvem cadeias de caracteres incluem um ou ambos os procedimentos: comparar com outra cadeia de caracteres e executar uma operação de classificação bem definida.

Observação

Você pode baixar as Tabelas de Peso de Classificação, um conjunto de arquivos de texto que contêm informações sobre os pesos de caractere usados em operações de classificação e comparação para sistemas operacionais Windows e a Tabela de Elementos de Ordenação Unicode Padrão, a versão mais recente da tabela de peso de classificação para Linux e macOS. A versão específica da tabela de peso de classificação no Linux e no macOS depende da versão das bibliotecas de Componentes Internacionais para Unicode instaladas no sistema. Para obter informações sobre as versões da UTI e as versões unicode que elas implementam, consulte Baixar a UTI.

No entanto, avaliar duas cadeias de caracteres para igualdade ou ordem de classificação não gera um único resultado correto; o resultado depende dos critérios usados para comparar as cadeias de caracteres. Em particular, as comparações de cadeia de caracteres ordinais ou baseadas no uso de maiúsculas e minúsculas e convenções classificação da cultura atual ou da cultura invariável (uma cultura independente de localidade com base no idioma inglês) podem gerar resultados diferentes.

Além disso, comparações de cadeia de caracteres usando diferentes versões do .NET ou usando o .NET em diferentes sistemas operacionais ou versões do sistema operacional podem retornar resultados diferentes. Para obter mais informações, consulte Strings e o Unicode Standard.

Comparações de cadeia de caracteres que usam a cultura atual

Um critério envolve o uso das convenções da cultura atual ao comparar cadeias de caracteres. Comparações baseadas na cultura atual usam a cultura ou a localidade atual do thread. Se a cultura não for definida pelo usuário, ela usará como padrão a configuração do sistema operacional. Você sempre deve usar comparações baseadas na cultura atual quando os dados são linguisticamente relevantes e quando refletem a interação do usuário sensível à cultura.

No entanto, o comportamento da comparação e do uso de maiúsculas e minúsculas no .NET muda quando a cultura muda. Isso acontece quando um aplicativo é executado em um computador que tem uma cultura diferente do computador no qual o aplicativo foi desenvolvido ou quando o thread em execução altera sua cultura. Esse comportamento é intencional, mas não é óbvio para muitos desenvolvedores. O exemplo a seguir ilustra diferenças na ordem de classificação entre as culturas de inglês dos EUA ("en-US") e sueco ("sv-SE"). Observe que as palavras "ångström", "Windows" e "Visual Studio" aparecem em posições diferentes nas matrizes de cadeia de caracteres classificadas.

using System.Globalization;

// Words to sort
string[] values= { "able", "ångström", "apple", "Æble",
                    "Windows", "Visual Studio" };

// Current culture
Array.Sort(values);
DisplayArray(values);

// Change culture to Swedish (Sweden)
string originalCulture = CultureInfo.CurrentCulture.Name;
Thread.CurrentThread.CurrentCulture = new CultureInfo("sv-SE");
Array.Sort(values);
DisplayArray(values);

// Restore the original culture
Thread.CurrentThread.CurrentCulture = new CultureInfo(originalCulture);

static void DisplayArray(string[] values)
{
    Console.WriteLine($"Sorting using the {CultureInfo.CurrentCulture.Name} culture:");
    
    foreach (string value in values)
        Console.WriteLine($"   {value}");

    Console.WriteLine();
}

// The example displays the following output:
//     Sorting using the en-US culture:
//        able
//        Æble
//        ångström
//        apple
//        Visual Studio
//        Windows
//
//     Sorting using the sv-SE culture:
//        able
//        apple
//        Visual Studio
//        Windows
//        ångström
//        Æble
Imports System.Globalization
Imports System.Threading

Module Program
    Sub Main()
        ' Words to sort
        Dim values As String() = {"able", "ångström", "apple", "Æble",
                                  "Windows", "Visual Studio"}

        ' Current culture
        Array.Sort(values)
        DisplayArray(values)

        ' Change culture to Swedish (Sweden)
        Dim originalCulture As String = CultureInfo.CurrentCulture.Name
        Thread.CurrentThread.CurrentCulture = New CultureInfo("sv-SE")
        Array.Sort(values)
        DisplayArray(values)

        ' Restore the original culture
        Thread.CurrentThread.CurrentCulture = New CultureInfo(originalCulture)
    End Sub

    Sub DisplayArray(values As String())
        Console.WriteLine($"Sorting using the {CultureInfo.CurrentCulture.Name} culture:")

        For Each value As String In values
            Console.WriteLine($"   {value}")
        Next

        Console.WriteLine()
    End Sub
End Module

' The example displays the following output:
'     Sorting using the en-US culture:
'        able
'        Æble
'        ångström
'        apple
'        Visual Studio
'        Windows
'
'     Sorting using the sv-SE culture:
'        able
'        apple
'        Visual Studio
'        Windows
'        ångström
'        Æble

As comparações que não diferenciam maiúsculas de minúsculas que usam a cultura atual são iguais às comparações que levam em conta a cultura, exceto que elas ignoram as maiúsculas e minúsculas conforme determinado pela cultura atual do thread. Esse comportamento pode se manifestar também em como as classificações são ordenadas.

Comparações que usam semântica de cultura atual são o padrão para os seguintes métodos:

Em qualquer caso, é recomendável que você chame uma sobrecarga que tenha um parâmetro StringComparison para deixar a intenção da chamada do método clara.

Bugs sutis e não tão sutis podem surgir quando dados de cadeias de caracteres não linguísticas são interpretados linguisticamente ou quando os dados da cadeia de caracteres de uma cultura específica são interpretados usando as convenções de outra cultura. O exemplo canônico é o problema Turkish-I.

Para quase todos os alfabetos latinos, incluindo inglês americano, o caractere "i" (\u0069) é a versão minúscula do caractere "I" (\u0049). Essa regra de maiúsculas e minúsculas rapidamente se torna o padrão para alguém programando em tal cultura. No entanto, o alfabeto turco ("tr-TR") inclui um caractere "I com ponto" "İ" (\u0130), que é a versão maiúscula de "i". O turco também inclui um caractere "i sem ponto" minúsculo, "ı" (\u0131), que capitaliza "I". Esse comportamento também ocorre na cultura do Azerbaijão ("az").

Portanto, convenções sobre usar "i" maiúscula ou minúscula não são válidas entre diferentes culturas. Se você usar as sobrecargas padrão para rotinas de comparação de cadeias de caracteres, elas estarão sujeitas à variação entre culturas. Se os dados a serem comparados não forem linguísticos, o uso das sobrecargas padrão poderá produzir resultados indesejáveis, como ilustra a tentativa a seguir de executar uma comparação que não diferencia maiúsculas de minúsculas das cadeias de caracteres "bill" e "BILL".

using System.Globalization;

string name = "Bill";

Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
Console.WriteLine($"Culture = {Thread.CurrentThread.CurrentCulture.DisplayName}");
Console.WriteLine($"   Is 'Bill' the same as 'BILL'? {name.Equals("BILL", StringComparison.OrdinalIgnoreCase)}");
Console.WriteLine($"   Does 'Bill' start with 'BILL'? {name.StartsWith("BILL", true, null)}");
Console.WriteLine();

Thread.CurrentThread.CurrentCulture = new CultureInfo("tr-TR");
Console.WriteLine($"Culture = {Thread.CurrentThread.CurrentCulture.DisplayName}");
Console.WriteLine($"   Is 'Bill' the same as 'BILL'? {name.Equals("BILL", StringComparison.OrdinalIgnoreCase)}");
Console.WriteLine($"   Does 'Bill' start with 'BILL'? {name.StartsWith("BILL", true, null)}");

//' The example displays the following output:
//'
//'     Culture = English (United States)
//'        Is 'Bill' the same as 'BILL'? True
//'        Does 'Bill' start with 'BILL'? True
//'     
//'     Culture = Turkish (Türkiye)
//'        Is 'Bill' the same as 'BILL'? True
//'        Does 'Bill' start with 'BILL'? False
Imports System.Globalization
Imports System.Threading

Module Program
    Sub Main()
        Dim name As String = "Bill"

        Thread.CurrentThread.CurrentCulture = New CultureInfo("en-US")
        Console.WriteLine($"Culture = {Thread.CurrentThread.CurrentCulture.DisplayName}")
        Console.WriteLine($"   Is 'Bill' the same as 'BILL'? {name.Equals("BILL", StringComparison.OrdinalIgnoreCase)}")
        Console.WriteLine($"   Does 'Bill' start with 'BILL'? {name.StartsWith("BILL", True, Nothing)}")
        Console.WriteLine()

        Thread.CurrentThread.CurrentCulture = New CultureInfo("tr-TR")
        Console.WriteLine($"Culture = {Thread.CurrentThread.CurrentCulture.DisplayName}")
        Console.WriteLine($"   Is 'Bill' the same as 'BILL'? {name.Equals("BILL", StringComparison.OrdinalIgnoreCase)}")
        Console.WriteLine($"   Does 'Bill' start with 'BILL'? {name.StartsWith("BILL", True, Nothing)}")
    End Sub

End Module

' The example displays the following output:
'
'     Culture = English (United States)
'        Is 'Bill' the same as 'BILL'? True
'        Does 'Bill' start with 'BILL'? True
'     
'     Culture = Turkish (Türkiye)
'        Is 'Bill' the same as 'BILL'? True
'        Does 'Bill' start with 'BILL'? False

Essa comparação poderá causar problemas significativos se a cultura for usada inadvertidamente em configurações sensíveis à segurança, como no exemplo a seguir. Uma chamada de método como IsFileURI("file:") retorna true se a cultura atual for inglês dos EUA, mas false se a cultura atual for turca. Assim, em sistemas turcos, alguém poderia contornar medidas de segurança que bloqueiam o acesso a URIs que não diferenciam maiúsculas de minúsculas que começam com "FILE:".

public static bool IsFileURI(string path) =>
    path.StartsWith("FILE:", true, null);
Public Shared Function IsFileURI(path As String) As Boolean
    Return path.StartsWith("FILE:", True, Nothing)
End Function

Nesse caso, como "file:" deve ser interpretado como um identificador não linguístico e sem diferenciação de cultura, o código deve ser escrito conforme mostrado no exemplo a seguir:

public static bool IsFileURI(string path) =>
    path.StartsWith("FILE:", StringComparison.OrdinalIgnoreCase);
Public Shared Function IsFileURI(path As String) As Boolean
    Return path.StartsWith("FILE:", StringComparison.OrdinalIgnoreCase)
End Function

Operações ordinais da cadeia de caracteres

Especificar o valor StringComparison.Ordinal ou StringComparison.OrdinalIgnoreCase em uma chamada de método significa uma comparação não linguística, na qual são ignorados os recursos das linguagens naturais. Os métodos que são invocados com esses valores de StringComparison baseiam as decisões de operação da cadeia de caracteres em comparações de byte simples em vez de no uso de maiúsculas e minúsculas ou tabelas de equivalência que são parametrizadas pela cultura. Na maioria dos casos, essa abordagem se ajusta melhor à interpretação pretendida de cadeias de caracteres, tornando o código mais rápido e confiável.

Comparações ordinais são comparações de cadeia de caracteres em que cada byte de cada cadeia de caracteres é comparado sem interpretação linguística; por exemplo, "windows" não corresponde a "Windows". Essa é essencialmente uma chamada para a função de runtime strcmp C. Use essa comparação quando o contexto determinar que as cadeias de caracteres devem corresponder exatamente ou exigir uma política de correspondência conservadora. Além disso, a comparação ordinal é a operação de comparação mais rápida porque não aplica regras linguísticas ao determinar um resultado.

As cadeias de caracteres no .NET podem conter caracteres nulos inseridos (e outros caracteres que não são de impressão). Uma das diferenças mais claras entre a comparação ordinal e sensível à cultura (incluindo comparações que usam a cultura invariável) diz respeito ao tratamento de caracteres nulos inseridos em uma cadeia de caracteres. Esses caracteres são ignorados quando você usa os métodos String.Compare e String.Equals para executar comparações sensíveis à cultura (incluindo comparações que usam a cultura invariável). Como resultado, cadeias de caracteres que contêm caracteres nulos inseridos podem ser consideradas iguais às cadeias de caracteres que não contêm. Caracteres não imprimíveis inseridos podem ser ignorados para fins de métodos de comparação de cadeia de caracteres, como String.StartsWith.

Importante

Embora os métodos de comparação de cadeia de caracteres desconsiderem caracteres nulos inseridos, os métodos de pesquisa de cadeia de caracteres, como String.Contains, String.EndsWith, String.IndexOfe String.LastIndexOfString.StartsWith não o fazem.

O exemplo a seguir executa uma comparação sensível à cultura da cadeia de caracteres "Aa" com uma cadeia de caracteres semelhante que contém vários caracteres nulos inseridos entre "A" e "a" e mostra como as duas cadeias de caracteres são consideradas iguais:

string str1 = "Aa";
string str2 = "A" + new string('\u0000', 3) + "a";

Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo("en-us");

Console.WriteLine($"Comparing '{str1}' ({ShowBytes(str1)}) and '{str2}' ({ShowBytes(str2)}):");
Console.WriteLine("   With String.Compare:");
Console.WriteLine($"      Current Culture: {string.Compare(str1, str2, StringComparison.CurrentCulture)}");
Console.WriteLine($"      Invariant Culture: {string.Compare(str1, str2, StringComparison.InvariantCulture)}");
Console.WriteLine("   With String.Equals:");
Console.WriteLine($"      Current Culture: {string.Equals(str1, str2, StringComparison.CurrentCulture)}");
Console.WriteLine($"      Invariant Culture: {string.Equals(str1, str2, StringComparison.InvariantCulture)}");

string ShowBytes(string value)
{
   string hexString = string.Empty;
   for (int index = 0; index < value.Length; index++)
   {
      string result = Convert.ToInt32(value[index]).ToString("X4");
      result = string.Concat(" ", result.Substring(0,2), " ", result.Substring(2, 2));
      hexString += result;
   }
   return hexString.Trim();
}

// The example displays the following output:
//     Comparing 'Aa' (00 41 00 61) and 'Aa' (00 41 00 00 00 00 00 00 00 61):
//        With String.Compare:
//           Current Culture: 0
//           Invariant Culture: 0
//        With String.Equals:
//           Current Culture: True
//           Invariant Culture: True

Module Program
    Sub Main()
        Dim str1 As String = "Aa"
        Dim str2 As String = "A" & New String(Convert.ToChar(0), 3) & "a"

        Console.WriteLine($"Comparing '{str1}' ({ShowBytes(str1)}) and '{str2}' ({ShowBytes(str2)}):")
        Console.WriteLine("   With String.Compare:")
        Console.WriteLine($"      Current Culture: {String.Compare(str1, str2, StringComparison.CurrentCulture)}")
        Console.WriteLine($"      Invariant Culture: {String.Compare(str1, str2, StringComparison.InvariantCulture)}")
        Console.WriteLine("   With String.Equals:")
        Console.WriteLine($"      Current Culture: {String.Equals(str1, str2, StringComparison.CurrentCulture)}")
        Console.WriteLine($"      Invariant Culture: {String.Equals(str1, str2, StringComparison.InvariantCulture)}")
    End Sub

    Function ShowBytes(str As String) As String
        Dim hexString As String = String.Empty

        For ctr As Integer = 0 To str.Length - 1
            Dim result As String = Convert.ToInt32(str.Chars(ctr)).ToString("X4")
            result = String.Concat(" ", result.Substring(0, 2), " ", result.Substring(2, 2))
            hexString &= result
        Next

        Return hexString.Trim()
    End Function

    ' The example displays the following output:
    '     Comparing 'Aa' (00 41 00 61) and 'Aa' (00 41 00 00 00 00 00 00 00 61):
    '        With String.Compare:
    '           Current Culture: 0
    '           Invariant Culture: 0
    '        With String.Equals:
    '           Current Culture: True
    '           Invariant Culture: True
End Module

No entanto, as cadeias de caracteres não são consideradas iguais quando você usa a comparação ordinal, como mostra o exemplo a seguir:

string str1 = "Aa";
string str2 = "A" + new String('\u0000', 3) + "a";

Console.WriteLine($"Comparing '{str1}' ({ShowBytes(str1)}) and '{str2}' ({ShowBytes(str2)}):");
Console.WriteLine("   With String.Compare:");
Console.WriteLine($"      Ordinal: {string.Compare(str1, str2, StringComparison.Ordinal)}");
Console.WriteLine("   With String.Equals:");
Console.WriteLine($"      Ordinal: {string.Equals(str1, str2, StringComparison.Ordinal)}");

string ShowBytes(string str)
{
    string hexString = string.Empty;
    for (int ctr = 0; ctr < str.Length; ctr++)
    {
        string result = Convert.ToInt32(str[ctr]).ToString("X4");
        result = " " + result.Substring(0, 2) + " " + result.Substring(2, 2);
        hexString += result;
    }
    return hexString.Trim();
}

// The example displays the following output:
//    Comparing 'Aa' (00 41 00 61) and 'A   a' (00 41 00 00 00 00 00 00 00 61):
//       With String.Compare:
//          Ordinal: 97
//       With String.Equals:
//          Ordinal: False
Module Program
    Sub Main()
        Dim str1 As String = "Aa"
        Dim str2 As String = "A" & New String(Convert.ToChar(0), 3) & "a"

        Console.WriteLine($"Comparing '{str1}' ({ShowBytes(str1)}) and '{str2}' ({ShowBytes(str2)}):")
        Console.WriteLine("   With String.Compare:")
        Console.WriteLine($"      Ordinal: {String.Compare(str1, str2, StringComparison.Ordinal)}")
        Console.WriteLine("   With String.Equals:")
        Console.WriteLine($"      Ordinal: {String.Equals(str1, str2, StringComparison.Ordinal)}")
    End Sub

    Function ShowBytes(str As String) As String
        Dim hexString As String = String.Empty

        For ctr As Integer = 0 To str.Length - 1
            Dim result As String = Convert.ToInt32(str.Chars(ctr)).ToString("X4")
            result = String.Concat(" ", result.Substring(0, 2), " ", result.Substring(2, 2))
            hexString &= result
        Next

        Return hexString.Trim()
    End Function

    ' The example displays the following output:
    '    Comparing 'Aa' (00 41 00 61) and 'A   a' (00 41 00 00 00 00 00 00 00 61):
    '       With String.Compare:
    '          Ordinal: 97
    '       With String.Equals:
    '          Ordinal: False
End Module

As comparações ordinais que não diferenciam maiúsculas de minúsculas são a próxima abordagem conservadora. Essas comparações ignoram a maioria das maiúsculas e minúsculas, por exemplo, "windows" corresponde a "Windows". Ao lidar com caracteres ASCII, essa política é equivalente a StringComparison.Ordinal, exceto que ela ignora as maiúsculas e minúsculas de ASCII normais. Portanto, qualquer caractere em [A, Z] (\u0041-\u005A) corresponde ao caractere correspondente em [a,z] (\u0061-\007A). As maiúsculas e minúsculas fora do intervalo de ASCII usam as tabelas de cultura invariável. Portanto, a seguinte comparação:

string.Compare(strA, strB, StringComparison.OrdinalIgnoreCase);
String.Compare(strA, strB, StringComparison.OrdinalIgnoreCase)

é equivalente a (mas mais rápido do que) essa comparação:

string.Compare(strA.ToUpperInvariant(), strB.ToUpperInvariant(), StringComparison.Ordinal);
String.Compare(strA.ToUpperInvariant(), strB.ToUpperInvariant(), StringComparison.Ordinal)

Essas comparações ainda são muito rápidas.

Ambos StringComparison.Ordinal e StringComparison.OrdinalIgnoreCase usam os valores binários diretamente e são mais adequados para comparação. Quando você não tiver certeza sobre suas configurações de comparação, use um desses dois valores. No entanto, como eles executam uma comparação byte por byte, eles não classificam por uma ordem de classificação linguística (como um dicionário inglês), mas por uma ordem de classificação binária. Os resultados podem parecer ímpares na maioria dos contextos se exibidos aos usuários.

A semântica ordinal é o padrão para sobrecargas de String.Equals que não incluem um argumento StringComparison (incluindo o operador de igualdade). Em qualquer caso, é recomendável que você chame uma sobrecarga que tenha um parâmetro StringComparison.

Operações da cadeia de caracteres que usam a cultura invariável

Comparações com a cultura invariável usam a CompareInfo propriedade retornada pela propriedade estática CultureInfo.InvariantCulture . Esse comportamento é o mesmo em todos os sistemas; ele converte todos os caracteres fora de seu intervalo no que acredita serem caracteres invariáveis equivalentes. Essa política pode ser útil para manter um conjunto de comportamentos de cadeia de caracteres entre culturas, mas geralmente fornece resultados inesperados.

As comparações que não diferenciam maiúsculas de minúsculas com a cultura invariável usam a propriedade CompareInfo estática retornada pela propriedade CultureInfo.InvariantCulture estática para informações de comparação também. As diferenças de maiúsculas e minúsculas entre esses caracteres convertidos são ignoradas.

Comparações que usam StringComparison.InvariantCulture e StringComparison.Ordinal funcionam de forma idêntica em cadeias de caracteres ASCII. No entanto, StringComparison.InvariantCulture toma decisões linguísticas que podem não ser apropriadas para cadeias de caracteres que precisam ser interpretadas como um conjunto de bytes. O CultureInfo.InvariantCulture.CompareInfo objeto faz com que o Compare método interprete determinados conjuntos de caracteres como equivalentes. Por exemplo, a seguinte equivalência é válida na cultura invariável:

InvariantCulture: a + ̊ = å

O caractere de LETRA A MINÚSCULA LATINA "a" (\u0061), quando está próximo ao CARACTERE DE ANEL SUPERIOR COMBINÁVEL "+ " ̊" (\u030a), é interpretado como a LETRA A MINÚSCULA LATINA COM O CARACTERE DE ANEL SUPERIOR "å" (\u00e5). Como mostra o exemplo a seguir, esse comportamento difere da comparação ordinal.

string separated = "\u0061\u030a";
string combined = "\u00e5";

Console.WriteLine($"Equal sort weight of {separated} and {combined} using InvariantCulture: {string.Compare(separated, combined, StringComparison.InvariantCulture) == 0}");

Console.WriteLine($"Equal sort weight of {separated} and {combined} using Ordinal: {string.Compare(separated, combined, StringComparison.Ordinal) == 0}");

// The example displays the following output:
//     Equal sort weight of a° and å using InvariantCulture: True
//     Equal sort weight of a° and å using Ordinal: False
Module Program
    Sub Main()
        Dim separated As String = ChrW(&H61) & ChrW(&H30A)
        Dim combined As String = ChrW(&HE5)

        Console.WriteLine("Equal sort weight of {0} and {1} using InvariantCulture: {2}",
                          separated, combined,
                          String.Compare(separated, combined, StringComparison.InvariantCulture) = 0)

        Console.WriteLine("Equal sort weight of {0} and {1} using Ordinal: {2}",
                          separated, combined,
                          String.Compare(separated, combined, StringComparison.Ordinal) = 0)

        ' The example displays the following output:
        '     Equal sort weight of a° and å using InvariantCulture: True
        '     Equal sort weight of a° and å using Ordinal: False
    End Sub
End Module

Ao interpretar nomes de arquivo, cookies ou qualquer outra coisa em que uma combinação como "å" possa aparecer, as comparações ordinais ainda oferecem o comportamento mais transparente e adequado.

Em equilíbrio, a cultura invariável tem poucas propriedades que a tornam útil para comparação. Ele faz a comparação de maneira linguisticamente relevante, o que o impede de garantir equivalência simbólica completa, mas não é a escolha para exibição em nenhuma cultura. Um dos poucos motivos para usar StringComparison.InvariantCulture para comparação é a persistência de dados ordenados para uma exibição que seja idêntica entre culturas. Por exemplo, se um arquivo de dados grande que contém uma lista de identificadores classificados para exibição acompanha um aplicativo, a adição a essa lista exige uma inserção com a inserção de estilo invariável.

Como escolher um membro de StringComparison

A tabela a seguir descreve o mapeamento do contexto semântico da cadeia de caracteres para um membro de enumeração StringComparison:

Dados Comportamento System.StringComparison correspondente

valor
Identificadores internos que diferenciam maiúsculas de minúsculas.

Identificadores que diferenciam maiúsculas e minúsculas nos padrões como XML e HTTP.

Configurações relacionadas à segurança que diferenciam maiúsculas de minúsculas.
Um identificador não linguístico, em que os bytes correspondem exatamente. Ordinal
Identificadores internos que não diferenciam maiúsculas de minúsculas.

Identificadores que não diferenciam maiúsculas e minúsculas em padrões como XML e HTTP.

Caminhos de arquivo.

Chaves do Registro e valores.

Variáveis de ambiente.

Identificadores de recurso (por exemplo, nomes de identificador).

Configurações relacionadas à segurança que não diferenciam maiúsculas de minúsculas.
Um identificador não linguístico, onde o caso é irrelevante. OrdinalIgnoreCase
Alguns dados persistentes e linguisticamente relevantes.

Exibição de dados linguísticos que exigem uma ordem de classificação fixa.
Dados culturalmente independentes que ainda são linguisticamente relevantes. InvariantCulture

- ou -

InvariantCultureIgnoreCase
Dados exibidos para o usuário.

A maioria das entradas do usuário.
Dados que exigem costumes linguísticos locais. CurrentCulture

- ou -

CurrentCultureIgnoreCase

Métodos comuns de comparação de cadeia de caracteres no .NET

As seções a seguir descrevem os métodos mais usados para comparação de cadeias de caracteres.

String.Compare

Interpretação padrão: StringComparison.CurrentCulture.

Sendo a operação mais central na interpretação de cadeias de caracteres, todas as instâncias dessas chamadas aos métodos devem ser examinadas para determinar se as cadeias de caracteres devem ser interpretadas de acordo com a cultura atual ou se devem ser interpretadas sem considerar o contexto cultural (simbolicamente). Normalmente, é o último caso, e deve-se usar uma comparação StringComparison.Ordinal como alternativa.

A classe System.Globalization.CompareInfo, que é retornada pela propriedade CultureInfo.CompareInfo, também inclui um método Compare que fornece um grande número de opções de correspondência (ordinal, ignorando espaço em branco, ignorando tipo kana, e assim por diante) por meio da enumeração de sinalizador CompareOptions.

String.CompareTo

Interpretação padrão: StringComparison.CurrentCulture.

Esse método no momento não oferece uma sobrecarga que especifica um tipo StringComparison. Normalmente, é possível converter esse método no formulário recomendado String.Compare(String, String, StringComparison) .

Os tipos que implementam as interfaces IComparable e IComparable<T> implementam esse método. Como ele não oferece a opção de um parâmetro StringComparison, os tipos de implementação geralmente permitem que o usuário especifique um StringComparer em seu construtor. O exemplo a seguir define uma FileName classe cujo construtor de classe inclui um StringComparer parâmetro. Esse StringComparer objeto é então usado no FileName.CompareTo método.

class FileName : IComparable
{
    private readonly StringComparer _comparer;

    public string Name { get; }

    public FileName(string name, StringComparer? comparer)
    {
        if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));

        Name = name;

        if (comparer != null)
            _comparer = comparer;
        else
            _comparer = StringComparer.OrdinalIgnoreCase;
    }

    public int CompareTo(object? obj)
    {
        if (obj == null) return 1;

        if (obj is not FileName)
            return _comparer.Compare(Name, obj.ToString());
        else
            return _comparer.Compare(Name, ((FileName)obj).Name);
    }
}
Class FileName
    Implements IComparable

    Private ReadOnly _comparer As StringComparer

    Public ReadOnly Property Name As String

    Public Sub New(name As String, comparer As StringComparer)
        If (String.IsNullOrEmpty(name)) Then Throw New ArgumentNullException(NameOf(name))

        Me.Name = name

        If comparer IsNot Nothing Then
            _comparer = comparer
        Else
            _comparer = StringComparer.OrdinalIgnoreCase
        End If
    End Sub

    Public Function CompareTo(obj As Object) As Integer Implements IComparable.CompareTo
        If obj Is Nothing Then Return 1

        If TypeOf obj IsNot FileName Then
            Return _comparer.Compare(Name, obj.ToString())
        Else
            Return _comparer.Compare(Name, DirectCast(obj, FileName).Name)
        End If
    End Function
End Class

String.Equals

Interpretação padrão: StringComparison.Ordinal.

A classe String permite que você teste a igualdade utilizando as sobrecargas de método estáticas ou de instância Equals, ou usando o operador de igualdade estático. O operador e as sobrecargas utilizam a comparação ordinal por padrão. No entanto, ainda é recomendável que você chame uma sobrecarga que especifique explicitamente o tipo StringComparison mesmo se você desejar executar uma comparação ordinal. Isso facilita a pesquisa de código para uma determinada interpretação da cadeia de caracteres.

String.ToUpper e String.ToLower

Interpretação padrão: StringComparison.CurrentCulture.

Tenha cuidado ao usar esses métodos String.ToUpper() e String.ToLower(), pois forçar uma cadeia de caracteres para maiúsculas ou minúsculas normalmente é usado como uma normalização pequena para comparar cadeias de caracteres independentemente de maiúsculas e minúsculas. Nesse caso, considere o uso de uma comparação que não diferencie maiúsculas de minúsculas.

Os String.ToUpperInvariant métodos e os métodos String.ToLowerInvariant também estão disponíveis. ToUpperInvariant é o modo padrão para normalizar maiúsculas e minúsculas. As comparações feitas usando StringComparison.OrdinalIgnoreCase são a composição de duas chamadas de maneira comportamental: chamar ToUpperInvariant em ambos os argumentos de cadeia de caracteres e fazer uma comparação usando StringComparison.Ordinal.

Também há sobrecargas disponíveis para converter para maiúsculas e minúsculas em uma cultura específica, passando um objeto CultureInfo que representa aquela cultura para o método.

Char.ToUpper e Char.ToLower

Interpretação padrão: StringComparison.CurrentCulture.

Os métodos Char.ToUpper(Char) e Char.ToLower(Char) funcionam de forma semelhante aos métodos String.ToUpper() e String.ToLower() descritos na seção anterior.

String.StartsWith e String.EndsWith

Interpretação padrão: StringComparison.CurrentCulture.

Por padrão, ambos os métodos executam uma comparação sensível à cultura. Em particular, eles podem ignorar caracteres que não são impressos.

String.IndexOf e String.LastIndexOf

Interpretação padrão: StringComparison.CurrentCulture.

Há uma falta de consistência em como as sobrecargas padrão desses métodos executam comparações. Todos os métodos String.IndexOf e String.LastIndexOf que incluem um parâmetro Char executam uma comparação ordinal, mas os métodos padrão String.IndexOf e String.LastIndexOf que incluem um parâmetro String executam uma comparação sensível à cultura.

Se você chamar o método String.IndexOf(String) ou String.LastIndexOf(String) e passar a ele uma cadeia de caracteres para localizar na instância atual, é recomendável que você chame uma sobrecarga que especifique explicitamente o tipo StringComparison. As sobrecargas que incluem um Char argumento não permitem que você especifique um StringComparison tipo.

MemoryExtensions.AsSpan.IndexOfAny e o tipo SearchValues<T>

O .NET 8 introduziu o SearchValues<T> tipo, que fornece uma solução otimizada para pesquisar conjuntos específicos de caracteres ou bytes em intervalos.

Se você estiver comparando uma cadeia de caracteres com um conjunto fixo de valores conhecidos repetidamente, considere usar o SearchValues<T>.Contains(T) método em vez de comparações encadeadas ou abordagens baseadas em LINQ. SearchValues<T> pode pré-compilar estruturas de pesquisa internas e otimizar a lógica de comparação com base nos valores fornecidos. Para ver os benefícios de desempenho, crie e armazene a instância em cache uma vez e reutilize-a SearchValues<string> para comparações:

using System.Buffers;

private static readonly SearchValues<string> Commands = SearchValues.Create(
    new[] { "start", "run", "go", "begin", "commence" },
    StringComparison.OrdinalIgnoreCase);

if (Commands.Contains(command))
{
    // ...
}

No .NET 9, SearchValues foi estendido para dar suporte à pesquisa de subcadeias de caracteres dentro de uma cadeia de caracteres maior. Para obter um exemplo, consulte SearchValues expansão.

Métodos que executam a comparação de cadeia de caracteres indiretamente

Alguns métodos que não são de cadeia de caracteres que têm a comparação de cadeia de caracteres como uma operação central usam o StringComparer tipo. A StringComparer classe inclui seis propriedades estáticas que retornam StringComparer instâncias cujos StringComparer.Compare métodos executam os seguintes tipos de comparações de cadeia de caracteres:

Array.Sort e Array.BinarySearch

Interpretação padrão: StringComparison.CurrentCulture.

Quando você armazena qualquer dado em uma coleção ou lê dados persistentes de um arquivo ou banco de dados para uma coleção, mudar a cultura atual pode invalidar os invariantes na coleção. O Array.BinarySearch método pressupõe que os elementos na matriz a ser pesquisada já estão classificados. Para classificar qualquer elemento de cadeia de caracteres na matriz, o Array.Sort método chama o String.Compare método para ordenar elementos individuais. O uso de um comparador sensível à cultura poderá ser perigoso se a cultura mudar entre o tempo em que a matriz é classificada e seu conteúdo for pesquisado. Por exemplo, no código a seguir, o armazenamento e a recuperação operam no comparador fornecido implicitamente pela Thread.CurrentThread.CurrentCulture propriedade. Se a cultura puder mudar entre as chamadas para StoreNames e DoesNameExist, e especialmente se os conteúdos da matriz forem mantidos em algum lugar entre as duas chamadas de método, a pesquisa binária poderá falhar.

// Incorrect
string[] _storedNames;

public void StoreNames(string[] names)
{
    _storedNames = new string[names.Length];

    // Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length);

    Array.Sort(_storedNames); // Line A
}

public bool DoesNameExist(string name) =>
    Array.BinarySearch(_storedNames, name) >= 0; // Line B
' Incorrect
Dim _storedNames As String()

Sub StoreNames(names As String())
    ReDim _storedNames(names.Length - 1)

    ' Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length)

    Array.Sort(_storedNames) ' Line A
End Sub

Function DoesNameExist(name As String) As Boolean
    Return Array.BinarySearch(_storedNames, name) >= 0 ' Line B
End Function

Uma variação recomendada aparece no exemplo a seguir, que usa o mesmo método de comparação ordinal (que não diferencia cultura) tanto para classificar quanto pesquisar a matriz. O código de alteração é refletido nas linhas rotuladas Line A e Line B nos dois exemplos.

// Correct
string[] _storedNames;

public void StoreNames(string[] names)
{
    _storedNames = new string[names.Length];

    // Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length);

    Array.Sort(_storedNames, StringComparer.Ordinal); // Line A
}

public bool DoesNameExist(string name) =>
    Array.BinarySearch(_storedNames, name, StringComparer.Ordinal) >= 0; // Line B
' Correct
Dim _storedNames As String()

Sub StoreNames(names As String())
    ReDim _storedNames(names.Length - 1)

    ' Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length)

    Array.Sort(_storedNames, StringComparer.Ordinal) ' Line A
End Sub

Function DoesNameExist(name As String) As Boolean
    Return Array.BinarySearch(_storedNames, name, StringComparer.Ordinal) >= 0 ' Line B
End Function

Se esses dados forem persistidos e movidos entre culturas, e a classificação for usada para apresentar esses dados ao usuário, você poderá considerar o uso de StringComparison.InvariantCulture, que funciona linguisticamente para melhores resultados para o usuário, embora não seja afetado por mudanças culturais. O exemplo a seguir modifica os dois exemplos anteriores para usar a cultura invariável para classificar e pesquisar a matriz.

// Correct
string[] _storedNames;

public void StoreNames(string[] names)
{
    _storedNames = new string[names.Length];

    // Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length);

    Array.Sort(_storedNames, StringComparer.InvariantCulture); // Line A
}

public bool DoesNameExist(string name) =>
    Array.BinarySearch(_storedNames, name, StringComparer.InvariantCulture) >= 0; // Line B
' Correct
Dim _storedNames As String()

Sub StoreNames(names As String())
    ReDim _storedNames(names.Length - 1)

    ' Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length)

    Array.Sort(_storedNames, StringComparer.InvariantCulture) ' Line A
End Sub

Function DoesNameExist(name As String) As Boolean
    Return Array.BinarySearch(_storedNames, name, StringComparer.InvariantCulture) >= 0 ' Line B
End Function

Exemplo de coleções: Hashtable construtor

As cadeias de caracteres de hash fornecem um segundo exemplo de uma operação que é afetada pela maneira como as cadeias de caracteres são comparadas.

O exemplo a seguir instancia um objeto Hashtable passando o objeto StringComparer, que é retornado pela propriedade StringComparer.OrdinalIgnoreCase. Como uma classe StringComparer derivada de StringComparer implementa a IEqualityComparer interface, seu GetHashCode método é usado para calcular o código hash de cadeias de caracteres na tabela de hash.

using System.IO;
using System.Collections;

const int InitialCapacity = 100;

Hashtable creationTimeByFile = new(InitialCapacity, StringComparer.OrdinalIgnoreCase);
string directoryToProcess = Directory.GetCurrentDirectory();

// Fill the hash table
PopulateFileTable(directoryToProcess);

// Get some of the files and try to find them with upper cased names
foreach (var file in Directory.GetFiles(directoryToProcess))
    PrintCreationTime(file.ToUpper());


void PopulateFileTable(string directory)
{
    foreach (string file in Directory.GetFiles(directory))
        creationTimeByFile.Add(file, File.GetCreationTime(file));
}

void PrintCreationTime(string targetFile)
{
    object? dt = creationTimeByFile[targetFile];

    if (dt is DateTime value)
        Console.WriteLine($"File {targetFile} was created at time {value}.");
    else
        Console.WriteLine($"File {targetFile} does not exist.");
}
Imports System.IO

Module Program
    Const InitialCapacity As Integer = 100

    Private ReadOnly s_creationTimeByFile As New Hashtable(InitialCapacity, StringComparer.OrdinalIgnoreCase)
    Private ReadOnly s_directoryToProcess As String = Directory.GetCurrentDirectory()

    Sub Main()
        ' Fill the hash table
        PopulateFileTable(s_directoryToProcess)

        ' Get some of the files and try to find them with upper cased names
        For Each File As String In Directory.GetFiles(s_directoryToProcess)
            PrintCreationTime(File.ToUpper())
        Next
    End Sub

    Sub PopulateFileTable(directoryPath As String)
        For Each file As String In Directory.GetFiles(directoryPath)
            s_creationTimeByFile.Add(file, IO.File.GetCreationTime(file))
        Next
    End Sub

    Sub PrintCreationTime(targetFile As String)
        Dim dt As Object = s_creationTimeByFile(targetFile)

        If TypeOf dt Is Date Then
            Console.WriteLine($"File {targetFile} was created at time {DirectCast(dt, Date)}.")
        Else
            Console.WriteLine($"File {targetFile} does not exist.")
        End If
    End Sub
End Module

Consulte também