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

O .NET fornece suporte extensivo 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 classificação e exibição de cadeias de caracteres. Mas classificar ou comparar cadeias de caracteres nem sempre é uma operação sensível à cultura. Por exemplo, as cadeias de caracteres 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, tags 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, baixo desempenho e, em alguns casos, problemas de segurança.

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

Recomendações para o uso de cadeia de caracteres

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

Gorjeta

Vários métodos relacionados a cadeias de caracteres executam a comparação. Exemplos incluem String.Equals, String.Compare, String.IndexOf, e String.StartsWith.

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

  • Não use sobrecargas que não especifiquem explícita ou implicitamente as regras de comparação de cadeia de caracteres para operações de cadeia de caracteres.
  • Não use operações de cadeia de caracteres baseadas StringComparison.InvariantCulture em na maioria dos casos. Uma das poucas exceções é quando você persiste dados linguisticamente significativos, mas culturalmente agnósticos.
  • Não use uma sobrecarga do String.Compare método ou CompareTo e teste um valor de retorno de zero para determinar se duas cadeias de caracteres são iguais.

Especificando comparações de cadeia de caracteres explicitamente

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

Membro StringComparison Description
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 invariante.
InvariantCultureIgnoreCase Executa uma comparação que não diferencia maiúsculas de minúsculas usando a cultura invariante.
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 substring 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 (aquelas que pesquisam um Char na instância de cadeia de caracteres) executam uma comparação ordinal, enquanto outras (aquelas que procuram uma cadeia de caracteres na instância de cadeia de caracteres) são sensíveis à 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 é 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 strings, ou se uma diferença de maiúsculas e minúsculas 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, porque 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. Deixa claro que a comparação ordinal é usada e que as diferenças de 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 cadeias de caracteres

A comparação de cadeias de caracteres é o coração de muitas operações relacionadas a cadeias de caracteres, particularmente a classificação e o teste de igualdade. Strings sort in a determined order: Se "my" aparecer antes de "string" em uma lista ordenada de strings, "my" deve comparar menor 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 das cordas é 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.

Nota

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 caracteres usados em operações de classificação e comparação para sistemas operacionais Windows, e a Tabela de Elementos de Agrupamento 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 macOS depende da versão dos componentes internacionais para bibliotecas Unicode instalados no sistema. Para obter informações sobre as versões da UTI e as versões Unicode que elas implementam, consulte Baixando a UTI.

No entanto, a avaliação de duas cadeias de caracteres para igualdade ou ordem de classificação não produz um único resultado correto; O resultado depende dos critérios usados para comparar as cordas. Em particular, comparações de cadeia que são ordinais ou que são baseadas nas convenções de invólucro e classificação da cultura atual ou da cultura invariante (uma cultura agnóstica de localidade baseada na língua inglesa) podem produzir 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 padrão Unicode.

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 cordas. As comparações baseadas na cultura atual usam a cultura ou localidade atual do thread. Se a cultura não for definida pelo usuário, ela será padronizada para a configuração do sistema operacional. Você deve sempre 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, a comparação e o comportamento de invólucro no .NET mudam 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 de execução muda sua cultura. Esse comportamento é intencional, mas permanece não óbvio para muitos desenvolvedores. O exemplo a seguir ilustra as diferenças na ordem de classificação entre as culturas inglesa ("en-US") e sueca ("sv-SE") dos EUA. 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 as mesmas que as comparações sensíveis à cultura, exceto que ignoram as maiúsculas e minúsculas conforme ditado pela cultura atual do tópico. Esse comportamento também pode se manifestar em ordens de classificação.

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

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

Bugs sutis e não tão sutis podem surgir quando dados de string não linguísticos são interpretados linguisticamente, ou quando dados de string de uma determinada cultura são interpretados usando as convenções de outra cultura. O exemplo canónico é o problema turco-I.

Para quase todos os alfabetos latinos, incluindo o inglês dos EUA, o caractere "i" (\u0069) é a versão minúscula do caractere "I" (\u0049). Esta regra de caixa rapidamente se torna o padrão para alguém que programação em tal cultura. No entanto, o alfabeto turco ("tr-TR") inclui um caractere "I com um ponto" "İ" (\u0130), que é a versão maiúscula de "i". O turco também inclui um caractere minúsculo "i without a dot", "ı" (\u0131), que maiúscula para "I". Este comportamento ocorre na cultura do Azerbaijão ("az") também.

Portanto, as suposições feitas sobre capitalizar "i" ou diminuir "eu" não são válidas entre todas as culturas. Se você usar as sobrecargas padrão para rotinas de comparação de cadeia de caracteres, elas estarão sujeitas a variância entre culturas. Se os dados a serem comparados não forem linguísticos, o uso das sobrecargas padrão pode produzir resultados indesejáveis, como ilustra a tentativa a seguir de realizar uma comparação sem diferenciação de maiúsculas e 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 pode 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 as 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

Neste caso, como "file:" deve ser interpretado como um identificador não linguístico e insensível à cultura, o código deve ser escrito como 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 de cadeia de caracteres ordinais

Especificar o StringComparison.Ordinal ou StringComparison.OrdinalIgnoreCase valor em uma chamada de método significa uma comparação não-linguística na qual as características das línguas naturais são ignoradas. Os métodos que são invocados com esses StringComparison valores baseiam as decisões de operação de cadeia de caracteres em comparações simples de bytes, em vez de tabelas de caixa ou equivalência parametrizadas por cultura. Na maioria dos casos, essa abordagem se adapta 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 em que cada byte de cada cadeia é comparado sem interpretação linguística; por exemplo, "windows" não corresponde a "windows". Esta é essencialmente uma chamada para a função de tempo de execução strcmp C. Use essa comparação quando o contexto ditar que as cadeias de caracteres devem ser correspondidas 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 incorporados (e outros caracteres não imprimíveis). Uma das diferenças mais claras entre comparação ordinal e sensível à cultura (incluindo comparações que usam a cultura invariante) diz respeito ao tratamento de caracteres nulos incorporados em uma cadeia de caracteres. Esses caracteres são ignorados quando você usa os String.Compare métodos e String.Equals para realizar comparações sensíveis à cultura (incluindo comparações que usam a cultura invariante). Como resultado, as cadeias de caracteres que contêm caracteres nulos incorporados podem ser consideradas iguais às cadeias de caracteres que não contêm. Caracteres não imprimíveis incorporados 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 incorporados, os métodos de pesquisa de cadeia de caracteres como String.Contains, String.EndsWith, String.IndexOf, String.LastIndexOf, e String.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 incorporados 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 mais conservadora. Estas comparações ignoram a maioria dos invólucros; por exemplo, "windows" corresponde a "windows". Ao lidar com caracteres ASCII, essa política é equivalente a StringComparison.Ordinal, exceto que ignora o invólucro ASCII usual. Portanto, qualquer caractere em [A, Z] (\u0041-\u005A) corresponde ao caractere correspondente em [a,z] (\u0061-\007A). O invólucro fora da gama ASCII usa as tabelas da cultura invariante. Portanto, a seguinte comparação:

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

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

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

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

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

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

Operações de cadeia de caracteres que usam a cultura invariante

As comparações com a cultura invariante usam a CompareInfo propriedade retornada pela propriedade estática CultureInfo.InvariantCulture . Este comportamento é o mesmo em todos os sistemas; ele traduz quaisquer caracteres fora de seu alcance para o que acredita serem caracteres invariantes 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 invariante também usam a propriedade static CompareInfo retornada pela propriedade static CultureInfo.InvariantCulture para obter informações de comparação. Quaisquer diferenças de maiúsculas e minúsculas entre esses caracteres traduzidos 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 têm de ser interpretadas como um conjunto de bytes. O CultureInfo.InvariantCulture.CompareInfo objeto faz com que o Compare método interprete certos conjuntos de caracteres como equivalentes. Por exemplo, a seguinte equivalência é válida sob a cultura invariante:

InvariantCulture: a + ̊ = å

O caractere "a" (\u0061), quando está ao lado do caractere "+ " ̊" (\u030a), é interpretado como o caractere "å" da LETRA PEQUENA LATINA A COM ANEL ACIMA (\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 {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
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 arquivos, 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.

No cômputo geral, a cultura invariante tem poucas propriedades que a tornam útil para comparação. Faz a comparação de uma forma linguisticamente relevante, o que a impede de garantir a plena equivalência simbólica, mas não é a escolha para exibição em qualquer cultura. Uma das poucas razões para usar StringComparison.InvariantCulture para comparação é persistir dados ordenados para uma exibição interculturalmente idêntica. Por exemplo, se um arquivo de dados grande que contém uma lista de identificadores classificados para exibição acompanha um aplicativo, adicionar a essa lista exigiria uma inserção com classificação de estilo invariante.

