Codectionary / Developer documentation / C#

IComparable & IComparer

IComparable<T> and IComparer<T> both define how to order objects, but serve different purposes. A class implements IComparable<T> to define its single 'natural' ordering via CompareTo(), used automatically by List<T>.Sort() and Array.Sort(). An IComparer<T> is a separate object passed alongside a collection to define one or more custom, alternative orderings, without modifying the class being sorted at all.

Read the syntax

IComparable<T>.CompareTo defines a type’s default order: negative, zero and positive results mean before, equal and after. List<T>.Sort uses that order when no comparer is supplied.

IComparer<T> supplies a separate ordering, while a Comparison<T> lambda is useful for a one-off sort. Comparers must be consistent; handle nulls explicitly if null values are possible.

Syntax

class X : IComparable<X> { public int CompareTo(X other) { } }

Examples

Implementing IComparable

Defining a class's single natural ordering.

List<Student> students = new List<Student>
{
    new Student("John", 85),
    new Student("Zain", 92),
    new Student("Jamal", 78)
};

students.Sort();  // uses CompareTo() automatically
Console.WriteLine(string.Join(", ", students));  // Jamal(78), John(85), Zain(92)

public class Student : IComparable<Student>
{
    public string Name;
    public int Grade;

    public Student(string name, int grade) { Name = name; Grade = grade; }

    public int CompareTo(Student? other) => other is null ? 1 : Grade.CompareTo(other.Grade);

    public override string ToString() => $"{Name}({Grade})";
}
  1. Defining a class's single natural ordering.
  2. IComparable<T>.CompareTo defines a type’s default order: negative, zero and positive results mean before, equal and after. List<T>.Sort uses that order when no comparer is supplied.
  3. Student.CompareTo compares grades, so Sort arranges the students by 78, 85 and 92. The resulting display is Jamal, John, Zain with those grades.

Using a Custom Comparer

An IComparer<T> lets you sort the same objects in a different way without changing the class.

List<Student> students = new List<Student>
{
    new Student("Zain", 92),
    new Student("John", 85)
};

students.Sort(new ByNameComparer());
Console.WriteLine(string.Join(", ", students));  // John(85), Zain(92)

record Student(string Name, int Grade)
{
    public override string ToString() => $"{Name}({Grade})";
}

public class ByNameComparer : IComparer<Student>
{
    public int Compare(Student? x, Student? y) => string.Compare(x?.Name, y?.Name);
}
  1. An IComparer<T> lets you sort the same objects in a different way without changing the class.
  2. IComparable<T>.CompareTo defines a type’s default order: negative, zero and positive results mean before, equal and after. List<T>.Sort uses that order when no comparer is supplied.
  3. ByNameComparer compares Name rather than Grade. Sorting a list containing Zain and John places John first; this snippet also needs the Student type defined in the same project.

Sorting with a Lambda via Comparison<T>

For quick one-off custom sorts, a lambda is often simpler than a full IComparer<T> class.

List<Student> students = new List<Student>
{
    new Student("Zain", 92),
    new Student("John", 85),
    new Student("Jamal", 78)
};

students.Sort((a, b) => b.Grade.CompareTo(a.Grade));  // descending by grade
Console.WriteLine(string.Join(", ", students));  // Zain(92), John(85), Jamal(78)

record Student(string Name, int Grade)
{
    public override string ToString() => $"{Name}({Grade})";
}
  1. For quick one-off custom sorts, a lambda is often simpler than a full IComparer<T> class.
  2. IComparable<T>.CompareTo defines a type’s default order: negative, zero and positive results mean before, equal and after. List<T>.Sort uses that order when no comparer is supplied.
  3. The lambda compares b.Grade with a.Grade to reverse grade order. It prints Zain, John, Jamal; this snippet also relies on the Student definition.

Best practices

  • Implement IComparable<T> when a class has one obvious, natural ordering (like numbers by value, or dates chronologically)
  • Write a separate IComparer<T> class (or use a lambda) when you need multiple different orderings, or cannot modify the class being sorted
  • Use a lambda passed to Sort()/OrderBy() for simple, one-off custom orderings instead of writing a full IComparer<T> class
  • Handle null carefully in CompareTo()/Compare() implementations - a naive call can throw a NullReferenceException on unexpected null input

Runtime & versions

.NET runtime. Feature availability depends on the language and runtime version used by your project; consult the official reference below.

C# language reference

Specifications & further reading