Modifiche funzionali durante il confronto di stringhe in .NET 5+

.NET 5 introduce una modifica funzionale del runtime in cui le API di globalizzazione usano ICU per impostazione predefinita in tutte le piattaforme supportate. Si tratta di un cambiamento rispetto alle versioni precedenti di .NET Core e .NET Framework, che usano la funzionalità NLS (National Language Support) del sistema operativo durante l'esecuzione in Windows. Per altre informazioni su queste modifiche, incluse le opzioni di compatibilità che possono ripristinare la modifica funzionale, vedere Globalizzazione .NET e ICU.

Motivo della modifica

Questa modifica è stata introdotta per unificare . Comportamento di globalizzazione di NET in tutti i sistemi operativi supportati. Consente inoltre alle applicazioni di aggregare le proprie librerie di globalizzazione anziché dipendere dalle librerie predefinite del sistema operativo. Per altre informazioni, vedere la notifica sulle modifiche che causano un'interruzione.

Differenze di comportamento

Se si usano funzioni come string.IndexOf(string) senza chiamare l'overload che accetta un argomento StringComparison, si potrebbe voler eseguire una ricerca ordinale, ma invece si crea inavvertitamente una dipendenza dal comportamento specifico delle impostazioni cultura. Poiché NLS e ICU implementano una logica diversa nei relativi operatori di confronto linguistici, i risultati di metodi come string.IndexOf(string) possono restituire valori imprevisti.

Questo può manifestarsi anche dove non si prevede sempre che le strutture di globalizzazione siano attive. 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 Core 2.x - 3.x (Windows)
// '0' when running on .NET 5 or later (Windows)
// '0' when running on .NET Core 2.x - 3.x or .NET 5 (non-Windows)
// '3' when running on .NET Core 2.x or .NET 5+ (in invariant mode)

string s = "Hello\r\nworld!";
int idx = s.IndexOf("\n");
Console.WriteLine(idx);

// The snippet prints:
//
// '6' when running on .NET Core 3.1
// '-1' when running on .NET 5 or .NET Core 3.1 (non-Windows OS)
// '-1' when running on .NET 5 (Windows 10 May 2019 Update or later)
// '6' when running on .NET 6+ (all Windows and non-Windows OSs)

Per altre informazioni, vedere Le API per la globalizzazione usano librerie ICU in Windows.

Proteggersi da comportamenti imprevisti

Questa sezione offre due opzioni per gestire le modifiche funzionali impreviste in .NET 5.

Abilitare gli analizzatori di codice

Gli analizzatori di codice possono rilevare siti di chiamata potenzialmente pieni di bug. Per evitare comportamenti inaspettati, è consigliabile abilitare gli analizzatori della piattaforma del compilatore .NET (Roslyn) nel progetto. Gli analizzatori aiutano a segnalare il codice che potrebbe inavvertitamente usare un operatore di confronto linguistico quando è probabile che fosse previsto un operatore di confronto ordinale. Le regole seguenti devono aiutare a segnalare questi problemi:

Queste regole specifiche non sono abilitate per impostazione predefinita. Per abilitarle e visualizzare eventuali violazioni come errori di compilazione, impostare le proprietà seguenti nel file di progetto:

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

Il frammento di codice seguente mostra esempi di codice che generano avvisi o errori dell'analizzatore di codice pertinenti.

//
// Potentially incorrect code - answer might vary based on locale.
//
string s = GetString();
// Produces analyzer warning CA1310 for string; CA1307 matches on char ','
int idx = s.IndexOf(",");
Console.WriteLine(idx);

//
// Corrected code - matches the literal substring ",".
//
string s = GetString();
int idx = s.IndexOf(",", StringComparison.Ordinal);
Console.WriteLine(idx);

//
// Corrected code (alternative) - searches for the literal ',' character.
//
string s = GetString();
int idx = s.IndexOf(',');
Console.WriteLine(idx);

Allo stesso modo, quando si crea un'istanza di una raccolta ordinata di stringhe o si ordina una raccolta esistente basata su stringhe, specificare un operatore di confronto esplicito.

//
// Potentially incorrect code - behavior might vary based on locale.
//
SortedSet<string> mySet = new SortedSet<string>();
List<string> list = GetListOfStrings();
list.Sort();

