CodeGym /Courses /C# SELF /Interface IComparer<T&...

Interface IComparer<T>

C# SELF
Level 30 , Lesson 1
Available

1. Introduction

Imagine you're the dean of a university with a ton of students. You regularly need lists sorted by different criteria:

  1. By name (so you can quickly find a student alphabetically).
  2. By GPA (to hand out scholarships to top students).
  3. By age (for stats, contests, and so on).
  4. By course, and inside each course — by last name.

If we relied only on IComparable<T>, our Student class would have to implement just one way to compare. For example, we might decide that the "natural" order for a student is by GPA. Great, List.Sort() works now! But what if we need a list sorted by name? The Student class is already "busy" comparing by GPA. It can't have two "natural" orders at the same time. It's like if you only had one instruction manual for "how to be the best at boxing," but you suddenly needed to be the best at chess, and you tried to use the "boxing" manual. You probably wouldn't get very far, right?

That's exactly the kind of scenario where you need to sort the same type of objects in different ways, and you don't want to cram all the comparison logic into the class itself — that's what the IComparer<T> interface is for. It's super useful if you don't have access to the class's source code or if the class shouldn't know about all the possible ways it could be sorted.

2. The IComparer<T> Interface

If IComparable<T> is like an object's inner sense, knowing how to compare itself to another, then IComparer<T> is a totally separate, outside judge or independent referee who takes any two players (objects) and compares them by its own, pre-set rules.

Imagine you're a soccer coach. You need to pick a team captain.

  • IComparable<T>: Each player says, "I'm better than this guy because I run faster!" (Their own, internal rule).
  • IComparer<T>: You, as the coach, say: "Alright guys, today we're picking the captain by passing accuracy. Petya, make a pass! Vasya, make a pass! Nice, Petya's more accurate! He's captain today." (That's your external rule, which you apply to any two players).

Definition: IComparer<T> is a .NET interface that lets us define external comparison logic for two objects of type T. That means the class implementing IComparer<T> isn't one of the objects being compared; it just provides a Compare method that takes two objects and figures out their relative order.

Syntax

The syntax for the IComparer<T> interface is pretty simple:


public interface IComparer<T>
{
    // Method that compares two objects of type T
    // x - first object to compare
    // y - second object to compare
    int Compare(T x, T y);
}

The Compare(T x, T y) method works just like the CompareTo(T other) method in IComparable<T>:

  • Returns a negative number if x is "less than" y.
  • Returns zero (0) if x is "equal to" y.
  • Returns a positive number if x is "greater than" y.

In our case, "less than," "equal," and "greater than" depend entirely on the comparison logic we define in our Compare implementation.

How is IComparer<T> different from IComparable<T>?

Class / Interface Where it's implemented What it's for Example usage
IComparable<T>
Directly in the type (class/struct) One standardized way to compare Sort by ascending ID
IComparer<T>
In a separate class Any number of comparison methods Sort by name, date

3. Implementing IComparer<T> in Practice

Let's keep building our "one big app" — a simple user model. Let's say we have this class:


// Our user class
public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
}

Sorting by name: let's make a separate comparator

Let's write a special class that implements IComparer<User> and compares users by name:


// Comparator class for sorting by name
public class UserNameComparer : IComparer<User>
{
    public int Compare(User x, User y)
    {
        // Check for null (so there are no surprises!)
        if (ReferenceEquals(x, y)) return 0;
        if (x is null) return -1;   // null is "less than" any object
        if (y is null) return 1;

        // Compare by name (using standard string sorting)
        return string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
    }
}

Using the comparator to sort a list:


List<User> users = new List<User>
{
    new User { Name = "Ivan", Age = 20, Email = "ivan@mail.com" },
    new User { Name = "Anna", Age = 32, Email = "anna@gmail.com" },
    new User { Name = "Boris", Age = 28, Email = "boris@work.org" },
    new User { Name = "Ruslan", Age = 19, Email = "ruslan@yandex.ru" }
};

// Sort by name using IComparer
users.Sort(new UserNameComparer());

users.ForEach(u => Console.WriteLine(u.Name)); // Anna, Boris, Ivan, Ruslan

See how clean and elegant that is? The comparator class becomes a first-class citizen in your program — you can use it for any other user lists.

4. Multiple Comparison Options: Creating Different Comparators

The cool thing is, you can make as many comparators as you want. For example, let's implement sorting by age:


// Comparator for sorting by age
public class UserAgeComparer : 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;

        // Sort by ascending age
        return x.Age.CompareTo(y.Age);
    }
}

And now:


users.Sort(new UserAgeComparer());
users.ForEach(u => Console.WriteLine($"{u.Name} ({u.Age})"));
// Output: Ruslan (19) Ivan (20) Boris (28) Anna (32)

If you want to sort by age descending, just swap the arguments:


// Comparator for sorting by descending age
public class UserAgeDescendingComparer : 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;

        // Swap order: y.CompareTo(x)
        return y.Age.CompareTo(x.Age);
    }
}

5. Handy Nuances

How does this work under the hood?

When you call Sort() with a comparator, the list passes each element to the comparator and asks: "Who should go first?" Your Compare answers: "this one, that one, or they're equal." Sorting repeats this convo for all pairs until you get the final, sorted list.

What if the values are equal? Just return 0 — that means the order of the elements won't change (or it'll be decided by the internal sorting mechanism).

Where else is IComparer<T> used?

The IComparer<T> interface is used not just in lists. Here are a few places you'll see it in .NET:

Here's an example:


var sortedSet = new SortedSet<User>(new UserAgeComparer());

Now SortedSet will always automatically keep things sorted by age!

Quick note on null-safety

One of the most common mistakes beginners make is NullReferenceException. Don't forget to check for null inside Compare, especially if your list might have those values.

A common pattern (here it is again, just to be sure):


if (ReferenceEquals(x, y)) return 0;
if (x is null) return -1;
if (y is null) return 1;

It's a good habit: it helps you avoid your program crashing at the worst possible moment!

Pros and Cons of the IComparer<T> Approach

  • Clearly separates comparison logic from data. The user class doesn't have to care how or why it's being sorted.
  • Easy to reuse comparison logic in different places.
  • Scalable: you can make as many sorting options as you want without changing the original type.

But be careful not to fall into the "forgot about null" or "comparison logic isn't consistent" trap. For example, if Compare(x, y) returns 0, then Compare(y, x) should also return 0; if Compare(x, y) returns >0, then Compare(y, x) should return <0, and so on.

Visual Cheat Sheet: When to Use What?

Task What to use Where to put the logic
One "natural" sort order
IComparable<T>
In the type itself (class/struct)
Different sorting options
IComparer<T>
In a separate class
Quick, one-off, "on the fly"
Comparison<T>
/ lambda
In the Sort method parameter, via delegate
Complex, often-used logic
IComparer<T>
As a separate comparator class

In the next lecture, you'll learn how to use and combine delegates and lambda expressions for comparing objects. For now, try making a few different comparators for your own app and enjoy the elegant architecture where sorting is done outside the main class, and the logic for picking a criterion stays flexible and extendable.

6. Common Mistakes When Implementing Comparators

Mistake #1: Not checking for null.
If one of the objects being compared is null and you didn't account for that in your code, your program might crash with a NullReferenceException.

Mistake #2: Incorrect values for -1, 0, +1.
The Compare method should return a negative number if the first object is less than the second, zero if they're equal, and a positive number if it's greater. Breaking this rule leads to "weird" sorting behavior.

Mistake #3: Asymmetric comparison logic.
If when comparing x and y you return one value, but when comparing y and x you return the same (instead of the opposite), the result gets unpredictable.

Mistake #4: Using Sort() without a comparator for a custom type.
If your type doesn't implement IComparable or IComparable<T>, calling Sort() without an explicit comparator will throw an InvalidOperationException.

How to avoid:
Check edge cases, cover critical code with unit tests (we'll get to that!), don't be afraid to check the docs — and let your comparators run like Swiss watches.

2
Task
C# SELF, level 30, lesson 1
Locked
Implementing a string comparator
Implementing a string comparator
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION