Guide

Confronto tra stringhe in C#: Equals, OrdinalIgnoreCase, StringComparer e le insidie culturali

Dario Fadda Aprile 16, 2026

Il confronto tra stringhe è una delle operazioni più comuni in qualsiasi applicazione .NET, eppure è anche una delle fonti più insidiose di bug difficili da riprodurre — specialmente quando l’applicazione viene eseguita in ambienti con culture diverse o in pipeline CI/CD con impostazioni locali variabili. In questo articolo vediamo come funzionano correttamente string.Equals(), OrdinalIgnoreCase, StringComparer e come evitare le trappole più comuni legate alla cultura.

L’operatore == e il confronto ordinale

L’operatore == su stringhe esegue un confronto ordinale case-sensitive, basato sui valori Unicode dei caratteri, senza alcuna considerazione culturale:

var a = "Hello";
var b = "hello";
Console.WriteLine(a == b);       // False
Console.WriteLine(a == "Hello"); // True

Questo è corretto e prevedibile per confronti interni al codice (chiavi di dizionari, nomi di variabili, costanti). Il problema nasce quando si vuole un confronto case-insensitive, o quando si confrontano stringhe con caratteri soggetti a regole culturali.

string.Equals() con StringComparison

La regola d’oro è: passare sempre esplicitamente un parametro StringComparison. Senza di esso, alcuni overload usano CurrentCulture, creando comportamenti potenzialmente incoerenti tra ambienti diversi.

// Confronto case-insensitive, senza dipendenze culturali
bool uguale = string.Equals("admin", input, StringComparison.OrdinalIgnoreCase);

// Equivalente con metodo d'istanza
bool ancheUguale = "admin".Equals(input, StringComparison.OrdinalIgnoreCase);

Panoramica dei valori di StringComparison

Valore Case Cultura Uso consigliato
Ordinal Sensibile Nessuna File, chiavi, dati binari
OrdinalIgnoreCase Insensibile Nessuna Comandi, URL, identificatori
InvariantCulture Sensibile Invariante Testo serializzato/persistito
InvariantCultureIgnoreCase Insensibile Invariante Testo serializzato, case-indipendente
CurrentCulture Sensibile Locale utente Testo mostrato all’utente
CurrentCultureIgnoreCase Insensibile Locale utente Ricerca/filtro lato UI

La trappola di ToLower() e ToUpper()

Uno degli antipattern più diffusi è usare ToLower() per normalizzare le stringhe prima del confronto:

// Antipattern: alloca una nuova stringa inutilmente
if (input.ToLower() == "admin") { }
if (input.ToLowerInvariant() == "admin") { }

// Corretto: nessuna allocazione, semantica esplicita
if (string.Equals(input, "admin", StringComparison.OrdinalIgnoreCase)) { }

Il problema non è solo di prestazioni (allocazione di una stringa temporanea), ma di correttezza semantica. La versione con ToLower() dipende dalla cultura corrente del thread, mentre OrdinalIgnoreCase è culturalmente neutro e deterministico.

Il problema della “i” Turca

Questo è forse il bug più famoso legato alla cultura nelle stringhe. In turco esistono quattro varianti della lettera i: la “i” minuscola con punto diventa “İ” maiuscola con punto (non “I” come in italiano), e la “ı” minuscola senza punto diventa “I” maiuscola. Il risultato:

var culture = new System.Globalization.CultureInfo("tr-TR");

// Bug su locale turco!
bool sbagliato = "file".ToUpper(culture) == "FILE"; // False! "file" diventa "FİLE" in turco

// OrdinalIgnoreCase usa regole invarianti
bool corretto = string.Equals("file", "FILE", StringComparison.OrdinalIgnoreCase); // True

Questo bug si manifesta tipicamente in applicazioni multi-tenant o globali dove il server ha una cultura diversa dall’ambiente di sviluppo. La soluzione è sempre usare OrdinalIgnoreCase per confronti tecnici (nomi di file, comandi, URL, header HTTP) e riservare CurrentCulture solo al testo destinato all’utente finale.

StringComparer per collezioni

StringComparer implementa sia IComparer<string> che IEqualityComparer<string>, rendendolo ideale per strutture dati come Dictionary, HashSet e SortedSet:

Dizionario case-insensitive per gli header HTTP

// "Content-Type" e "content-type" devono essere equivalenti
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
headers["Content-Type"] = "application/json";