//
// Corrected code - uses ordinal sorting; doesn't vary by locale.
//
SortedSet<string> mySet = new SortedSet<string>(StringComparer.Ordinal);
List<string> list = GetListOfStrings();
list.Sort(StringComparer.Ordinal);

Ripristinare i comportamenti NLS

Per ripristinare i comportamenti NLS precedenti delle applicazioni .NET 5+ durante l'esecuzione in Windows, seguire la procedura descritta in Globalizzazione .NET e ICU. Questa opzione di compatibilità a livello di applicazione deve essere impostato a livello di applicazione. Le singole librerie non possono scegliere esplicitamente se adottare o meno questo comportamento.

Suggerimento

È consigliabile abilitare le regole di analisi del codice CA1307, CA1309 e CA1310 per migliorare lo stato del codice e individuare eventuali bug latenti esistenti. Per altre informazioni, vedere Abilitare gli analizzatori di codice.

API interessate

La maggior parte delle applicazioni .NET non rileva comportamenti imprevisti a causa delle modifiche apportate a .NET 5. Tuttavia, a causa del numero di API interessate e di quanto queste API siano fondamentali per l'ecosistema .NET in generale, è necessario essere consapevoli del fatto che .NET 5 potrebbe introdurre comportamenti indesiderati o esporre bug latenti già esistenti nell'applicazione.

Le API interessate includono:

Nota

Non si tratta di un elenco completo delle API interessate.

Per impostazione predefinita, tutte le API precedenti usano la ricerca e il confronto linguistici delle stringhe usando le impostazioni cultura correnti del thread. Le differenze tra ricerca e confronto linguistici e ordinali sono descritte in Ricerca e confronto linguistici e ordinali.

Poiché ICU implementa confronti linguistici delle stringhe in modo diverso da NLS, le applicazioni basate su Windows che eseguono l'aggiornamento a .NET 5 da una versione precedente di .NET Core o .NET Framework e che chiamano una delle API interessate possono notare che le API iniziano a presentare comportamenti diversi.

Eccezioni

  • Se un'API accetta un parametro StringComparison o CultureInfo esplicito, tale parametro esegue l'override del comportamento predefinito dell'API.
  • I membri System.String in cui il primo parametro è di tipo char (ad esempio, String.IndexOf(Char)) usano la ricerca ordinale, a meno che il chiamante non passi un argomento StringComparison esplicito che specifica CurrentCulture[IgnoreCase] o InvariantCulture[IgnoreCase].

Per un'analisi più dettagliata del comportamento predefinito di ogni API String, vedere la sezione Tipi di ricerca e confronto predefiniti.

Ricerca e confronto linguistici e ordinali

La ricerca e il confronto ordinali (noti anche come non linguistici) scompongono una stringa nei singoli elementi char ed eseguono una ricerca o un confronto carattere per carattere. Ad esempio, le stringhe "dog" e "dog" vengono confrontate come uguali con un operatore di confronto Ordinal, poiché le due stringhe sono costituite esattamente dalla stessa sequenza di caratteri. Tuttavia, "dog" e "Dog" vengono confrontati come non uguali con un operatore di confronto Ordinal, perché non sono costituiti dalla stessa sequenza di caratteri. Ovvero, il punto di codice U+0044 della 'D' maiuscola si verifica prima del punto di codice U+0064 della 'd' minuscola, con il risultato che "Dog" viene ordinato prima di "dog".

Un operatore di confronto OrdinalIgnoreCase funziona ancora su base carattere per carattere, ma elimina le differenze tra maiuscole e minuscole durante l'esecuzione dell'operazione. Con un operatore di confronto OrdinalIgnoreCase le coppie di caratteri 'd' e 'D' vengono confrontate come uguali, così come le coppie di caratteri 'á' e 'Á'. Ma il carattere 'a' non accentato viene confrontato come non uguale al carattere 'á' accentato.

Nella tabella seguente sono riportati alcuni esempi:

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

Unicode consente inoltre alle stringhe di avere diverse rappresentazioni in memoria. Ad esempio, una e acuta (é) può essere rappresentata in due modi possibili:

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

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

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

