Procedure consigliate per il confronto delle stringhe in .NET

.NET offre un ampio supporto per lo sviluppo di applicazioni localizzate e globalizzate e semplifica l'applicazione delle convenzioni relative alle impostazioni cultura correnti o alle impostazioni cultura specifiche quando si eseguono operazioni comuni come l'ordinamento e la visualizzazione delle stringhe. Tuttavia, l'ordinamento o il confronto delle stringhe non è sempre un'operazione sensibile alle impostazioni cultura. Ad esempio, le stringhe usate internamente da un'applicazione in genere devono essere gestite in modo identico in tutte le impostazioni cultura. Quando i dati di stringa indipendenti dalle impostazioni cultura, ad esempio i tag XML, i tag HTML, i nomi utente, i percorsi di file e i nomi degli oggetti di sistema, vengono interpretati come dati con distinzione delle impostazioni cultura, nel codice dell'applicazione possono verificarsi bug complessi, riduzioni delle prestazioni e, in alcuni casi, problemi di sicurezza.

Questo articolo esamina i metodi di ordinamento, confronto e utilizzo di maiuscole e minuscole nelle stringhe in .NET, offre delle raccomandazioni per selezionare il metodo di gestione delle stringhe appropriato e fornisce informazioni aggiuntive sui metodi di gestione delle stringhe.

Suggerimenti per l'uso delle stringhe

Quando si sviluppa con .NET, seguire queste indicazioni quando si confrontano stringhe.

Suggerimento

Vengono eseguiti confronti tra vari metodi correlati a stringhe. Ne sono esempi String.Equals, String.Compare, String.IndexOf e String.StartsWith.

Evitare le procedure seguenti quando si confrontano stringhe:

  • Non usare overload che non specificano in modo esplicito o implicito le regole di confronto tra stringhe per le operazioni di stringa.
  • Non usare operazioni di stringa basate sulla StringComparison.InvariantCulture maggior parte dei casi. Una delle poche eccezioni consiste nel rendere persistenti i dati linguistici significativi ma culturalmente indipendenti.
  • Non usare un overload del String.Compare metodo o CompareTo e verificare la presenza di un valore restituito pari a zero per determinare se due stringhe sono uguali.

Specifica esplicita per il confronto tra stringhe

Molti dei metodi di modifica delle stringhe in .NET sono di tipo overload. In genere, uno o più overload accettano le impostazioni predefinite, mentre altri accettano le impostazioni non predefinite, specificando invece una determinata procedura di confronto o modifica delle stringhe. La maggior parte dei metodi che non si basano sulle impostazioni predefinite include un parametro di tipo StringComparison, che è un'enumerazione che specifica in modo esplicito le regole per il confronto tra stringhe in base alle impostazioni cultura e alle maiuscole/minuscole. La tabella seguente descrive i membri dell'enumerazione StringComparison .

Membro StringComparison Descrizione
CurrentCulture Esegue un confronto con distinzione tra maiuscole e minuscole usando le impostazioni cultura correnti.
CurrentCultureIgnoreCase Esegue un confronto senza distinzione tra maiuscole e minuscole usando le impostazioni cultura correnti.
InvariantCulture Esegue un confronto con distinzione tra maiuscole e minuscole usando la lingua inglese.
InvariantCultureIgnoreCase Esegue un confronto senza distinzione tra maiuscole e minuscole usando la lingua inglese.
Ordinal Esegue un confronto ordinale.
OrdinalIgnoreCase Esegue un confronto ordinale senza distinzione tra maiuscole e minuscole.

Ad esempio, il metodo IndexOf , che restituisce l'indice di una sottostringa in un oggetto String che corrisponde a un carattere o a una stringa, ha nove overload:

È consigliabile selezionare un overload che non usa valori predefiniti, per i motivi seguenti:

  • Alcuni overload con parametri predefiniti (quelli che cercano Char nell'istanza della stringa) eseguono un confronto ordinale, mentre altri (quelli che cercano una stringa nell'istanza della stringa) applicano la distinzione delle impostazioni cultura. È difficile ricordare quale metodo usa quale valore predefinito e facile confondere gli overload.

  • La finalità del codice che si basa sui valori predefiniti per le chiamate al metodo non è chiara. Nell'esempio seguente, che si basa sulle impostazioni predefinite, è difficile sapere se lo sviluppatore ha effettivamente previsto un ordinale o un confronto linguistico di due stringhe o se una differenza tra maiuscole e minuscole tra url.Scheme e "https" potrebbe causare la restituzione falsedell'uguaglianza del test.

    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
    

In generale, è consigliabile chiamare un metodo che non si basa sulle impostazioni predefinite, perché rende la finalità del codice non ambigua. In questo modo, anche il codice diventa più leggibile ed è più facile eseguirne il debug e la manutenzione. L'esempio seguente riguarda le domande relative all'esempio precedente. Viene specificato che viene utilizzato il confronto ordinale e che le differenze tra maiuscole e minuscole vengono ignorate.

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

Dettagli sul confronto tra stringhe

Il confronto tra stringhe è la base di molte operazioni relative alle stringhe, in particolare l'ordinamento e il test di uguaglianza. L'ordinamento delle stringhe viene eseguito in un modo specifico: se "my" compare prima di "string" in un elenco ordinato di stringhe, nel confronto "my" deve essere minore o uguale a "string". Inoltre, il confronto definisce implicitamente l'uguaglianza. L'operazione di confronto restituisce zero per le stringhe che considera uguali. In altre parole, nessuna stringa viene considerata minore delle altre. Le operazioni più significative relative alle stringhe includono una o più delle seguenti procedure: confronto con un'altra stringa ed esecuzione di un'operazione di ordinamento definita correttamente.

Nota

È possibile scaricare le tabelle di ordinamento spessore, un set di file di testo che contengono informazioni sugli spessori dei caratteri usati nelle operazioni di ordinamento e confronto per i sistemi operativi Windows, e la tabella degli elementi delle regole di confronto Unicode predefinite, la versione più recente della tabella di ordinamento spessore per Linux e MacOS. La versione specifica della tabella di ordinamento spessore in Linux e macOS dipende dalla versione delle librerie International Components for Unicode installate nel sistema. Per informazioni sulle versioni ICU e sulle versioni Unicode implementate, vedere Downloading ICU (Download di ICU).

Tuttavia, la valutazione di due stringhe per l'uguaglianza o l'ordinamento non restituisce un singolo risultato corretto; il risultato dipende dai criteri usati per confrontare le stringhe. In particolare, i confronti tra stringhe ordinali o basati sulle convenzioni di uso di maiuscole e minuscole e di ordinamento delle impostazioni cultura correnti o delle impostazioni cultura inglese non dipendenti da paese/area geografica possono produrre risultati diversi.

I confronti di stringhe tramite versioni diverse di .NET oppure tramite .NET in sistemi operativi o versioni del sistema operativo diverse possono inoltre restituire risultati diversi. Per altre informazioni, vedere Stringhe e standard Unicode.

Confronti tra stringhe che usano le impostazioni cultura correnti

Un criterio prevede l'uso delle convenzioni delle impostazioni cultura correnti quando si confrontano le stringhe. I confronti basati sulle impostazioni cultura correnti usano le impostazioni cultura o le impostazioni locali correnti del thread. Se le impostazioni cultura non sono impostate dall'utente, per impostazione predefinita viene impostata l'impostazione del sistema operativo. È necessario usare sempre i confronti basati sulle impostazioni cultura correnti quando i dati sono linguisticamente rilevanti e quando riflettono un'interazione utente con distinzione delle impostazioni cultura.

Tuttavia, il comportamento di confronto e di utilizzo di maiuscole e minuscole in .NET cambia quando vengono modificate le impostazioni cultura. Ciò accade quando un'applicazione viene eseguita in un computer con impostazioni cultura diverse da quelle del computer in cui è stata sviluppata oppure quando il thread di esecuzione modifica le proprie impostazioni cultura. Questo comportamento è intenzionale, tuttavia resta poco chiaro per molti sviluppatori. L'esempio seguente illustra le differenze nell'ordinamento tra le impostazioni cultura della lingua inglese per gli Stati Uniti ("en-US") e di quella svedese ("sv-SE"). Si noti che le parole "ångström", "Windows" e "Visual Studio" vengono visualizzate in posizioni diverse nelle matrici di stringhe ordinate.

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

I confronti senza distinzione tra maiuscole e minuscole che usano le impostazioni cultura correnti sono uguali a quelli con distinzione delle impostazioni cultura, ma ignorano la distinzione tra maiuscole e minuscole come indicato dalle impostazioni cultura correnti del thread. Questo comportamento può manifestarsi anche negli ordinamenti.

I confronti che usano la semantica delle impostazioni cultura correnti sono i confronti predefiniti per i seguenti metodi:

In ogni caso, si consiglia di chiamare un overload con il parametro StringComparison per rendere chiaro lo scopo della chiamata al metodo.

È possibile che vengano generati bug complessi e meno complessi quando i dati non linguistici della stringa vengono interpretati linguisticamente oppure quando i dati della stringa di specifiche impostazioni cultura vengono interpretati usando le convenzioni di altre impostazioni cultura. L'esempio canonico è il problema della I turca.

Per quasi tutti gli alfabeti latini, incluso l'inglese (Stati Uniti), il carattere "i" (\u0069) corrisponde alla versione minuscola del carattere "I" (\u0049). Questa regola di utilizzo di maiuscole e minuscole diventa rapidamente l'impostazione predefinita per chi programma queste impostazioni cultura. Tuttavia, l'alfabeto turco ("tr-TR") include una "I con punto", "İ" (\u0130), che è la versione maiuscola di "i". In turco esiste anche un carattere minuscolo "i senza punto", "ı" (\u0131), la cui versione maiuscola è "I". Questo comportamento si verifica anche con le impostazioni cultura azerbaigiana ("az").

Di conseguenza, le ipotesi effettuate sulla capitalizzazione di "i" o sulla minuscola "I" non sono valide tra tutte le culture. Se si usano gli overload predefiniti per le routine di confronto tra stringhe, questi saranno soggetti a variazioni tra le diverse impostazioni cultura. Se i dati da confrontare non sono linguistici, l'uso degli overload predefiniti può produrre risultati indesiderati, come illustrato nel tentativo seguente di eseguire un confronto senza distinzione tra maiuscole e minuscole delle stringhe "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

Questo confronto può causare problemi significativi se le impostazioni cultura vengono usate inavvertitamente in impostazioni relative alla sicurezza, come nel seguente esempio. Una chiamata al metodo come IsFileURI("file:") restituisce true , se le impostazioni cultura correnti sono per la lingua inglese (Stati Uniti) oppure false se le impostazioni cultura correnti sono per la lingua turca. Quindi, nei sistemi turchi, qualcuno potrebbe aggirare le misure di sicurezza che bloccano l'accesso agli URI senza distinzione tra maiuscole e minuscole che iniziano con "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

In questo caso, poiché "file:" deve essere interpretato come identificatore non linguistico senza distinzione tra impostazioni cultura, il codice deve invece essere scritto come illustrato nell'esempio seguente:

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

Operazioni di stringa ordinale

La specifica del valore StringComparison.Ordinal o StringComparison.OrdinalIgnoreCase in una chiamata al metodo indica un confronto non linguistico in cui le funzionalità dei linguaggi naturali vengono ignorate. I metodi richiamati con questi valori StringComparison basano le decisioni relative alle operazioni di stringa su confronti di byte semplici invece che sull'utilizzo di maiuscole e minuscole o di tabelle di equivalenza parametrizzate dalle impostazioni cultura. In molti casi, questo approccio risulta più adatto per l'interpretazione desiderata delle stringhe e rende il codice più veloce e affidabile.

I confronti ordinali sono confronti tra stringhe in cui ogni byte di ogni stringa viene confrontata senza interpretazione linguistica; Ad esempio, "windows" non corrisponde a "Windows". Si tratta essenzialmente di una chiamata alla funzione strcmp di C Runtime. Usare questo confronto quando il contesto impone l'esatta corrispondenza delle stringhe o richiede un criterio di corrispondenza conservativo. Inoltre, il confronto ordinale è l'operazione di confronto più rapida perché non applica regole linguistiche quando determina un risultato.

Le stringhe in .NET possono contenere caratteri Null incorporati (e altri caratteri non stampati). Una delle differenze più evidenti tra il confronto ordinale e quello con distinzione delle impostazioni cultura (inclusi i confronti che usano la lingua inglese) riguarda la gestione dei caratteri null incorporati in una stringa. Questi caratteri vengono ignorati quando si usano i metodi String.Compare e String.Equals per eseguire confronti con distinzione delle impostazioni cultura (inclusi i confronti che usano la lingua inglese). Di conseguenza, le stringhe che contengono caratteri Null incorporati possono essere considerate uguali a stringhe che non lo fanno. I caratteri non stampati incorporati potrebbero essere ignorati ai fini dei metodi di confronto tra stringhe, ad esempio String.StartsWith.

Importante

Anche se i metodi di confronto tra stringhe ignorano i caratteri null incorporati, i metodi di ricerca di stringhe come String.Contains, String.EndsWith, String.IndexOf, String.LastIndexOfe String.StartsWith li prendono in considerazione.

Nell'esempio seguente viene eseguito un confronto con distinzione tra le impostazioni cultura della stringa "Aa" con una stringa simile che contiene diversi caratteri Null incorporati tra "A" e "a" e mostra come le due stringhe vengono considerate uguali:

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

Tuttavia, le stringhe non vengono considerate uguali quando si usa il confronto ordinale, come illustrato nell'esempio seguente:

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

I confronti ordinali senza distinzione tra maiuscole e minuscole rappresentano il secondo approccio più conservativo. Questi confronti ignorano quasi del tutto l'utilizzo di maiuscole e minuscole, ad esempio "windows" corrisponde a "Windows". Quando si usano i caratteri ASCII, questo criterio è equivalente a StringComparison.Ordinal, ma ignora il normale utilizzo di maiuscole e minuscole ASCII. Quindi, qualsiasi carattere in [A, Z] (\u0041-\u005A) corrisponde al carattere corrispondente in [a,z] (\u0061-\007A). L'utilizzo di maiuscole e minuscole al di fuori dell'intervallo ASCII usa le tabelle in lingua inglese. Pertanto, il confronto seguente:

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

è equivalente (ma più rapido) rispetto al confronto:

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

Questi confronti sono comunque molto rapidi.

StringComparison.Ordinal e StringComparison.OrdinalIgnoreCase usano direttamente i valori binari e sono i più adatti per la corrispondenza. Quando non si è certi delle impostazioni di confronto, usare uno di questi due valori. Tuttavia, poiché eseguono un confronto di byte per byte, non ordinano in base a un ordinamento linguistico (ad esempio un dizionario inglese) ma in base a un ordinamento binario. Se visualizzati dagli utenti, i risultati possono sembrare strani in molti contesti.

La semantica ordinale è l'impostazione predefinita per String.Equals gli overload che non includono un StringComparison argomento (incluso l'operatore di uguaglianza). In ogni caso, si consiglia di chiamare un overload con un parametro StringComparison .

Operazioni di stringa che usano impostazioni cultura non dipendenti da paese/area geografica

I confronti con la lingua inglese usano la proprietà CompareInfo restituita dalla proprietà statica CultureInfo.InvariantCulture . Questo comportamento è uguale in tutti i sistemi. Converte i caratteri al di fuori dell'intervallo in quelli che considera caratteri equivalenti in lingua inglese. Questo criterio può essere utile per gestire il comportamento di un set di stringhe nelle impostazioni cultura, ma spesso produce risultati imprevisti.

I confronti senza distinzione tra maiuscole e minuscole con la lingua inglese usano la proprietà statica CompareInfo restituita dalla proprietà statica CultureInfo.InvariantCulture anche per le informazioni sul confronto. Le differenze tra maiuscole e minuscole in questi caratteri convertiti vengono ignorate.

I confronti che usano StringComparison.InvariantCulture e StringComparison.Ordinal funzionano in modo identico nelle stringhe ASCII. Tuttavia, StringComparison.InvariantCulture prende decisioni linguistiche che potrebbero non essere appropriate per le stringhe che devono essere interpretate come set di byte. L'oggetto CultureInfo.InvariantCulture.CompareInfo fa sì che il metodo Compare interpreti alcuni set di caratteri come equivalenti. Ad esempio, la seguente equivalenza è valida in lingua inglese:

InvariantCulture: a + ̊ = å

La LETTERA MINUSCOLA LATINA Un carattere "a" (\u0061), quando si trova accanto al carattere "+ RING ABOVE" "+ " ̊" (\u030a), viene interpretato come LA LETTERA MINUSCOLA IN ALFABETO LATINO A CON ANELLO SOPRA il carattere "å" (\u00e5). Come mostrato nell'esempio seguente, questo comportamento è diverso dal confronto ordinale.

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

Quando si interpretano i nomi di file, i cookie o qualsiasi altro elemento che può contenere un carattere come "å", i confronti ordinali offrono il comportamento più trasparente e appropriato.

In caso di equilibrio, le impostazioni cultura invarianti hanno poche proprietà che lo rendono utile per il confronto. Esegue il confronto in modo linguistico rilevante, che impedisce di garantire l'equivalenza simbolica completa, ma non è la scelta per la visualizzazione in alcuna cultura. Uno dei pochi motivi per usare StringComparison.InvariantCulture per il confronto riguarda la possibilità di conservare i dati ordinati per visualizzarli in modo identico nelle varie impostazioni cultura. Ad esempio, se un file di dati di grandi dimensioni che contiene un elenco di identificatori ordinati per la visualizzazione viene associato a un'applicazione, per aggiungere elementi all'elenco è necessario l'inserimento con un ordinamento in lingua inglese.

Scelta di un membro StringComparison per la chiamata al metodo

Nella tabella seguente viene descritto il mapping dal contesto di stringa semantica a un StringComparison membro di enumerazione:

Dati Comportamento System.StringComparison corrispondente

Valore
Identificatori interni con distinzione tra maiuscole e minuscole.

Identificatori con distinzione tra maiuscole e minuscole in standard come XML e HTTP.

Impostazioni relative alla sicurezza con distinzione tra maiuscole e minuscole.
Identificatore non linguistico, con una corrispondenza esatta dei byte. Ordinal
Identificatori interni senza distinzione tra maiuscole e minuscole.

Identificatori senza distinzione tra maiuscole e minuscole in standard come XML e HTTP.

Percorsi di file.

Chiavi e valori del Registro di sistema.

Variabili di ambiente.

Identificatori di risorse (ad esempio, nomi di handle).

Impostazioni relative alla sicurezza senza distinzione tra maiuscole e minuscole.
Identificatore non linguistico, indipendente dalla distinzione tra maiuscole e minuscole. OrdinalIgnoreCase
Alcuni dati persistenti e linguisticamente rilevanti.

Visualizzazione di dati linguistici che richiedono un ordinamento fisso.
Dati indipendenti dalle impostazioni cultura, ma ancora linguisticamente rilevanti. InvariantCulture

-oppure-

InvariantCultureIgnoreCase
Dati visualizzati dall'utente.

La maggior parte dell'input utente.
Dati che richiedono personalizzazioni linguistiche locali. CurrentCulture

-oppure-

CurrentCultureIgnoreCase

Metodi comuni di confronto tra stringhe in .NET

Le sezioni seguenti descrivono i metodi più comuni per il confronto tra stringhe.

String.Compare

Interpretazione predefinita: StringComparison.CurrentCulture.

Poiché si tratta dell'operazione più importante per l'interpretazione della stringa, tutte le istanze di queste chiamate al metodo devono essere esaminate per determinare se le stringhe devono essere interpretate in base alle impostazioni cultura correnti o devono essere dissociate dalle impostazioni cultura (simbolicamente). In genere, è il secondo e deve essere usato un StringComparison.Ordinal confronto.

La classe System.Globalization.CompareInfo , restituita dalla proprietà CultureInfo.CompareInfo , include anche un metodo Compare che fornisce numerose opzioni di corrispondenza (ordinale, con esclusione degli spazi vuoti, con esclusione del tipo Kana e così via) mediante l'enumerazione flag CompareOptions .

String.CompareTo

Interpretazione predefinita: StringComparison.CurrentCulture.

Questo metodo non offre attualmente un overload che specifica un StringComparison tipo. In genere è possibile convertire questo metodo nel modulo consigliato String.Compare(String, String, StringComparison) .

I tipi che implementano le interfacce IComparable e IComparable<T> implementano questo metodo. Poiché non offre l'opzione di un StringComparison parametro, l'implementazione dei tipi spesso consente all'utente di specificare un StringComparer nel costruttore. L'esempio seguente definisce una classe FileName il cui costruttore include un parametro StringComparer . L'oggetto StringComparer viene quindi usato nel metodo FileName.CompareTo .

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

Interpretazione predefinita: StringComparison.Ordinal.

La classe String consente di eseguire un test di uguaglianza chiamando gli overload del metodo Equals dell'istanza o statici oppure usando l'operatore di uguaglianza statico. Gli overload e l'operatore usano il confronto ordinale per impostazione predefinita. Tuttavia, si consiglia di chiamare comunque un overload che specifichi esplicitamente il tipo StringComparison anche se si vuole eseguire un confronto ordinale; questo semplifica la ricerca di codice per una determinata interpretazione della stringa.

String.ToUpper e String.ToLower

Interpretazione predefinita: StringComparison.CurrentCulture.

Prestare attenzione quando si usano i String.ToUpper() metodi e String.ToLower() , perché forzare una stringa in lettere maiuscole o minuscole viene spesso usata come normalizzazione piccola per confrontare le stringhe indipendentemente dalle maiuscole e minuscole. In questo caso, valutare l'uso di un confronto senza distinzione tra maiuscole e minuscole.

Sono disponibili anche i metodi String.ToUpperInvariant e String.ToLowerInvariant . ToUpperInvariant è la modalità standard di normalizzazione dei caratteri maiuscoli e minuscoli. A livello di comportamento, i confronti eseguiti con StringComparison.OrdinalIgnoreCase corrispondono alla composizione di due chiamate: la chiamata a ToUpperInvariant in entrambi gli argomenti di stringa e l'esecuzione di un confronto con StringComparison.Ordinal.

Sono disponibili anche overload per la conversione in maiuscole e minuscole in determinate impostazioni cultura, passando un oggetto CultureInfo che rappresenta le specifiche impostazioni cultura al metodo.

Char.ToUpper e Char.ToLower

Interpretazione predefinita: StringComparison.CurrentCulture.

I Char.ToUpper(Char) metodi e Char.ToLower(Char) funzionano in modo analogo ai String.ToUpper() metodi e String.ToLower() descritti nella sezione precedente.

String.StartsWith e String.EndsWith

Interpretazione predefinita: StringComparison.CurrentCulture.

Per impostazione predefinita, entrambi i metodi eseguono un confronto con distinzione delle impostazioni cultura. In particolare, possono ignorare i caratteri non stampati.

String.IndexOf e String.LastIndexOf

Interpretazione predefinita: StringComparison.CurrentCulture.

C'è una mancanza di coerenza nel modo in cui gli overload predefiniti di questi metodi eseguono confronti. Tutti i metodi String.IndexOf e String.LastIndexOf che includono un parametro Char eseguono un confronto ordinale, ma i metodi predefiniti String.IndexOf e String.LastIndexOf che includono un parametro String eseguono un confronto con distinzione delle impostazioni cultura.

Se si chiama il metodo String.IndexOf(String) o String.LastIndexOf(String) e si passa una stringa da individuare nell'istanza corrente, si consiglia di chiamare un overload che specifichi esplicitamente il tipo StringComparison . Gli overload che includono un Char argomento non consentono di specificare un StringComparison tipo.

Metodi che eseguono indirettamente il confronto tra stringhe

Alcuni metodi non di tipo stringa in cui il confronto tra stringhe rappresenta l'operazione più importante usano il tipo StringComparer . La classe StringComparer include sei proprietà statiche che restituiscono istanze StringComparer i cui metodi StringComparer.Compare eseguono i tipi di confronto tra stringhe seguenti:

Array.Sort e Array.BinarySearch

Interpretazione predefinita: StringComparison.CurrentCulture.

Quando si archiviano dati in una raccolta o si leggono dati persistenti da un file o da un database nella raccolta, la modifica delle impostazioni cultura correnti può invalidare le invarianti nella raccolta. Il metodo Array.BinarySearch presuppone che gli elementi nella matrice da cercare siano già ordinati. Per ordinare qualsiasi elemento di tipo stringa nella matrice, il metodo Array.Sort chiama il metodo String.Compare per ordinare i singoli elementi. L'uso di un operatore di confronto con distinzione delle impostazioni cultura può essere pericoloso se le impostazioni cultura vengono modificate tra l'ordinamento della matrice e la ricerca dei contenuti. Nel codice seguente, ad esempio, le operazioni di archiviazione e recupero vengono eseguite sull'operatore di confronto fornito in modo implicito dalla proprietà Thread.CurrentThread.CurrentCulture . Se le impostazioni cultura possono cambiare tra le chiamate a StoreNames e DoesNameExiste, in particolare, se il contenuto della matrice viene conservato tra le due chiamate al metodo, è possibile che la ricerca binaria non riesca.

// 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

L'esempio seguente mostra una variazione consigliata che usa lo stesso metodo di confronto ordinale (senza distinzione delle impostazioni cultura) sia per l'ordinamento che per la ricerca nella matrice. Il codice modificato si riflette nelle righe identificate con Line A e Line B nei due esempi.

// 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 i dati vengono conservati e spostati nelle impostazioni cultura e l'ordinamento viene usato per presentare i dati all'utente, potrebbe esser opportuno usare StringComparison.InvariantCulture, che garantisce un output utente migliore dal punto di vista linguistico ma non viene interessato dalle modifiche nelle impostazioni cultura. L'esempio seguente modifica i due esempi precedenti per usare la lingua inglese per l'ordinamento e la ricerca della matrice.

// 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

Esempio di raccolte: costruttore Hashtable

L'esecuzione dell'hashing nelle stringhe fornisce un secondo esempio di operazione interessata dal modo in cui vengono confrontate le stringhe.

L'esempio seguente crea un'istanza di un oggetto Hashtable passando l'oggetto StringComparer restituito dalla proprietà StringComparer.OrdinalIgnoreCase . Poiché una classe StringComparer derivata da StringComparer implementa l'interfaccia IEqualityComparer , il metodo GetHashCode viene usato per calcolare il codice hash delle stringhe nella tabella 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

Vedi anche