CodeGym /Corsi /C# SELF /Uso pratico dei comparatori in .NET

Uso pratico dei comparatori in .NET

C# SELF
Livello 30 , Lezione 4
Disponibile

1. Introduzione

Perché i comparatori sono davvero una cosa seria…

Nei progetti veri non è tutto così banale come sembra negli esercizi: gli oggetti sono complessi, i dati arrivano dagli utenti o da altri servizi, gli "ordini di ordinamento" che ci servono (o anche solo le regole di uguaglianza) spesso cambiano da caso a caso. I comparatori ti permettono di rendere il codice flessibile e il comportamento — prevedibile e sotto controllo.

In .NET i comparatori servono ogni volta che devi ordinare, cercare, raggruppare o escludere duplicati di oggetti delle tue classi, e anche quando costruisci strutture dati "ordinate". Soprattutto li trovi nelle collezioni, negli algoritmi e quando integri con API esterne.

Dove trovi i comparatori in .NET:

  • Ordinamento delle collezioni (List<T>.Sort, Array.Sort, OrderBy in LINQ)
  • Strutture dati ordinate (SortedSet<T>, SortedDictionary<TKey, TValue>)
  • Ricerca e confronto di oggetti nelle collezioni (Contains, IndexOf — quando serve definire "uguaglianza", non solo l’ordine)
  • Raggruppamento, filtraggio e deduplicazione (ad esempio, .Distinct())

Perché serve davvero nella pratica?

  • Report dove conta l’output ordinato (tipo lista studenti in ordine alfabetico o per media voti)
  • Confronto dei dati in ingresso con valori di riferimento (tipo ricerca di un record per un certo criterio)
  • Risparmio di memoria e velocità (scegliere la struttura dati giusta velocizza la ricerca e rende l’app più reattiva)
  • Validazione dell’unicità (ad esempio, quando registri un utente per e-mail)

2. Esempio reale: ordinamento per criteri diversi

Supponiamo di avere una classe utente:

public class User
{
    public string FirstName { get; set; } = "";
    public string LastName { get; set; } = "";
    public string Email { get; set; } = "";
    public int Age { get; set; }

    // Puoi aggiungere override di Equals e GetHashCode — ma questo è per lettura a casa
}

Abbiamo una lista di utenti e vogliamo:

  • Ordinarli per cognome, e se il cognome è uguale — per nome.
  • Permettere la ricerca dell’utente per e-mail (ignorando maiuscole/minuscole).
  • Escludere i duplicati quando aggiungiamo utenti.

Ordinamento con IComparer<T>

Opzione 1: il modo classico — creare un comparatore separato.

// Comparatore per ordinare per cognome e nome
public class UserFullNameComparer : IComparer<User>
{
    public int Compare(User? x, User? y)
    {
        if (ReferenceEquals(x, y)) return 0;
        if (x is null) return -1;
        if (y is null) return 1;

        int lastNameComparison = StringComparer.OrdinalIgnoreCase.Compare(x.LastName, y.LastName);
        if (lastNameComparison != 0)
            return lastNameComparison;

        return StringComparer.OrdinalIgnoreCase.Compare(x.FirstName, y.FirstName);
    }
}

Uso:

var users = new List<User>
{
    new User { FirstName = "Ivan", LastName = "Petrov", Age = 20, Email = "ivan.petrov@email.com" },
    new User { FirstName = "Anna", LastName = "Smirnova", Age = 22, Email = "anna.smirnova@email.com" },
    new User { FirstName = "Pietro", LastName = "Petrov", Age = 18, Email = "petr.petrov@email.com" }
};

users.Sort(new UserFullNameComparer());

foreach (var user in users)
{
    Console.WriteLine($"{user.LastName} {user.FirstName} ({user.Age})");
}

// Risultato — utenti ordinati per cognome, e se il cognome è uguale, per nome.

Nota: Si fa così se l’ordine di confronto serve in più parti dell’app e se la lambda non basta a evitare il copia-incolla.

Ordinamento "al volo" con una lambda

Non vuoi creare classi solo per un ordine che usi una volta? La lambda ti salva!

users.Sort((x, y) =>
{
    int lastNameComparison = StringComparer.OrdinalIgnoreCase.Compare(x.LastName, y.LastName);
    if (lastNameComparison != 0)
        return lastNameComparison;
    return StringComparer.OrdinalIgnoreCase.Compare(x.FirstName, y.FirstName);
});

Funziona uguale, ma il comparatore lo crei direttamente nella chiamata. Risparmi righe, ma non sempre è comodo se ti serve più volte.

3. Ricerca furba: confronto e-mail senza distinzione tra maiuscole/minuscole

Nella vita vera gli utenti scrivono l’e-mail come vogliono, e tu — sei un programmatore, non un giudice, quindi ha senso confrontare le e-mail senza badare alle maiuscole.

Facciamolo con un comparatore e la ricerca:

public class EmailComparer : IEqualityComparer<User>
{
    public bool Equals(User? x, User? y)
    {
        if (ReferenceEquals(x, y)) return true;
        if (x is null || y is null) return false;
        return string.Equals(x.Email, y.Email, StringComparison.OrdinalIgnoreCase);
    }
    public int GetHashCode(User obj)
    {
        return obj.Email?.ToLowerInvariant().GetHashCode() ?? 0;
    }
}

Uso in HashSet:

var usersSet = new HashSet<User>(new EmailComparer());
usersSet.Add(new User { Email = "Petrov@example.com" });
bool contains = usersSet.Contains(new User { Email = "petrov@example.com" }); // true!

Nota importante: Quando crei il tuo EqualityComparer, ricordati di implementare entrambi i metodi: Equals e GetHashCode. Se dimentichi il secondo — il comportamento sarà strano: il confronto può "sballare" quando cerchi nelle collezioni.

4. Uso di SortedSet e SortedDictionary

Qui i comparatori danno davvero il meglio.

SortedSet<T> e SortedDictionary<TKey, TValue> non funzionano con i tuoi oggetti se non spieghi a .NET come confrontarli. E l’ordine di confronto e uguaglianza influisce su quali elementi sono considerati diversi!

Esempio con SortedSet<User>

var sortedUsersByFullName = new SortedSet<User>(new UserFullNameComparer())
{
    new User { FirstName = "Ivan", LastName = "Petrov", Age = 20 },
    new User { FirstName = "Anna", LastName = "Smirnova", Age = 22 },
    new User { FirstName = "Pietro", LastName = "Petrov", Age = 18 },
    new User { FirstName = "Ivan", LastName = "Petrov", Age = 25 } // Duplicato per nome e cognome
};

// "Ivan Petrov" con età diversa — solo uno finisce nell’insieme
Console.WriteLine("Utenti in SortedSet:");
foreach (var user in sortedUsersByFullName)
    Console.WriteLine($"{user.LastName} {user.FirstName} ({user.Age})");

SortedSet non aggiunge due "Ivan Petrov"!

Importante: Qui il comparatore definisce la logica di "unicità". Se confronti solo per cognome e nome, utenti con stessi nome e cognome ma età diversa sono considerati la stessa persona.

5. Tabella: quando usare quale comparatore in .NET

Scenario Cosa implementare Esempio d’uso
Ordine naturale di ordinamento (uno, universale per il tipo)
IComparable<T>
List<T>.Sort()
Più criteri di ordinamento (per nome, età, e-mail ...)
IComparer<T>
(classi, lambda)
List<T>.Sort(comparer)
Ricerca di elementi unici in una collezione
IEqualityComparer<T>
HashSet<T>
,
Dictionary<TKey, TValue>
Raggruppamento, rimozione duplicati
IEqualityComparer<T>
LINQ
.Distinct(comparer)
Ordinamento per tempo, data, o combinazione complessa di campi
Comparison<T>
, lambda al volo
List<T>.Sort((a, b) => ...)
Uso in LINQ (query una tantum) lambda in
OrderBy
,
ThenBy
OrderBy(x => x.Name)

6. Pratica: ricerca e confronto nelle collezioni

Implementiamo nell’app la registrazione di un utente con e-mail unica (ignorando maiuscole/minuscole). Se esiste già un utente con quell’indirizzo, bisogna dirlo.

public static bool RegisterUser(List<User> users, User newUser)
{
    // Usiamo Any con lambda per cercare l’unicità dell’e-mail
    bool exists = users.Any(u =>
        string.Equals(u.Email, newUser.Email, StringComparison.OrdinalIgnoreCase));
    if (exists)
    {
        Console.WriteLine($"Utente con e-mail: {newUser.Email} è già registrato!");
        return false;
    }
    users.Add(newUser);
    Console.WriteLine($"Utente {newUser.FirstName} aggiunto.");
    return true;
}

Uso:

var userList = new List<User>
{
    new User { Email = "first@example.com" }
};

RegisterUser(userList, new User { FirstName = "Vasya", Email = "FIRST@example.com" });
// Stampa: "Utente con e-mail: FIRST@example.com è già registrato!"

7. Errori tipici quando usi i comparatori

Tutti amano le liste di errori, ma noi lo facciamo in modo pittoresco.

A volte, quando un programmatore implementa un comparatore per un tipo personalizzato, si dimentica di controllare il null oppure — peggio ancora — implementa il confronto in modo che "l’ordine rigoroso" viene violato. Ad esempio, se nel comparatore ritorni valori contraddittori, l’ordinamento si comporterà in modo imprevedibile e nelle collezioni gli oggetti inizieranno a perdersi o "fondersi".

Un altro errore frequente è l’incoerenza tra l’implementazione di Equals e la logica del comparatore. Ad esempio, se Equals pensa che gli oggetti siano diversi, ma il comparatore — che sono uguali, allora in SortedSet o SortedDictionary succede il caos: l’elemento non si trova, anche se sembra che ci sia.

Capita anche che il programmatore confronti solo una proprietà dell’oggetto (tipo il cognome), dimenticando che ci possono essere altri utenti con lo stesso cognome. Così gli oggetti "si sovrascrivono", si perdono, e i dati diventano inconsistenti. Cioè, non riflettono più lo stato reale del sistema — compaiono duplicati, sparisce l’informazione giusta o si rompe la logica del programma.

1
Sondaggio/quiz
Comparatori, livello 30, lezione 4
Non disponibile
Comparatori
Comparatori e confronto di oggetti
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION