← Back to Articles         
Sorting and Comparers

Sorting and Comparers

Published on 12 Oct 2026     12 min read C# Collections & Generics
Sorting and Comparers

Sorting and comparers are used to sort, compare, and identify duplicate objects in collections such as List<T>, HashSet<T>, and Dictionary<TKey, TValue>.

C# provides three important interfaces:

  • IComparable<T> – Defines the default sorting order of an object.

  • IComparer<T> – Defines custom sorting rules for objects.

  • IEqualityComparer<T> – Defines custom equality rules for objects.

 

1. What is the difference between IComparable<T> and IComparer<T>?

What is IComparable<T>?

IComparable<T> is an interface used to define the default (natural) sorting order of objects.

It contains one method:

int CompareTo(T? other);

The class itself implements the comparison logic.

What is IComparer<T>?

IComparer<T> is an interface used to define custom sorting logic.

It contains one method:

int Compare(T? x, T? y);

The comparison logic is typically implemented in a separate class.

Comparison

Feature

IComparable<T>

IComparer<T>

Purpose

Default sorting

Custom sorting

Method

CompareTo()

Compare()

Implemented in

Object class

Separate comparer class

Compares

Current object with another

Two supplied objects

Multiple sorting rules

One default implementation

Multiple comparers

Used by

List.Sort()

List.Sort(comparer)

Example Program

Suppose we have employees and want to sort them by ID or salary.

using System;
using System.Collections.Generic;

// Default sorting by ID
class Employee : IComparable<Employee>
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public int Salary { get; set; }

    public int CompareTo(Employee? other)
    {
        if (other == null)
            return 1;

        return Id.CompareTo(other.Id);
    }
}

// Custom sorting by Salary
class SalaryComparer : IComparer<Employee>
{
    public int Compare(Employee? x, Employee? y)
    {
        if (ReferenceEquals(x, y)) return 0;
        if (x == null) return -1;
        if (y == null) return 1;

        return x.Salary.CompareTo(y.Salary);
    }
}

class Program
{
    static void Main()
    {
        List<Employee> employees = new()
        {
            new Employee { Id = 3, Name = "John", Salary = 50000 },
            new Employee { Id = 1, Name = "David", Salary = 70000 },
            new Employee { Id = 2, Name = "Peter", Salary = 40000 }
        };

        // IComparable - Sort by ID
        employees.Sort();

        Console.WriteLine("Sort by ID:");

        foreach (var emp in employees)
            Console.WriteLine($"{emp.Id} - {emp.Name}");

        // IComparer - Sort by Salary
        employees.Sort(new SalaryComparer());

        Console.WriteLine("Sort by Salary:");

        foreach (var emp in employees)
            Console.WriteLine($"{emp.Name} - {emp.Salary}");
    }
}

Output:

Sort by ID:
1 - David
2 - Peter
3 - John

Sort by Salary:
Peter - 40000
John - 50000
David - 70000

Explanation

IComparable<T>:

  1. Employee implements IComparable<Employee>.

  2. CompareTo() compares employee IDs.

  3. employees.Sort() automatically uses CompareTo().

  4. Employees are sorted by ID.

IComparer<T>:

  1. SalaryComparer implements IComparer<Employee>.

  2. Compare() compares salaries.

  3. employees.Sort(new SalaryComparer()) uses custom sorting.

  4. Employees are sorted by salary.

How does comparison work?

Both methods follow the same comparison contract:

Compare(x, y)
      |
      v
Result < 0  --> x comes before y
Result = 0  --> x and y have equal sort order
Result > 0  --> x comes after y

Key Points

  • IComparable<T> defines the natural sorting order.

  • IComparer<T> supports multiple custom sorting rules.

  • CompareTo() compares the current object with another.

  • Compare() compares two supplied objects.

  • Both return negative, zero, or positive values.

  • Prefer CompareTo() instead of subtracting numeric values because subtraction may overflow.

 

2. What is IEqualityComparer<T>, and when is it used?

What is it?

IEqualityComparer<T> is an interface used to determine whether two objects should be considered equal.

It is commonly used with:

  • HashSet<T>

  • Dictionary<TKey, TValue>

  • LINQ Distinct()

  • LINQ GroupBy()

  • LINQ Except()

It contains two methods:

bool Equals(T? x, T? y);

int GetHashCode(T obj);

Why is it used?

Suppose two employee objects have the same employee ID but different names.

We want to treat them as duplicates based on the employee ID.

By default, a normal class uses reference equality unless equality behavior is overridden.

IEqualityComparer<T> allows us to define custom equality without modifying the employee class.

Example Program

using System;
using System.Collections.Generic;
using System.Linq;

class Employee
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
}

class EmployeeComparer : IEqualityComparer<Employee>
{
    public bool Equals(Employee? x, Employee? y)
    {
        if (ReferenceEquals(x, y)) return true;
        if (x == null || y == null) return false;

        return x.Id == y.Id;
    }

    public int GetHashCode(Employee obj)
    {
        return obj.Id.GetHashCode();
    }
}

class Program
{
    static void Main()
    {
        List<Employee> employees = new()
        {
            new Employee { Id = 1, Name = "John" },
            new Employee { Id = 2, Name = "David" },
            new Employee { Id = 1, Name = "Peter" }
        };

        var uniqueEmployees =
            employees.Distinct(new EmployeeComparer());

        foreach (var emp in uniqueEmployees)
        {
            Console.WriteLine($"{emp.Id} - {emp.Name}");
        }
    }
}

Output:

1 - John
2 - David

Explanation

  1. Three employee objects are created.

  2. John and Peter both have Id = 1.

  3. EmployeeComparer.Equals() compares employee IDs.

  4. GetHashCode() calculates a hash code based on the ID.

  5. Distinct() treats John and Peter as duplicates.

  6. Only the first employee with ID 1 is returned.

How does it work?

Employee List
     |
     v
[1-John, 2-David, 1-Peter]
     |
     v
Distinct(EmployeeComparer)
     |
     v
GetHashCode()
     |
     v
Check candidate matches with Equals()
     |
     v
Remove duplicates
     |
     v
[1-John, 2-David]

Key Points

  • IEqualityComparer<T> defines custom equality.

  • It implements Equals() and GetHashCode().

  • Useful for duplicate detection and hash-based collections.

  • Two objects can be considered equal even when they are different instances.

  • IComparer<T> is for ordering; IEqualityComparer<T> is for equality.

  • Hash codes should be based on the same properties used for equality.

 

3. What is the relationship between Equals() and GetHashCode()?

What is Equals()?

Equals() determines whether two objects are logically equal.

obj1.Equals(obj2);

Returns:

  • true – Objects are equal.

  • false – Objects are not equal.

What is GetHashCode()?

GetHashCode() returns an integer hash code for an object.

Hash-based collections use hash codes to help locate stored elements efficiently.

Important Rule

If two objects are equal according to Equals(), they must return the same hash code.

Equals(x, y) == true
         |
         v
GetHashCode(x) == GetHashCode(y)

However, the reverse is not necessarily true.

Same Hash Code
      |
      v
Objects may be equal or unequal
      |
      v
Equals() determines actual equality

This is because different objects can have the same hash code, called a hash collision.

Example Program

using System;

class Employee
{
    public int Id { get; set; }
    public string Name { get; set; } = "";

    public override bool Equals(object? obj)
    {
        if (obj is not Employee other)
            return false;

        return Id == other.Id;
    }

    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}

class Program
{
    static void Main()
    {
        Employee emp1 = new()
        {
            Id = 1,
            Name = "John"
        };

        Employee emp2 = new()
        {
            Id = 1,
            Name = "David"
        };

        Console.WriteLine(emp1.Equals(emp2));

        Console.WriteLine(emp1.GetHashCode());

        Console.WriteLine(emp2.GetHashCode());
    }
}

Output:

True
1
1

Explanation

  1. emp1 and emp2 are different object instances.

  2. Both have the same employee ID.

  3. Equals() compares their IDs and returns true.

  4. GetHashCode() uses the ID for hashing.

  5. Both objects return the same hash code.

What happens if GetHashCode() is not implemented correctly?

Suppose Equals() compares employee IDs, but GetHashCode() uses a different property such as Name.

Two employees with the same ID could then have different hash codes.

This breaks the equality contract and can cause incorrect behavior in HashSet<T> and Dictionary<TKey, TValue>.

Key Points

  • Equals() checks logical equality.

  • GetHashCode() supports efficient hash-based lookups.

  • Equal objects must have equal hash codes.

  • Equal hash codes do not guarantee equal objects.

  • Override GetHashCode() whenever overriding value-based Equals().

  • Avoid changing equality-related properties while an object is stored as a hash-based key.

 

4. What is the difference between Sort() and OrderBy()?

What is Sort()?

Sort() is a method available on List<T>.

It modifies the existing list by sorting its elements.

What is OrderBy()?

OrderBy() is a LINQ extension method.

It returns a new ordered sequence without modifying the original collection.

Comparison

Feature

Sort()

OrderBy()

Namespace

System.Collections.Generic

System.Linq

Modifies original list

Yes

No

Return type

void

IOrderedEnumerable<T> for in-memory LINQ

Execution

Immediate

Deferred

Multiple properties

Custom comparer

ThenBy()

Descending order

Custom comparer or Reverse()

OrderByDescending()

Stable sorting

Not guaranteed

Yes

Typical use

Sort existing list

LINQ queries

Example Program

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> numbers = new()
        {
            40, 10, 30, 20
        };

        // Sort()
        numbers.Sort();

        Console.WriteLine("After Sort():");

        Console.WriteLine(string.Join(", ", numbers));

        List<int> values = new()
        {
            40, 10, 30, 20
        };

        // OrderBy()
        var sortedValues = values.OrderBy(x => x);

        Console.WriteLine("After OrderBy():");

        Console.WriteLine(string.Join(", ", sortedValues));

        Console.WriteLine("Original List:");

        Console.WriteLine(string.Join(", ", values));
    }
}

Output:

After Sort():
10, 20, 30, 40

After OrderBy():
10, 20, 30, 40

Original List:
40, 10, 30, 20

Explanation

Sort():

Original List
[40, 10, 30, 20]
       |
       v
    Sort()
       |
       v
Same List Modified
[10, 20, 30, 40]

OrderBy():

Original List
[40, 10, 30, 20]
       |
       v
OrderBy(x => x)
       |
       v
New Ordered Sequence
[10, 20, 30, 40]

Original List Unchanged
[40, 10, 30, 20]

Important: Deferred Execution

OrderBy() does not perform sorting immediately when the query is created.

var result = numbers.OrderBy(x => x);

The sorting occurs when the sequence is enumerated:

foreach (var number in result)
{
    Console.WriteLine(number);
}

Or when it is materialized:

List<int> sorted = numbers.OrderBy(x => x).ToList();

Real-Time Scenario

In an ASP.NET Core application:

var employees = employeeList
    .OrderBy(e => e.Name)
    .ToList();

This sorts an in-memory collection without modifying the original list.

With EF Core:

var employees = await dbContext.Employees
    .OrderBy(e => e.Name)
    .ToListAsync();

Here, EF Core normally translates OrderBy() into SQL ORDER BY, so the database performs the sorting.

Key Points

  • Sort() modifies the original list.

  • OrderBy() returns an ordered sequence.

  • Sort() executes immediately.

  • LINQ OrderBy() uses deferred execution for in-memory sequences.

  • OrderBy() supports ThenBy() for multiple sorting properties.

  • OrderBy() is stable, while List<T>.Sort() is not guaranteed to be stable.

  • Use database-side OrderBy() for EF Core queries when appropriate.

 

5. How do you sort a list of custom objects by multiple properties?

What is it?

Multiple-property sorting means sorting objects using more than one field.

For example:

  1. Sort employees by Department (ascending).

  2. Within the same department, sort by Salary (descending).

  3. If salaries are equal, sort by Name (ascending).

LINQ provides:

  • OrderBy() – Primary ascending sort.

  • OrderByDescending() – Primary descending sort.

  • ThenBy() – Secondary ascending sort.

  • ThenByDescending() – Secondary descending sort.

Example Program

using System;
using System.Collections.Generic;
using System.Linq;

class Employee
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public string Department { get; set; } = "";
    public int Salary { get; set; }
}

class Program
{
    static void Main()
    {
        List<Employee> employees = new()
        {
            new Employee
            {
                Id = 1,
                Name = "John",
                Department = "IT",
                Salary = 50000
            },
            new Employee
            {
                Id = 2,
                Name = "David",
                Department = "HR",
                Salary = 40000
            },
            new Employee
            {
                Id = 3,
                Name = "Peter",
                Department = "IT",
                Salary = 70000
            },
            new Employee
            {
                Id = 4,
                Name = "Robert",
                Department = "HR",
                Salary = 60000
            }
        };

        var sortedEmployees = employees
            .OrderBy(e => e.Department)
            .ThenByDescending(e => e.Salary)
            .ThenBy(e => e.Name)
            .ToList();

        foreach (var emp in sortedEmployees)
        {
            Console.WriteLine(
                $"{emp.Department} - {emp.Name} - {emp.Salary}");
        }
    }
}

Output:

HR - Robert - 60000
HR - David - 40000
IT - Peter - 70000
IT - John - 50000

Explanation

Step 1: Sort by Department

.OrderBy(e => e.Department)

Employees are grouped in ascending department order:

HR
HR
IT
IT

Step 2: Sort by Salary within each Department

.ThenByDescending(e => e.Salary)
HR
  Robert - 60000
  David  - 40000

IT
  Peter  - 70000
  John   - 50000

Step 3: Sort by Name when Department and Salary are equal

.ThenBy(e => e.Name)

This acts as the third sorting condition.

Execution Flow

Employee List
      |
      v
OrderBy(Department)
      |
      v
HR Employees
IT Employees
      |
      v
ThenByDescending(Salary)
      |
      v
Highest Salary First
Within Each Department
      |
      v
ThenBy(Name)
      |
      v
Resolve Salary Ties by Name
      |
      v
Final Sorted List

Can we use Sort() for multiple properties?

Yes. We can use a custom comparer.

employees.Sort((x, y) =>
{
    int result = string.Compare(
        x.Department,
        y.Department,
        StringComparison.Ordinal);

    if (result != 0)
        return result;

    result = y.Salary.CompareTo(x.Salary);

    if (result != 0)
        return result;

    return string.Compare(
        x.Name,
        y.Name,
        StringComparison.Ordinal);
});

Explanation:

  1. Compare departments first.

  2. If departments are equal, compare salaries in descending order.

  3. If salaries are equal, compare names.

  4. The original list is sorted in place.

Important Difference: OrderBy() vs ThenBy()

Consider:

employees
    .OrderBy(e => e.Department)
    .OrderBy(e => e.Salary);

The second OrderBy() starts a new primary ordering. It does not preserve Department as the primary sort key.

Correct approach:

employees
    .OrderBy(e => e.Department)
    .ThenBy(e => e.Salary);

Key Points

  • Use OrderBy() for the primary sorting property.

  • Use ThenBy() for secondary sorting.

  • Use ThenByDescending() for descending secondary sorting.

  • Multiple ThenBy() calls are supported.

  • Sort() with a custom comparer can also sort by multiple properties.

  • Avoid using multiple OrderBy() calls when you intend hierarchical sorting.

 

Important Interview Revision

Question

Key Answer

What is IComparable<T>?

Defines default sorting using CompareTo().

What is IComparer<T>?

Defines custom sorting using Compare().

What is IEqualityComparer<T>?

Defines equality using Equals() and GetHashCode().

IComparer vs IEqualityComparer?

Ordering vs equality comparison.

Relationship between Equals() and GetHashCode()?

Equal objects must have equal hash codes.

Does the same hash code mean equal objects?

No. Hash collisions are possible.

Sort() vs OrderBy()?

Sort() modifies the list; OrderBy() returns an ordered sequence.

Does OrderBy() execute immediately?

No, in-memory LINQ uses deferred execution.

How to sort by multiple properties?

OrderBy().ThenBy().ThenByDescending().

What is stable sorting?

Equal sort keys retain their original relative order.

Does List<T>.Sort() guarantee stable sorting?

No.

Does LINQ OrderBy() guarantee stable sorting?

Yes, for LINQ to Objects.

Most important interview concepts: IComparable<T> vs IComparer<T>, equality and hash-code contracts, Sort() vs OrderBy(), and multi-property sorting using ThenBy().

← Back to Articles