Console.WriteLine(headers["content-type"]); // application/json
Console.WriteLine(headers["CONTENT-TYPE"]); // application/json

SortedSet case-insensitive

var comandi = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
comandi.Add("Start");
comandi.Add("stop");

bool haStart = comandi.Contains("START"); // True
// I duplicati vengono rilevati correttamente
comandi.Add("START"); // Non aggiunge, esiste già come "Start"
Console.WriteLine(comandi.Count); // 2

Confronto ad alte prestazioni con Span<char>

Per scenari con requisiti di performance elevati (parsing di protocolli, hot paths), .NET offre confronti allocation-free tramite ReadOnlySpan<char>:

var riga = "Content-Type: application/json";

// Confronto senza allocare nuove stringhe
bool isContentType = riga.AsSpan(0, 12).Equals(
    "Content-Type".AsSpan(),
    StringComparison.OrdinalIgnoreCase);

// StartsWith su Span
bool isHttps = url.AsSpan().StartsWith(
    "https://".AsSpan(),
    StringComparison.OrdinalIgnoreCase);

Questo approccio è particolarmente utile in middleware HTTP, parser di configurazione e codice che elabora grandi volumi di testo.

Pattern Matching con Switch

Il pattern matching di C# non supporta nativamente il confronto case-insensitive negli switch, ma esistono due approcci corretti:

// Approccio 1: Guard clause con Equals
var risultato = comando switch
{
    _ when string.Equals(comando, "start", StringComparison.OrdinalIgnoreCase) => "Avvio...",
    _ when string.Equals(comando, "stop", StringComparison.OrdinalIgnoreCase) => "Arresto...",
    _ => "Comando non riconosciuto"
};

// Approccio 2: Normalizzazione con ToUpperInvariant (accettabile per switch)
var risultato2 = comando.ToUpperInvariant() switch
{
    "START" => "Avvio...",
    "STOP" => "Arresto...",
    _ => "Comando non riconosciuto"
};

Esempio completo: parser di configurazione

public sealed class ConfigParser
{
    private readonly FrozenDictionary<string, string> _impostazioni;

    public ConfigParser(IEnumerable<KeyValuePair<string, string>> rawSettings)
    {
        // FrozenDictionary è ottimizzato per letture frequenti (immutabile dopo la creazione)
        _impostazioni = rawSettings.ToFrozenDictionary(
            kvp => kvp.Key,
            kvp => kvp.Value,
            StringComparer.OrdinalIgnoreCase);
    }

    public string? Get(string chiave) =>
        _impostazioni.TryGetValue(chiave, out var valore) ? valore : null;
}

// Utilizzo
var config = new ConfigParser(new[]
{
    new KeyValuePair<string, string>("DatabaseUrl", "Server=..."),
    new KeyValuePair<string, string>("MaxConnections", "100"),
});

Console.WriteLine(config.Get("databaseurl"));    // "Server=..."
Console.WriteLine(config.Get("MAXCONNECTIONS")); // "100"

Riepilogo: regole pratiche

  1. Usa OrdinalIgnoreCase per chiavi, identificatori, URL, nomi di file, comandi, header HTTP.
  2. Usa CurrentCulture solo per testo mostrato all’utente, quando le regole locali sono rilevanti.
  3. Non usare ToLower() per confronti: alloca inutilmente e dipende dalla cultura.
  4. Specifica sempre StringComparison esplicitamente nelle chiamate a Equals e Compare.
  5. Usa StringComparer quando passi logica di confronto a strutture dati.
  6. Usa MemoryExtensions su Span<char> per hot path ad alta frequenza e senza allocazioni.

Seguendo queste linee guida, si eliminano intere classi di bug difficili da riprodurre e si ottiene codice più robusto, portabile e performante.

Fonte: DevLeader — C# String Comparison: Equals, OrdinalIgnoreCase, StringComparer, and Culture Pitfalls

💬 Unisciti alla discussione!


Questo è un blog del Fediverso: puoi trovare quindi questo articolo ovunque con @blog@spcnet.it e ogni commento/risposta apparirà qui sotto.

Se vuoi commentare su Confronto tra stringhe in C#: Equals, OrdinalIgnoreCase, StringComparer e le insidie culturali, utilizza la discussione sul Forum.
Condividi la tua esperienza, confrontati con altri professionisti e approfondisci i dettagli tecnici nel nostro 👉 forum community