CodeGym /Courses /C# SELF /Practical Use of Comparers in .NET

Practical Use of Comparers in .NET

C# SELF
Level 30 , Lesson 4
Available

1. Introduction

Why comparers are actually a big deal…

In real-world projects, things are way less trivial than in textbook tasks: objects are complex, data comes from users or other services, and the "sort order" (or even equality rules) you need often change from task to task. Comparers let you make your code flexible, and the behavior — predictable and under control.

In .NET, you need comparers whenever you want to sort, search, group, or filter out duplicates of your custom class objects, and also when you build data structures that have "order". You’ll see this a lot in collections, algorithms, and when integrating with external APIs.

Where you’ll run into comparers in .NET:

  • Sorting collections (List<T>.Sort, Array.Sort, OrderBy in LINQ)
  • Ordered data structures (SortedSet<T>, SortedDictionary<TKey, TValue>)
  • Searching and comparing objects in collections (Contains, IndexOf — when you need to define "equality", not just order)
  • Grouping, filtering, and deduplication (for example, .Distinct())

Why does this matter in practice?

  • Reports where sorted output matters (like a list of students alphabetically or by GPA)
  • Matching input data to reference values (like searching for a record by a specific criterion)
  • Saving memory and speed (picking the right data structure speeds up search and makes your app more responsive)
  • Uniqueness validation (like when registering a user by e-mail)

2. Real-life example: sorting by different criteria

Let’s say we have a user class:

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

    // You can add override Equals and GetHashCode — but that’s for homework
}

We have a list of users, and we want to:

  • Sort them by last name, and if last names match — by first name.
  • Be able to search for a user by e-mail (ignoring case).
  • Exclude duplicate users when adding.

Sorting with IComparer<T>

Option 1: the classic way — make a separate comparer class.

// Comparer for sorting by last name and first name
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);
    }
}

Usage:

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 = "Petr", 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})");
}

// Result — users are sorted by last name, and if last names are equal, by first name.

Pro tip: Do it this way if you need the comparison order in several different places in your app and if a lambda won’t save you from copy-paste.

On-the-fly sorting with a lambda

Don’t want to make classes just for a one-off sort order? Lambda to the rescue!

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);
});

Works the same, but the comparer is created right inside the call. Saves lines, but not always handy for reuse.

3. Smart search: comparing e-mail case-insensitively

In real life, users type e-mails however they want, and you’re a programmer, not a judge, so it makes sense to compare e-mails case-insensitively.

Let’s do this with a comparer and search:

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;
    }
}

Usage 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!

Important note: When you make your own EqualityComparer, don’t forget to implement both methods: Equals and GetHashCode. If you forget the second one, things will get weird: searching in collections might "glitch".

4. Using SortedSet and SortedDictionary

This is where comparers really show their power.

SortedSet<T> and SortedDictionary<TKey, TValue> can’t work with your objects unless you tell .NET how to compare them. And the comparison and equality order affects which elements are considered different!

Example with 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 = "Petr", LastName = "Petrov", Age = 18 },
    new User { FirstName = "Ivan", LastName = "Petrov", Age = 25 } // Duplicate by name and last name
};

// "Ivan Petrov" with different ages — only one will get into the set
Console.WriteLine("Users in SortedSet:");
foreach (var user in sortedUsersByFullName)
    Console.WriteLine($"{user.LastName} {user.FirstName} ({user.Age})");

SortedSet won’t add two "Ivan Petrovs"!

Heads up: The comparer here defines the "uniqueness" logic. If you compare only by last name and first name, then users with the same full name but different ages are considered the same user.

5. Table: when to use which comparer in .NET

Scenario What to implement Usage example
Natural sort order (one, universal for the type)
IComparable<T>
List<T>.Sort()
Multiple sort options (by name, age, e-mail ...)
IComparer<T>
(classes, lambdas)
List<T>.Sort(comparer)
Searching for unique elements in a collection
IEqualityComparer<T>
HashSet<T>
,
Dictionary<TKey, TValue>
Grouping, removing duplicates
IEqualityComparer<T>
LINQ
.Distinct(comparer)
Sorting by time, date, or a complex combo of fields
Comparison<T>
, on-the-fly lambda
List<T>.Sort((a, b) => ...)
Using in LINQ (one-off queries) lambda in
OrderBy
,
ThenBy
OrderBy(x => x.Name)

6. Practice: searching and comparing in collections

Let’s implement user registration in our app with unique e-mail (case-insensitive). If a user with that address already exists, we need to say so.

public static bool RegisterUser(List<User> users, User newUser)
{
    // Use Any with a lambda to check e-mail uniqueness
    bool exists = users.Any(u =>
        string.Equals(u.Email, newUser.Email, StringComparison.OrdinalIgnoreCase));
    if (exists)
    {
        Console.WriteLine($"User with e-mail: {newUser.Email} is already registered!");
        return false;
    }
    users.Add(newUser);
    Console.WriteLine($"User {newUser.FirstName} added.");
    return true;
}

Usage:

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

RegisterUser(userList, new User { FirstName = "Vasya", Email = "FIRST@example.com" });
// Will print: "User with e-mail: FIRST@example.com is already registered!"

7. Typical mistakes when using comparers

Everyone loves a list of mistakes, but let’s make it colorful.

Sometimes, when a programmer implements a comparer for a custom type, they forget to check for null or — even worse — write the comparison so that it breaks "strict ordering". For example, if your comparer returns contradictory values, sorting will act unpredictably, and objects will start getting lost or "merged" in collections.

Another common mess-up is inconsistency between Equals and the comparer logic. For example, if Equals thinks objects are different, but the comparer thinks they’re the same, then in SortedSet or SortedDictionary you’ll get chaos: an element won’t be found even though it looks like it’s there.

You’ll also see cases where a programmer compares only one property of an object (like last name), forgetting that there can be lots of users with the same last name. As a result, objects get "overwritten", lost, and your data becomes inconsistent. That means it no longer reflects the real state of the system — you get duplicates, missing info, or broken program logic.

2
Task
C# SELF, level 30, lesson 4
Locked
Creating a comparator for sorting by age
Creating a comparator for sorting by age
1
Survey/quiz
Comparators, level 30, lesson 4
Unavailable
Comparators
Comparators and comparing objects
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION