Condividi tramite


Procedure consigliate per il confronto di stringhe in .NET

.NET offre un ampio supporto per lo sviluppo di applicazioni localizzate e globalizzate e semplifica l'applicazione delle convenzioni culturali della cultura corrente o di una cultura specifica quando si eseguono operazioni comuni, come l'ordinamento e la visualizzazione di stringhe. L'ordinamento o il confronto delle stringhe non è sempre un'operazione sensibile alle differenze culturali. Ad esempio, le stringhe utilizzate all'interno di un'applicazione devono essere gestite in modo identico in tutte le culture. Quando i dati stringa indipendenti dalla lingua, come tag XML, tag HTML, nomi utente, percorsi di file e nomi di oggetti di sistema, vengono interpretati come se fossero sensibili alle impostazioni culturali, il codice dell'applicazione può essere soggetto a bug sottili, prestazioni scarse e, in alcuni casi, problemi di sicurezza.

Questo articolo esamina i metodi di ordinamento, confronto e gestione delle maiuscole e minuscole di stringhe in .NET, presenta consigli per la selezione di un metodo di gestione delle stringhe appropriato e fornisce informazioni aggiuntive sui metodi di gestione delle stringhe.

Raccomandazioni per l'utilizzo delle stringhe

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

Suggerimento

Diversi metodi correlati a stringhe eseguono il confronto. Gli esempi includono String.Equals, String.Compare, String.IndexOfe String.StartsWith.

Evita le pratiche seguenti quando confronti stringhe:

  • Non usare overload che non specificano esplicitamente o implicitamente le regole di confronto delle stringhe nelle operazioni sulle stringhe.
  • 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 indipendenti dalla cultura.
  • Non usare un overload del String.Compare metodo o CompareTo e verificare se il valore restituito è zero per determinare se due stringhe sono uguali.

Suggerimento

Le regole di analisi del codice CA1307, CA1309 e CA1310 consentono di identificare i siti di chiamata in cui un operatore di confronto linguistico viene usato involontariamente. Per abilitarle ed evitare violazioni come errori di compilazione, impostare le proprietà seguenti nel file di progetto:

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

Specificare i confronti tra stringhe in modo esplicito

La maggior parte dei metodi di manipolazione delle stringhe in .NET è in overload. In genere, uno o più overload accettano impostazioni predefinite, mentre altre non accettano impostazioni predefinite e definiscono invece il modo preciso in cui le stringhe devono essere confrontate o modificate. 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 maiuscole/minuscole. Nella tabella seguente vengono descritti i membri dell'enumerazione StringComparison .

StringComparisonMembro 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 utilizzando la cultura invariante.
InvariantCultureIgnoreCase Esegue un confronto senza distinzione tra maiuscole e minuscole utilizzando la cultura invariante.
Ordinal Esegue un confronto ordinale.
OrdinalIgnoreCase Esegue un confronto ordinale che non fa distinzione tra maiuscole e minuscole.

Ad esempio, il IndexOf metodo , che restituisce l'indice di una sottostringa in un String oggetto 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 sovraccarichi con parametri di default (quelli che cercano un oggetto Char nell'istanza di stringa) eseguono un confronto ordinale, mentre altri (quelli che cercano una stringa nell'istanza della stringa) sono sensibili alla 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 inteso un confronto ordinale o un confronto linguistico di due stringhe, o se una differenza di maiuscole e minuscole tra url.Scheme e "https" potrebbe causare il test per l'uguaglianza a restituire 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
    

In generale, è consigliabile chiamare un metodo che non si basa sulle impostazioni predefinite, perché rende la finalità del codice non ambigua. Questo, a sua volta, rende il codice più leggibile e più facile da eseguire per il debug e la gestione. Nell'esempio seguente vengono affrontate le domande generate sull'esempio precedente. Rende chiaro che viene usato il confronto ordinale e che le differenze nel caso vengano 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 del confronto tra stringhe

Il confronto tra stringhe è il cuore di molte operazioni relative alle stringhe, in particolare l'ordinamento e la verifica dell'uguaglianza. Le stringhe vengono ordinate in un ordine determinato: se "my" viene visualizzato prima di "string" in un elenco ordinato di stringhe, "my" deve essere inferiore o uguale a "string". Inoltre, il confronto definisce in modo implicito l'uguaglianza. L'operazione di confronto restituisce zero per le stringhe ritenute uguali. Una buona interpretazione è che nessuna delle due stringhe è minore dell'altra. Le operazioni più significative che coinvolgono stringhe includono una o entrambe queste procedure: confronto con un'altra stringa ed esecuzione di un'operazione di ordinamento ben definita.

Annotazioni

È possibile scaricare le Tabelle dei Pesi di Ordinamento, un insieme di file di testo che contengono informazioni sui pesi dei caratteri utilizzati nelle operazioni di ordinamento e confronto per i sistemi operativi Windows, e la Tabella degli Elementi di Collazione Unicode Predefiniti, la versione più recente della tabella dei pesi di ordinamento per Linux e macOS. La versione specifica della tabella dei pesi di ordinamento in Linux e macOS dipende dalla versione delle librerie International Components for Unicode installate nel sistema. Per informazioni sulle versioni di ICU e sulle versioni Unicode implementate, vedere 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 di stringhe che sono ordinali o basati sulle convenzioni di maiuscole e minuscole della cultura corrente o della cultura invariante (una cultura indipendente dalle impostazioni locali basata sulla lingua inglese) possono produrre risultati diversi.

Inoltre, i confronti tra stringhe che usano versioni diverse di .NET o l'uso di .NET in sistemi operativi diversi o versioni del sistema operativo possono restituire risultati diversi. .NET usa la libreria International Components for Unicode (ICU) per confronti linguistici di stringhe in tutte le piattaforme supportate. Per altre informazioni, vedere Stringhe e lo Standard Unicode e la globalizzazione .NET e ICU.

Confronti tra stringhe che usano le impostazioni cultura correnti

Un criterio prevede l'uso delle convenzioni delle impostazioni cultura correnti durante il confronto delle stringhe. I confronti basati sulla cultura corrente utilizzano la cultura o il locale attuale del thread di esecuzione. Se la cultura non è definita dall'utente, per impostazione predefinita viene utilizzata quella del sistema operativo. Dovresti sempre usare confronti basati sulle impostazioni culturali correnti quando i dati sono linguisticamente pertinenti e quando riflettono interazioni con l'utente sensibili alle differenze culturali.

Tuttavia, il comportamento di confronto e di maiuscole e minuscole in .NET cambia quando cambiano le impostazioni culturali. Ciò si verifica quando un'applicazione viene eseguita in un computer con impostazioni cultura diverse rispetto al computer in cui è stata sviluppata l'applicazione o quando il thread in esecuzione modifica le impostazioni cultura. Questo comportamento è intenzionale, ma rimane non ovvio per molti sviluppatori. L'esempio seguente illustra le differenze nell'ordinamento delle culture inglese statunitense ("en-US") e 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 la cultura corrente sono uguali ai confronti sensibili alla cultura, tranne per il fatto che ignorano la distinzione tra maiuscole e minuscole come stabilito dalla cultura corrente del thread. Questo comportamento può manifestarsi anche in ordini di classificazione.

I confronti che usano la semantica delle impostazioni cultura correnti sono l'impostazione predefinita per i metodi seguenti:

In ogni caso, è consigliabile chiamare un overload con un StringComparison parametro per rendere chiara la finalità della chiamata al metodo.

I bug sottili e non così sottili possono emergere quando i dati delle stringhe non linguistici sono interpretati in modo linguistico o quando i dati stringa di una determinata cultura sono interpretati usando le convenzioni di un'altra cultura. L'esempio canonico è il problema Turkish-I.

Per quasi tutti gli alfabeti latini, inclusi l'inglese degli Stati Uniti, il carattere "i" (\u0069) è la versione minuscola del carattere "I" (\u0049). Questa regola di maiuscole e minuscole diventa rapidamente l'impostazione predefinita per un utente che programma in tali impostazioni cultura. Tuttavia, l'alfabeto turco ("tr-TR") include un carattere "I con un punto" "İ" (\u0130), che è la versione maiuscola di "i". Turco include anche un carattere minuscolo "i senza un punto", "ı" (\u0131), che maiuscole in "I". Questo comportamento si verifica anche nelle impostazioni cultura azere ("az").

Pertanto, i presupposti fatti riguardo alla maiuscola "I" o alla minuscola "i" non sono validi in tutte le culture. Se si utilizzano gli overload predefiniti per le routine di confronto tra stringhe, saranno soggetti a variazioni tra culture. 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 nelle impostazioni sensibili alla sicurezza, come nell'esempio seguente. Una chiamata al metodo come IsFileURI("file:") restituisce true se le impostazioni culturali correnti sono inglese statunitense, ma false se le impostazioni culturali correnti sono turche. Pertanto, 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 un identificatore non linguistico insensibile alla cultura, il codice dovrebbe invece essere scritto come 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

Se si specifica il StringComparison.Ordinal valore o StringComparison.OrdinalIgnoreCase in una chiamata al metodo, si intende un confronto non linguistico in cui le caratteristiche dei linguaggi naturali vengono ignorate. I metodi richiamati con questi StringComparison valori fondano le decisioni relative alle operazioni sulle stringhe su semplici confronti di byte, anziché su tabelle di maiuscole/minuscole o di equivalenza parametrizzati da fattori culturali. Nella maggior parte dei casi, questo approccio si adatta meglio all'interpretazione desiderata delle stringhe, rendendo il codice più veloce e affidabile.

I confronti ordinali sono confronti di 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 di runtime strcmp C. Usare questo confronto quando il contesto determina che le stringhe devono corrispondere esattamente o richiedono criteri di corrispondenza conservativi. Inoltre, il confronto ordinale è l'operazione di confronto più veloce perché non applica regole linguistiche quando si determina un risultato.

Un OrdinalIgnoreCase comparatore opera ancora su base carattere per carattere, ma elimina le differenze di maiuscolo e minuscolo durante l'esecuzione dell'operazione. In un OrdinalIgnoreCase operatore di confronto le coppie 'd' char e 'D' vengono confrontate come uguali, come le coppie 'á' char e 'Á'. Tuttavia, il carattere 'a' non accentato è considerato diverso da quello accentato 'á'.

Nella tabella seguente sono riportati alcuni esempi:

Stringa 1 Stringa 2 Ordinal Confronto OrdinalIgnoreCase Confronto
"dog" "dog" uguale uguale
"dog" "Dog" diverso da uguale
"resume" "résumé" diverso da diverso da

Unicode consente anche alle stringhe di avere diverse rappresentazioni in memoria. Ad esempio, un e-acute (é) può essere rappresentato in due modi possibili:

  • Un singolo carattere letterale 'é' (scritto anche come '\u00E9').
  • Carattere letterale non accentato 'e' seguito da un carattere '\u0301' modificatore di accento combinato.

Ciò significa che le quattro stringhe seguenti vengono visualizzate come "résumé", anche se le parti costitutive sono diverse. Le stringhe usano una combinazione di caratteri letterali 'é' o caratteri letterali non accentati 'e' più il modificatore di accento combinato '\u0301'.

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

In un operatore di confronto ordinale nessuno di queste stringhe viene confrontato tra loro. Ciò è dovuto al fatto che contengono sequenze char sottostanti diverse, anche se quando viene eseguito il rendering sullo schermo, hanno tutti lo stesso aspetto.

Quando si esegue un'operazione string.IndexOf(..., StringComparison.Ordinal) , il runtime cerca una corrispondenza di sottostringa esatta. I risultati sono i seguenti.

Console.WriteLine("resume".IndexOf('e', StringComparison.Ordinal)); // "resume": prints '1'
Console.WriteLine("r\u00E9sum\u00E9".IndexOf('e', StringComparison.Ordinal)); // "résumé": prints '-1'
Console.WriteLine("r\u00E9sume\u0301".IndexOf('e', StringComparison.Ordinal)); // "résumé": prints '5'
Console.WriteLine("re\u0301sum\u00E9".IndexOf('e', StringComparison.Ordinal)); // "résumé": prints '1'
Console.WriteLine("re\u0301sume\u0301".IndexOf('e', StringComparison.Ordinal)); // "résumé": prints '1'
Console.WriteLine("resume".IndexOf('e', StringComparison.OrdinalIgnoreCase)); // "resume": prints '1'
Console.WriteLine("r\u00E9sum\u00E9".IndexOf('e', StringComparison.OrdinalIgnoreCase)); // "résumé": prints '-1'
Console.WriteLine("r\u00E9sume\u0301".IndexOf('e', StringComparison.OrdinalIgnoreCase)); // "résumé": prints '5'
Console.WriteLine("re\u0301sum\u00E9".IndexOf('e', StringComparison.OrdinalIgnoreCase)); // "résumé": prints '1'
Console.WriteLine("re\u0301sume\u0301".IndexOf('e', StringComparison.OrdinalIgnoreCase)); // "résumé": prints '1'
Sub IndexOfExample()
    Console.WriteLine("resume".IndexOf("e"c, StringComparison.Ordinal)) ' "resume": prints '1'
    Console.WriteLine(("r" & ChrW(&HE9) & "sum" & ChrW(&HE9)).IndexOf("e"c, StringComparison.Ordinal)) ' "résumé": prints '-1'
    Console.WriteLine(("r" & ChrW(&HE9) & "sume" & ChrW(&H301)).IndexOf("e"c, StringComparison.Ordinal)) ' "résumé": prints '5'
    Console.WriteLine(("re" & ChrW(&H301) & "sum" & ChrW(&HE9)).IndexOf("e"c, StringComparison.Ordinal)) ' "résumé": prints '1'
    Console.WriteLine(("re" & ChrW(&H301) & "sume" & ChrW(&H301)).IndexOf("e"c, StringComparison.Ordinal)) ' "résumé": prints '1'
    Console.WriteLine("resume".IndexOf("e"c, StringComparison.OrdinalIgnoreCase)) ' "resume": prints '1'
    Console.WriteLine(("r" & ChrW(&HE9) & "sum" & ChrW(&HE9)).IndexOf("e"c, StringComparison.OrdinalIgnoreCase)) ' "résumé": prints '-1'
    Console.WriteLine(("r" & ChrW(&HE9) & "sume" & ChrW(&H301)).IndexOf("e"c, StringComparison.OrdinalIgnoreCase)) ' "résumé": prints '5'
    Console.WriteLine(("re" & ChrW(&H301) & "sum" & ChrW(&HE9)).IndexOf("e"c, StringComparison.OrdinalIgnoreCase)) ' "résumé": prints '1'
    Console.WriteLine(("re" & ChrW(&H301) & "sume" & ChrW(&H301)).IndexOf("e"c, StringComparison.OrdinalIgnoreCase)) ' "résumé": prints '1'
End Sub

Le routine di ricerca e confronto ordinali non sono mai interessate dall'impostazione cultura del thread corrente.

Le stringhe in .NET possono contenere caratteri Null incorporati (e altri caratteri non stampati). Una delle differenze più chiare tra confronto ordinale e sensibile alla cultura (inclusi i confronti che usano la cultura invariante) 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 sensibili alle impostazioni cultura (inclusi i confronti che usano le impostazioni cultura invarianti). Di conseguenza, le stringhe che contengono caratteri Null incorporati possono essere considerate uguali alle stringhe che non lo fanno. I caratteri non stampati incorporati potrebbero essere ignorati allo scopo di metodi di confronto di 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, ad String.Containsesempio , String.EndsWithString.IndexOf, String.LastIndexOf, e String.StartsWith non .

Nell'esempio seguente viene eseguito un confronto sensibile alle 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 l'approccio più conservativo successivo. Questi confronti ignorano la maggior parte delle maiuscole e minuscole; Ad esempio, "windows" corrisponde a "Windows". Quando si gestiscono caratteri ASCII, questo criterio è equivalente a StringComparison.Ordinal, ad eccezione del fatto che ignora la consueta distinzione tra maiuscole e minuscole ASCII. Pertanto, qualsiasi carattere in [A, Z] (\u0041-\u005A) corrisponde al carattere corrispondente in [a,z] (\u0061-\007A). La conversione delle maiuscole e minuscole al di fuori dell'intervallo ASCII utilizza le tabelle della cultura invariante. Di conseguenza, il confronto seguente:

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

equivale a (ma più veloce di) questo confronto:

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

Questi confronti sono ancora molto veloci.

Sia StringComparison.Ordinal che StringComparison.OrdinalIgnoreCase usano direttamente i valori binari e sono 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. I risultati potrebbero risultare strani nella maggior parte dei contesti se visualizzati agli utenti.

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

Confronti di stringhe linguistiche

Le routine di ricerca e confronto linguistico scompongono una stringa in elementi di confronto ed eseguono ricerche o confronti su questi elementi. Non esiste necessariamente una corrispondenza 1:1 tra i caratteri di una stringa e i suoi elementi di collazione costitutivi. Ad esempio, una stringa di lunghezza 2 può essere costituita da un solo elemento delle regole di confronto. Quando due stringhe vengono confrontate con consapevolezza linguistica, il comparatore verifica se gli elementi di confronto delle due stringhe hanno lo stesso significato semantico, anche se i caratteri letterali della stringa sono diversi.

Si consideri la stringa "résumé" e le quattro diverse rappresentazioni descritte nella sezione precedente. La tabella seguente mostra ogni rappresentazione suddivisa nei relativi elementi delle regole di confronto.

String Come elementi di confronto
"r\u00E9sum\u00E9" "r" + "\u00E9" + "s" + "u" + "m" + "\u00E9"
"r\u00E9sume\u0301" "r" + "\u00E9" + "s" + "u" + "m" + "e\u0301"
"re\u0301sum\u00E9" "r" + "e\u0301" + "s" + "u" + "m" + "\u00E9"
"re\u0301sume\u0301" "r" + "e\u0301" + "s" + "u" + "m" + "e\u0301"

Un elemento delle regole di confronto corrisponde in modo libero a ciò che i lettori considerano come un singolo carattere o un cluster di caratteri. È concettualmente simile a un cluster grapheme , ma comprende un ombrello leggermente più grande.

In un operatore di confronto linguistico, le corrispondenze esatte non sono necessarie. Gli elementi delle regole di confronto vengono invece confrontati in base al significato semantico. Ad esempio, un operatore di confronto linguistico considera le sottostringhe "\u00E9" e "e\u0301" come uguali poiché entrambe significano semanticamente "una 'e' minuscola con accento acuto". In questo modo, il metodo IndexOf può trovare la sottostringa "e\u0301" all'interno di una stringa più grande che contiene la sottostringa semanticamente equivalente "\u00E9", come illustrato nell'esempio di codice seguente.

Console.WriteLine("r\u00E9sum\u00E9".IndexOf("e")); // "résumé": prints '-1' (not found)
Console.WriteLine("r\u00E9sum\u00E9".IndexOf("\u00E9")); // "résumé": prints '1'
Console.WriteLine("\u00E9".IndexOf("e\u0301")); // prints '0'
Sub IndexOfStringExample()
    Console.WriteLine(("r" & ChrW(&HE9) & "sum" & ChrW(&HE9)).IndexOf("e")) ' "résumé": prints '-1' (not found)
    Console.WriteLine(("r" & ChrW(&HE9) & "sum" & ChrW(&HE9)).IndexOf(ChrW(&HE9).ToString())) ' "résumé": prints '1'
    Console.WriteLine(ChrW(&HE9).ToString().IndexOf("e" & ChrW(&H301))) ' prints '0'
End Sub

Di conseguenza, due stringhe di lunghezze diverse possono essere confrontate come uguali se viene usato un confronto linguistico. I chiamanti devono prestare attenzione a non creare logica di casi particolari che gestisce la lunghezza della stringa in tali scenari.

Le routine di ricerca e confronto sensibili alla cultura sono una forma speciale di routine linguistiche per la ricerca e il confronto. Nell'ambito di un comparer sensibile alla cultura, il concetto di elemento di confronto viene esteso per includere informazioni specifiche della cultura specificata.

Ad esempio, nell'alfabeto ungherese, quando i due caratteri <dz> appaiono consecutivamente, vengono considerati una lettera unica distinta da <d> o <z>. Ciò significa che quando <dz> viene visto in una stringa, un comparatore sensibile alla cultura ungherese lo considera come un singolo elemento di confronto.

String Come elementi di collazione Osservazioni:
"endz" "e" + "n" + "d" + "z" (uso di un operatore di confronto linguistico standard)
"endz" "e" + "n" + "dz" (utilizzando un comparer sensibile alle impostazioni culturali ungheresi)

Quando si utilizza un comparatore compatibile con la cultura ungherese, la stringa "endz"non termina con la sottostringa "z", perché <dz> e <z> sono considerati elementi di confronto con significati semantici diversi.

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

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

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

Annotazioni

  • Comportamento: i comparatori linguistici e consapevoli delle differenze culturali possono subire modifiche comportamentali di tanto in tanto. Sia l'ICU che la vecchia struttura NLS di Windows vengono aggiornate per tenere conto del cambiamento delle lingue mondiali. Per ulteriori informazioni, vedere l'articolo del blog Informazioni locali (cultura) modifiche dei dati. Il comportamento dell'operatore di confronto ordinale non cambierà mai perché esegue una ricerca e un confronto bit per bit esatti. Tuttavia, il comportamento dell'operatore di confronto OrdinalIgnoreCase può cambiare man mano che Unicode cresce per includere più set di caratteri e corregge le omissioni nei dati di maiuscole e minuscole esistenti.
  • Utilizzo: I confrontatori linguistici StringComparison.InvariantCulture e StringComparison.InvariantCultureIgnoreCase sono confrontatori che non tengono conto delle impostazioni culturali. Ovvero, questi confrontatori comprendono concetti come il carattere accentato é che ha più rappresentazioni sottostanti possibili e che tutte queste rappresentazioni devono essere considerate uguali. Tuttavia, i comparer linguistici non sensibili alle impostazioni culturali non conterranno una gestione speciale per <dz> come distinto da <d> o <z>, come illustrato in precedenza. Inoltre, non gestiranno in modo speciale caratteri come l'Eszett tedesco (ß).

.NET offre anche la modalità di globalizzazione invariante. Questa modalità di consenso esplicito disabilita i percorsi di codice che gestiscono le routine di ricerca linguistica e confronto. In questa modalità, tutte le operazioni usano comportamenti Ordinal o OrdinalIgnoreCase , indipendentemente dall'argomento CultureInfo fornito StringComparison dal chiamante. Per altre informazioni, vedere Opzioni di configurazione del runtime per la globalizzazione e la modalità invariante globalizzazione di .NET Core.

Operazioni sulle stringhe che usano la cultura invariante

I confronti con la cultura invariante usano la proprietà CompareInfo restituita dalla proprietà statica CultureInfo.InvariantCulture. Questo comportamento è lo stesso in tutti i sistemi; converte tutti i caratteri al di fuori del suo intervallo in ciò che ritiene siano caratteri invarianti equivalenti. Questo criterio può essere utile per mantenere un set di comportamenti di stringa tra le impostazioni cultura, ma spesso fornisce risultati imprevisti.

I confronti senza distinzione tra maiuscole e minuscole con la cultura invariante usano anche la proprietà statica CompareInfo restituita dalla proprietà statica CultureInfo.InvariantCulture per le informazioni di confronto. Le differenze tra maiuscole e minuscole tra questi caratteri tradotti vengono ignorate.

Confronti che usano StringComparison.InvariantCulture e StringComparison.Ordinal funzionano allo stesso modo sulle stringhe ASCII. Tuttavia, StringComparison.InvariantCulture prende decisioni linguistiche che potrebbero non essere appropriate per le stringhe che devono essere interpretate come un set di byte. L'oggetto CultureInfo.InvariantCulture.CompareInfo rende il Compare metodo interpretare determinati set di caratteri come equivalenti. Ad esempio, la seguente equivalenza è valida nella cultura invariante:

InvariantCulture: a + ̊ = å

Il carattere LETTERA MINUSCOLA LATINA A "a" (\u0061), quando si trova accanto al carattere ANELLO COMBINATORE SOPRA "+ ̊" (\u030a), viene interpretato come il carattere LETTERA MINUSCOLA LATINA A CON ANELLO SOPRA "å" (\u00e5). Come illustrato nell'esempio seguente, questo comportamento è diverso dal confronto ordinale.

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

Quando si interpretano nomi di file, cookie o qualsiasi altra opzione in cui può essere visualizzata una combinazione come "å", i confronti ordinali offrono comunque il comportamento più trasparente e appropriato.

Nel complesso, la cultura invariante ha poche proprietà che la rendono utile per il confronto. Esegue un 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 è rendere persistenti i dati ordinati per una visualizzazione cross-culturalmente identica. Ad esempio, se un file di dati di grandi dimensioni che contiene un elenco di identificatori ordinati per la visualizzazione accompagna un'applicazione, l'aggiunta a questo elenco richiede un inserimento con ordinamento invariante.

Come scegliere un membro StringComparison

Nella tabella seguente viene descritto il mapping dal contesto semantico di stringhe a un membro di enumerazione StringComparison.

Dati Comportamento System.StringComparison corrispondente

valore
Identificatori interni sensibili alla distinzione tra maiuscole e minuscole.

Identificatori con distinzione tra maiuscole e minuscole negli standard, ad esempio XML e HTTP.

Impostazioni di sicurezza sensibili alle maiuscole.
Identificatore non linguistico, in cui i byte corrispondono esattamente. Ordinal
Identificatori interni insensibili alle maiuscole.

Identificatori senza distinzione tra maiuscole e minuscole negli standard, ad esempio XML e HTTP.

Percorsi di file.

Chiavi e valori del Registro di sistema.

variabili di ambiente.

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

Impostazioni relative alla sicurezza senza distinzione tra maiuscole e minuscole.
Identificatore non linguistico, in cui la distinzione tra maiuscole e minuscole è irrilevante. OrdinalIgnoreCase
Alcuni dati persistenti e pertinenti in modo linguistico.

Visualizzazione di dati linguistici che richiedono un ordinamento fisso.
Dati indipendenti dalla cultura che sono ancora rilevanti in modo linguistico. InvariantCulture

oppure

InvariantCultureIgnoreCase
Dati visualizzati all'utente.

La maggior parte degli input utente.
Dati che richiedono usi linguistici locali. CurrentCulture

oppure

CurrentCultureIgnoreCase

Implicazioni per la sicurezza

Se l'app usa stringhe API per il filtraggio o il controllo degli accessi, usa confronti ordinali. I confronti linguistici basati sulle impostazioni cultura correnti possono produrre risultati imprevisti che variano in base alla piattaforma e alle impostazioni locali. I modelli di codice come i seguenti potrebbero essere soggetti a exploit di sicurezza:

//
// THIS SAMPLE CODE IS INCORRECT.
// DO NOT USE IT IN PRODUCTION.
//
bool ContainsHtmlSensitiveCharacters(string input)
{
    if (input.IndexOf("<") >= 0) { return true; }
    if (input.IndexOf("&") >= 0) { return true; }
    return false;
}
'
' THIS SAMPLE CODE IS INCORRECT.
' DO NOT USE IT IN PRODUCTION.
'
Function ContainsHtmlSensitiveCharacters(input As String) As Boolean
    If input.IndexOf("<") >= 0 Then Return True
    If input.IndexOf("&") >= 0 Then Return True
    Return False
End Function

Poiché il string.IndexOf(string) metodo usa una ricerca linguistica per impostazione predefinita, è possibile che una stringa contenga un valore letterale '<' o '&' un carattere e che string.IndexOf(string) restituisca -1, a indicare che la sottostringa di ricerca non è stata trovata. Le regole di analisi del codice CA1307 e CA1309 contrassegnano tali siti di chiamata e avvisano lo sviluppatore che si è verificato un potenziale problema.

Metodi comuni di confronto tra stringhe in .NET

Nelle sezioni seguenti vengono descritti i metodi più comunemente usati per il confronto tra stringhe.

String.Compare

Interpretazione predefinita: StringComparison.CurrentCulture.

Poiché è l'operazione più fondamentale nell'interpretazione delle stringhe, tutte le istanze di queste chiamate al metodo devono essere esaminate per determinare se le stringhe devono essere interpretate in base alla cultura corrente o dissociate dalla cultura (simbolicamente). In genere, è quest'ultimo e dovrebbe essere usato invece un StringComparison.Ordinal confronto.

La System.Globalization.CompareInfo classe , restituita dalla CultureInfo.CompareInfo proprietà , include anche un Compare metodo che fornisce un numero elevato di opzioni corrispondenti (ordinale, ignorando lo spazio vuoto, ignorando il tipo kana e così via) tramite l'enumerazione CompareOptions flag.

String.CompareTo

Interpretazione predefinita: StringComparison.CurrentCulture.

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

I tipi che implementano le IComparable interfacce e IComparable<T> implementano questo metodo. Poiché non offre l'opzione di un StringComparison parametro, le implementazioni dei tipi spesso consentono all'utente di specificare un StringComparer parametro nel loro costruttore. Nell'esempio seguente viene definita una FileName classe il cui costruttore di classe include un StringComparer parametro . Questo StringComparer oggetto viene quindi utilizzato nel FileName.CompareTo metodo .

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 String classe consente di verificare l'uguaglianza chiamando gli overload del metodo statico o dell'istanza Equals oppure usando l'operatore di uguaglianza statica. Gli sovraccarichi e l'operatore usano il confronto ordinale per impostazione predefinita. Tuttavia, è comunque consigliabile chiamare un overload che specifichi in modo esplicito il StringComparison tipo anche se si vuole eseguire un confronto ordinale. In questo modo è più semplice cercare il codice per una determinata interpretazione di stringa.

String.ToUpper e String.ToLower

Interpretazione predefinita: StringComparison.CurrentCulture.

Prestare attenzione quando si usano i metodi String.ToUpper() e String.ToLower(), poiché forzare una stringa in maiuscolo o minuscolo viene spesso usato come una piccola normalizzazione per confrontare le stringhe indipendentemente dalle maiuscole e minuscole. In tal caso, prendere in considerazione l'uso di un confronto senza distinzione tra maiuscole e minuscole.

Sono disponibili anche i String.ToUpperInvariant metodi e String.ToLowerInvariant . ToUpperInvariant è il modo standard per normalizzare il caso. I confronti effettuati con StringComparison.OrdinalIgnoreCase sono la composizione comportamentale di due chiamate: la chiamata ToUpperInvariant a entrambi gli argomenti stringa e l'esecuzione di un confronto usando StringComparison.Ordinal.

Sono disponibili anche sovraccarichi per la conversione in lettere maiuscole e minuscole in una cultura specifica, passando un oggetto CultureInfo che rappresenta tale 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 (quando il primo parametro è un string), oppure StringComparison.Ordinal (quando il primo parametro è un char).

Esiste un'incoerenza nel modo in cui gli overload predefiniti di questi metodi eseguono confronti. Gli overload che accettano un parametro eseguono un char confronto ordinale, ma gli overload che accettano un string parametro eseguono un confronto sensibile alle differenze culturali e possono ignorare i caratteri non stampabili.

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 le comparazioni. Tutti i metodi String.IndexOf e String.LastIndexOf che includono un parametro Char eseguono un confronto ordinale, ma i metodi String.IndexOf e String.LastIndexOf predefiniti che includono un parametro String eseguono un confronto sensibile alle impostazioni culturali.

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

String.Contains

Interpretazione predefinita: StringComparison.Ordinal.

A differenza di String.IndexOf, il metodo String.Contains usa un confronto ordinale per impostazione predefinita sia per gli overload char che per quelli string. Tuttavia, è comunque necessario passare un argomento esplicito StringComparison quando la finalità è importante, per rendere chiaro il comportamento nel sito di chiamata.

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

.NET 8 ha introdotto il SearchValues<T> tipo , che offre una soluzione ottimizzata per la ricerca di set specifici di caratteri o byte all'interno di intervalli.

Se si confronta ripetutamente una stringa con un set fisso di valori noti, è consigliabile usare il SearchValues<T>.Contains(T) metodo anziché confronti concatenati o approcci basati su LINQ. SearchValues<T> può precompilare le strutture di ricerca interne e ottimizzare la logica di confronto in base ai valori specificati. Per visualizzare i vantaggi delle prestazioni, creare e memorizzare nella cache l'istanza SearchValues<string> una sola volta, quindi riutilizzarla per i confronti:

using System.Buffers;

namespace ExampleCode;

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

    void ProcessCommand(string command)
    {
        if (Commands.Contains(command))
        {
            // ...
        }
    }
}
Imports System.Buffers

Namespace ExampleCode
    Partial Friend Class DemoCode

        Private Shared ReadOnly Commands As SearchValues(Of String) =
            SearchValues.Create(
                {"start", "run", "go", "begin", "commence"},
                StringComparison.OrdinalIgnoreCase)

        Sub ProcessCommand(command As String)
            If Commands.Contains(command) Then
                ' ...
            End If
        End Sub

    End Class
End Namespace

In .NET 9 SearchValues è stato esteso per supportare la ricerca di sottostringhe all'interno di una stringa più grande. Per un esempio, vedere SearchValues Espansione.

Metodi che eseguono il confronto di stringhe indirettamente

Alcuni metodi non stringa con confronto tra stringhe come operazione centrale usano il StringComparer tipo . La StringComparer classe include sei proprietà statiche che restituiscono StringComparer istanze i cui StringComparer.Compare metodi 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 in una raccolta, il cambio delle impostazioni cultura correnti può invalidare gli invarianti nella raccolta. Il Array.BinarySearch metodo presuppone che gli elementi nella matrice in cui eseguire la ricerca siano già ordinati. Per ordinare qualsiasi elemento stringa nella matrice, il Array.Sort metodo chiama il String.Compare metodo per ordinare singoli elementi. L'uso di un operatore di confronto sensibile alle impostazioni cultura può essere pericoloso se le impostazioni cultura cambiano tra il momento in cui la matrice viene ordinata e il relativo contenuto vengono cercati. Nel codice seguente, ad esempio, l'archiviazione e il recupero operano sull'operatore di confronto fornito in modo implicito dalla Thread.CurrentThread.CurrentCulture proprietà . Se le impostazioni della cultura possono cambiare tra le chiamate a StoreNames e DoesNameExist, e soprattutto se i contenuti dell'array sono memorizzati tra le due chiamate al metodo, la ricerca binaria potrebbe non riuscire.

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

Nell'esempio seguente viene visualizzata una variante consigliata, che usa lo stesso metodo di confronto ordinale (senza distinzione delle impostazioni cultura) sia per ordinare che per eseguire la ricerca nella matrice. Il codice di modifica si riflette nelle righe etichettate 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 questi dati vengono salvati in modo permanente e spostati tra impostazioni cultura e l'ordinamento viene usato per presentare questi dati all'utente, è consigliabile usare StringComparison.InvariantCulture, che opera in modo linguistico per un output utente migliore, ma non è influenzato dalle modifiche apportate alle impostazioni cultura. L'esempio seguente modifica i due esempi precedenti per usare le impostazioni cultura invarianti per l'ordinamento e la ricerca nella 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: Hashtable costruttore

Le stringhe di hashing forniscono un secondo esempio di un'operazione interessata dal modo in cui vengono confrontate le stringhe.

Nell'esempio seguente viene creata un'istanza dell'oggetto Hashtable passandogli l'oggetto StringComparer restituito dalla proprietà StringComparer.OrdinalIgnoreCase. Poiché una classe StringComparer derivata da StringComparer implementa l'interfaccia IEqualityComparer , il relativo GetHashCode metodo 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

Esempio di raccolte: SortedSet<T> e List<T>.Sort

Lo stesso problema di sensibilità alla localizzazione si applica quando si crea un'istanza di una raccolta ordinata di stringhe o si ordina una raccolta esistente basata su stringhe. Specificare sempre un operatore di confronto esplicito:

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

//
// Potentially incorrect code - behavior might vary based on locale.
//
SortedSet<string> mySet = [.. values]; // No comparer specified

List<string> list = [.. values];
list.Sort(); // No comparer specified

//
// Corrected code - uses ordinal sorting; doesn't vary by locale.
//
SortedSet<string> mySet2 = new(values, StringComparer.Ordinal);

List<string> list2 = [.. values];
list2.Sort(StringComparer.Ordinal);
' Words to sort
Dim values As String() = {"able", "ångström", "apple", "Æble",
                          "Windows", "Visual Studio"}

'
' Potentially incorrect code - behavior might vary based on locale.
'
Dim mySet As New SortedSet(Of String)(values) ' No comparer specified

Dim list As New List(Of String)(values)
list.Sort() ' No comparer specified

'
' Corrected code - uses ordinal sorting; doesn't vary by locale.
'
Dim mySet2 As New SortedSet(Of String)(values, StringComparer.Ordinal)

Dim list2 As New List(Of String)(values)
list2.Sort(StringComparer.Ordinal)

Differenze tra .NET e .NET Framework

.NET e .NET Framework gestiscono la globalizzazione in modo diverso. .NET Framework in Windows usa la funzionalità NLS (National Language Support) del sistema operativo per confronti di stringhe linguistiche. .NET usa la libreria International Components for Unicode (ICU) per confronti linguistici di stringhe in tutte le piattaforme supportate.

Poiché l'ICU e NLS implementano logiche diverse nei rispettivi comparatori linguistici, i risultati dei metodi delle stringhe che utilizzano un confronto sensibile alla cultura possono differire tra .NET e .NET Framework. Questo aspetto è importante per qualsiasi metodo che usa un operatore di confronto linguistico per impostazione predefinita, tra cui:

Annotazioni

Non si tratta di un elenco completo delle API interessate.

Una differenza notevole è la gestione dei caratteri Null incorporati e di altri caratteri di controllo. Quando si usa un operatore di confronto linguistico in NLS, alcuni caratteri di controllo, ad esempio il carattere Null (\0) possono essere considerati ignorabili in determinati contesti di confronto. In ICU questi caratteri vengono considerati come caratteri effettivi nella stringa. Ciò può causare string.IndexOf(string) la restituzione di risultati diversi quando la stringa di ricerca contiene un carattere Null.

Ad esempio, il codice seguente può produrre una risposta diversa a seconda del runtime corrente:

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

// The snippet prints:
//
// '3' when running on .NET Framework and .NET Core 2.x - 3.x (Windows)
// '0' when running on .NET 5 or later (Windows)
// '0' when running on .NET Core 2.x - 3.x or .NET 5 (non-Windows)
// '3' when running on .NET Core 2.x or .NET 5+ (in invariant mode)
Const greeting As String = "Hel" & vbNullChar & "lo"
Console.WriteLine($"{greeting.IndexOf(CStr(vbNullChar))}")

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

Il modo migliore per evitare queste sorprese tra piattaforme e implementazioni incrociate consiste nel passare sempre un argomento esplicito StringComparison ai metodi di confronto tra stringhe e usare StringComparison.Ordinal o StringComparison.OrdinalIgnoreCase per confronti non linguistici.

Se si esegue la migrazione di un'applicazione da .NET Framework a .NET e si basano sui comportamenti NLS legacy in Windows, è possibile configurare l'applicazione per l'uso di NLS. Per altre informazioni, vedere Globalizzazione .NET e ICU.

Vedere anche