Escolhendo um membro StringComparison para sua chamada de método

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

Dados Comportamento System.StringComparison correspondente

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

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

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

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

Caminhos de arquivo.

Chaves e valores do Registro.

Variáveis de ambiente.

Identificadores de recursos (por exemplo, nomes de manipulação).

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

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

-or-

InvariantCultureIgnoreCase
Dados exibidos ao usuário.

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

-or-

CurrentCultureIgnoreCase

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

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

String.Compare

Interpretação por defeito: StringComparison.CurrentCulture.

Como a operação mais central para a interpretação de strings, todas as instâncias dessas chamadas de método devem ser examinadas para determinar se as strings devem ser interpretadas de acordo com a cultura atual ou dissociadas da cultura (simbolicamente). Normalmente, é o último, e uma StringComparison.Ordinal comparação deve ser usada em vez disso.

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

String.CompareTo

Interpretação por defeito: StringComparison.CurrentCulture.

Atualmente, esse método não oferece uma sobrecarga que especifica um StringComparison tipo. Normalmente, é possível converter este método para a forma recomendada String.Compare(String, String, StringComparison) .

Os tipos que implementam as interfaces e IComparable<T> implementam IComparable esse método. Como ele não oferece a opção de um StringComparison parâmetro, 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. Este 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 por defeito: StringComparison.Ordinal.

A String classe permite testar a igualdade chamando as sobrecargas do método estático ou de instância Equals ou usando o operador de igualdade estática. As sobrecargas e o operador usam a comparação ordinal por padrão. No entanto, ainda recomendamos que você chame uma sobrecarga que especifica explicitamente o StringComparison tipo, mesmo que você queira realizar uma comparação ordinal, o que facilita a pesquisa de código para uma determinada interpretação de cadeia de caracteres.

String.ToUpper e String.ToLower

Interpretação por defeito: StringComparison.CurrentCulture.

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

Os String.ToUpperInvariant e String.ToLowerInvariant métodos também estão disponíveis. ToUpperInvariant é a maneira padrão de normalizar o caso. As comparações feitas usando StringComparison.OrdinalIgnoreCase são comportamentalmente a composição de duas chamadas: chamando ToUpperInvariant ambos os argumentos de cadeia de caracteres e fazendo uma comparação usando StringComparison.Ordinal.

Sobrecargas também estão disponíveis para conversão em maiúsculas e minúsculas em uma cultura específica, passando um CultureInfo objeto que representa essa cultura para o método.

Char.ToUpper e Char.ToLower

Interpretação por defeito: StringComparison.CurrentCulture.

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

String.StartsWith e String.EndsWith

Interpretação por defeito: StringComparison.CurrentCulture.

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

String.IndexOf e String.LastIndexOf

Interpretação por defeito: StringComparison.CurrentCulture.

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

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

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

Alguns métodos não-string 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 por defeito: StringComparison.CurrentCulture.

Quando você armazena dados em uma coleção ou lê dados persistentes de um arquivo ou banco de dados em uma coleção, alternar a cultura atual pode invalidar as 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. Usar um comparador sensível à cultura pode ser perigoso se a cultura mudar entre o momento em que a matriz é classificada e seu conteúdo é pesquisado. Por exemplo, no código a seguir, o armazenamento e a recuperação operam no comparador que é fornecido implicitamente pela Thread.CurrentThread.CurrentCulture propriedade. Se a cultura puder mudar entre as chamadas para StoreNames e DoesNameExist, e especialmente se o conteúdo da matriz persistir 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 (insensível à cultura) para classificar e 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 persistentes e movidos entre culturas, e a classificação for usada para apresentar esses dados ao usuário, você pode considerar o uso StringComparison.InvariantCulturedo , que opera linguisticamente para uma melhor saída do usuário, mas não é afetado por mudanças na cultura. O exemplo a seguir modifica os dois exemplos anteriores para usar a cultura invariante 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: Construtor Hashtable

Hashing strings fornece 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 Hashtable objeto passando-lhe o StringComparer objeto que é retornado pela StringComparer.OrdinalIgnoreCase propriedade. Como uma classe StringComparer derivada implementa a IEqualityComparer interface, seu GetHashCode método é usado para calcular o código hash de cadeias de caracteres na tabela de StringComparer 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