Con un operatore di confronto ordinale nessuna di queste stringhe viene confrontata come uguale all'altra. Questo perché contengono tutte sequenze di caratteri sottostanti diverse, anche se quando ne viene eseguito il rendering sullo schermo sembrano tutte uguali.

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

Console.WriteLine("resume".IndexOf("e", StringComparison.Ordinal)); // prints '1'
Console.WriteLine("r\u00E9sum\u00E9".IndexOf("e", StringComparison.Ordinal)); // prints '-1'
Console.WriteLine("r\u00E9sume\u0301".IndexOf("e", StringComparison.Ordinal)); // prints '5'
Console.WriteLine("re\u0301sum\u00E9".IndexOf("e", StringComparison.Ordinal)); // prints '1'
Console.WriteLine("re\u0301sume\u0301".IndexOf("e", StringComparison.Ordinal)); // prints '1'
Console.WriteLine("resume".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '1'
Console.WriteLine("r\u00E9sum\u00E9".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '-1'
Console.WriteLine("r\u00E9sume\u0301".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '5'
Console.WriteLine("re\u0301sum\u00E9".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '1'
Console.WriteLine("re\u0301sume\u0301".IndexOf("E", StringComparison.OrdinalIgnoreCase)); // prints '1'

Le routine di ricerca e confronto ordinali non sono mai interessate dalle impostazioni cultura del thread corrente.

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

Prendere in considerazione di nuovo la stringa "résumé" e le quattro diverse rappresentazioni. La tabella seguente mostra ogni rappresentazione suddivisa nei relativi elementi delle regole di confronto.

String Come elementi delle regole 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 vagamente a ciò che i lettori considerano un singolo carattere o un cluster di caratteri. È concettualmente simile a un cluster di grafemi, ma comprende un ombrello leggermente più ampio.

Con 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 un modificatore di accento acuto". Ciò consente al metodo IndexOf di trovare una corrispondenza con 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")); // prints '-1' (not found)
Console.WriteLine("r\u00E9sum\u00E9".IndexOf("\u00E9")); // prints '1'
Console.WriteLine("\u00E9".IndexOf("e\u0301")); // prints '0'

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 usare la logica del caso speciale che riguarda la lunghezza delle stringhe in tali scenari.

Le routine di ricerca e confronto con riconoscimento delle impostazioni cultura sono una forma speciale di routine di ricerca e confronto linguistiche. Con un operatore di confronto con riconoscimento delle impostazioni cultura, il concetto di elemento delle regole di confronto viene esteso per includere informazioni specifiche delle impostazioni cultura specificate.

Ad esempio, nell'alfabeto ungherese, quando i due caratteri <dz> appaiono uno dopo l'altro, vengono considerati una lettera univoca distinta da <d> o <z>. Ciò significa che quando <dz> viene visualizzato in una stringa, un operatore di confronto che riconosce le impostazioni cultura ungheresi lo considera come un singolo elemento delle regole di confronto.

String Come elementi delle regole di confronto Osservazioni:
"endz" "e" + "n" + "d" + "z" (con un operatore di confronto linguistico standard)
"endz" "e" + "n" + "dz" (con un operatore di confronto con riconoscimento delle impostazioni cultura ungheresi)

Quando si usa un operatore di confronto con riconoscimento delle impostazioni cultura ungheresi, ciò significa che la stringa "endz"non termina con la sottostringa "z", poiché <dz> e <z> sono considerati elementi delle regole di confronto con significato semantico diverso.

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

Nota

  • Comportamento: gli operatori di confronto linguistici e con riconoscimento delle impostazioni cultura possono subire modifiche funzionali di tanto in tanto. Sia l'ICU che la precedente struttura NLS di Windows vengono aggiornate per tenere conto dei cambiamenti delle lingue del mondo. Per altre informazioni, vedere il post di blog Varianza dei dati delle impostazioni locali (impostazioni cultura). 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 esistenti di maiuscole e minuscole.
  • Utilizzo: gli operatori di confronto StringComparison.InvariantCulture e StringComparison.InvariantCultureIgnoreCase sono operatori di confronto linguistici che non riconoscono le impostazioni cultura. Ovvero, questi operatori di confronto comprendono concetti come il carattere accentato é che ha molteplici possibili rappresentazioni sottostanti e che tutte queste rappresentazioni devono essere trattate allo stesso modo. Ma gli operatori di confronto linguistici che non riconoscono le impostazioni cultura non conterranno una gestione speciale per <dz> distinto da <d> o <z>, come illustrato in precedenza. Inoltre, non includeranno caratteri speciali come il tedesco Eszett (ß).

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

Per altre informazioni, vedere Procedure consigliate per il confronto di stringhe in .NET.

Implicazioni per la sicurezza

Se l'app usa un'API interessata per il filtro, è consigliabile abilitare le regole di analisi del codice CA1307 e CA1309 per individuare i punti in cui potrebbe essere stata inavvertitamente usata una ricerca linguistica invece di una ricerca ordinale. I modelli di codice come i seguenti possono essere soggetti a exploit di sicurezza.

//
// THIS SAMPLE CODE IS INCORRECT.
// DO NOT USE IT IN PRODUCTION.
//
public bool ContainsHtmlSensitiveCharacters(string input)
{
    if (input.IndexOf("<") >= 0) { return true; }
    if (input.IndexOf("&") >= 0) { return true; }
    return false;
}

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

Tipi di ricerca e confronto predefiniti

Nella tabella seguente sono elencati i tipi di ricerca e confronto predefiniti per varie API di tipo stringa e stringa. Se il chiamante fornisce un parametro CultureInfo o StringComparison esplicito, verrà applicato tale parametro rispetto a quelli predefiniti.

API Comportamento predefinito Osservazioni:
string.Compare CurrentCulture
string.CompareTo CurrentCulture
string.Contains Ordinale
string.EndsWith Ordinale (quando il primo parametro è char)
string.EndsWith CurrentCulture (quando il primo parametro è string)
string.Equals Ordinale
string.GetHashCode Ordinale
string.IndexOf Ordinale (quando il primo parametro è char)
string.IndexOf CurrentCulture (quando il primo parametro è string)
string.IndexOfAny Ordinale
string.LastIndexOf Ordinale (quando il primo parametro è char)
string.LastIndexOf CurrentCulture (quando il primo parametro è string)
string.LastIndexOfAny Ordinale
string.Replace Ordinale
string.Split Ordinale
string.StartsWith Ordinale (quando il primo parametro è char)
string.StartsWith CurrentCulture (quando il primo parametro è string)
string.ToLower CurrentCulture
string.ToLowerInvariant InvariantCulture
string.ToUpper CurrentCulture
string.ToUpperInvariant InvariantCulture
string.Trim Ordinale
string.TrimEnd Ordinale
string.TrimStart Ordinale
string == string Ordinale
string != string Ordinale

A differenza delle API string, tutte le API MemoryExtensions eseguono ricerche e confronti ordinali per impostazione predefinita, con le eccezioni seguenti.

API Comportamento predefinito Osservazioni:
MemoryExtensions.ToLower CurrentCulture (quando viene passato un argomento CultureInfo Null)
MemoryExtensions.ToLowerInvariant InvariantCulture
MemoryExtensions.ToUpper CurrentCulture (quando viene passato un argomento CultureInfo Null)
MemoryExtensions.ToUpperInvariant InvariantCulture

Una conseguenza è che quando si converte il codice dall'utilizzo di string all'utilizzo di ReadOnlySpan<char>, è possibile che vengano introdotte inavvertitamente modifiche funzionali. Di seguito ne viene illustrato un esempio.

string str = GetString();
if (str.StartsWith("Hello")) { /* do something */ } // this is a CULTURE-AWARE (linguistic) comparison

ReadOnlySpan<char> span = s.AsSpan();
if (span.StartsWith("Hello")) { /* do something */ } // this is an ORDINAL (non-linguistic) comparison

Il modo consigliato per risolvere questo problema consiste nel passare un parametro StringComparison esplicito a queste API. Le regole di analisi del codice CA1307 e CA1309 possono essere utili.

string str = GetString();
if (str.StartsWith("Hello", StringComparison.Ordinal)) { /* do something */ } // ordinal comparison

ReadOnlySpan<char> span = s.AsSpan();
if (span.StartsWith("Hello", StringComparison.Ordinal)) { /* do something */ } // ordinal comparison

Vedi anche