In this article, we’re going to learn about the IComparable and the IComparer interfaces, and the Comparison delegate.

In C#, if we have an array of types such as integers or strings, it supports inbuilt comparison and sorting. This is because it implements the IComparer interface. However, what if we want to compare the custom objects that we create? In that case, we have to implement some additional interfaces like IComparable, or use the Comparison<T> delegate.

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

The IComparable Interface

The IComparable<T> interface provides a way to compare two objects of a particular type. This helps in adding the sorting capability to an object. By implementing the IComparable<T> interface, we are providing a default sort order for our objects.

For instance, let’s consider that we have an Employee class with two properties:

public partial class Employee 
{
    public int Id { get; set; }
    public string? Name { get; set; }

    public Employee(int id, string? name)
    {
        Id = id;
        Name = name;
    }
}

Now let’s say that we want to add a default sorting capability for this class. We can do that by implementing the IComparable<T> interface.

While implementing the IComparable<T> interface, we need to define the CompareTo() method. This method compares the current instance with another object of the same type and returns an integer that indicates whether the current instance should be before, after, or at the same position as the other object.

Let’s implement sorting in such a way to order the employees based on their Id values:

public partial class Employee : IComparable<Employee>
{
    public int Id { get; set; }
    public string? Name { get; set; }

    public Employee(int id, string? name)
    {
        Id = id;
        Name = name;
    }

    public int CompareTo(Employee? otherEmployee)
    {
        return otherEmployee is null ? 1 : Id.CompareTo(otherEmployee.Id);
    }
}

If the CompareTo() method returns a negative value, the current instance comes before the other object. On the other hand, if it returns a positive value, the current instance comes after the other object. Similarly, returning a zero indicates that the current instance and the other object should be at the same level.

Now let’s test the functionality in the Program class:

var employees = new Employee[5]
{
    new Employee(4, "John"),
    new Employee(2, "Tom"),
    new Employee(1, "Eric"),
    new Employee(5, "Dan"),
    new Employee(3, "Alen")
};

Console.WriteLine("The Employee Array:");

PrintEmployees(employees);

Array.Sort(employees);

Console.WriteLine($"{Environment.NewLine}Default Sorting:");

PrintEmployees(employees);

static void PrintEmployees(Employee[] employees)
{
    foreach (var employee in employees)
    {
        Console.WriteLine(employee.Id + "\t\t" + employee.Name);
    }
}

Let’s run the program and observe the output:

The Employee Array:
4               John
2               Tom
1               Eric
5               Dan
3               Alen

Default Sorting:
1               Eric
2               Tom
3               Alen
4               John
5               Dan

While printing the employee array, we can notice that initially, the elements are in the order we add them. However, after calling the Array.Sort(), we can see that they are ordered based on their Ids.

In this example, we have implemented a default sorting in ascending order. For implementing the sorting in descending order, we just need to reverse the order of elements in the CompareTo() method. Similarly, even though we tried the example with Array, this works pretty well with all types of collections.

The IComparer Interface

We can use the IComparer<T> interface to provide additional comparison mechanisms for our objects. For instance, we may want to provide sorting on several fields or support sorting in both ascending and descending order. The IComparer<T> interface can support these kinds of scenarios.

Using IComparer<T> is a two-step process. First, we need to declare a class that implements the IComparer<T> interface and implements the Compare() method. Let’s create a class for implementing sorting by Id in ascending order:

public class SortByIdAscendingHelper : IComparer<Employee>
{
    public int Compare(Employee? firstEmployee, Employee? secondEmployee)
    {
        return ((firstEmployee is null) || (secondEmployee is null)) ?
            0 :
            firstEmployee.Id.CompareTo(secondEmployee.Id);
    }
}

The second step is to declare a method that returns an instance of the previous IComparer object:

public static IComparer<Employee> SortByIdAscending()
{
    return new SortByIdAscendingHelper();
}

After that, we can supply this IComparer object as the second argument when we call the overloaded Array.Sort() method in the Program class:

Array.Sort(employees, Employee.SortByIdAscending());

Console.WriteLine($"{Environment.NewLine}Sorting by Id Ascending");

PrintEmployees(employees);

Now let’s run the application and test the results:

Sorting by Id Ascending
1               Eric
2               Tom
3               Alen
4               John
5               Dan

We can see that this sorts the Employee array based on ascending order of Id.

Similarly, we can define any number of IComparer objects that can sort the objects based on different fields and different orders. For instance, if we want to implement sorting by descending order of Id, we can create a new class for it:

public class SortByIdDescendingHelper : IComparer<Employee>
{
    public int Compare(Employee? firstEmployee, Employee? secondEmployee)
    {
        return ((firstEmployee is null) || (secondEmployee is null)) ?
            0 :
            secondEmployee.Id.CompareTo(firstEmployee.Id);
    }
}

Of course, we need to create a new method that returns an instance of the IComparer object:

public static IComparer<Employee> SortByIdDescending()
{
    return new SortByIdDescendingHelper();
}

After that, we can use this IComparer object for sorting the Employee array:

Array.Sort(employees, Employee.SortByIdDescending());

Console.WriteLine($"{Environment.NewLine}Sorting by Id Descending");

PrintEmployees(employees);

This will sort the array in the descending order of Id:

Sorting by Id Descending
5               Dan
4               John
3               Alen
2               Tom
1               Eric

This way, we can implement any number of additional sorting capabilities for a class by creating different IComparer objects. We can create IComparer objects to sort not just arrays, but any type of collection.

The Comparison Delegate

We can use the Comparison<T> delegate to implement a method that compares two objects of the same type:

public delegate int Comparison<in T>(T x, T y);

Here, T represents the type of objects to compare and the delegate has two parameters x and y that represent objects of the same type. It returns a signed integer value – when x is less than y the value is less than zero and vice versa. If x equals y, it returns zero.

Let’s implement a  Comparison<T> delegate method to compare employees by Id in ascending order:

public static int CompareEmployeesByIdAscending(Employee employee1, Employee employee2)
{
    if ((employee1 is null) || (employee2 is null))
    {
        return 0;
    }

    int value1 = employee1?.Id ?? 0;
    int value2 = employee2?.Id ?? 0;

    return value1.CompareTo(value2);
}

We can use the Comparison<T> delegate for sorting both Arrays or Collections by passing it as an argument into the respective Sort() method.

In the Program class, let’s create the employee list and use the Comparison<T> delegate to sort it:

var employeeList = new List<Employee>
{
    new Employee(4, "John"),
    new Employee(2, "Tom"),
    new Employee(1, "Eric"),
    new Employee(5, "Dan"),
    new Employee(3, "Alen")
};

employeeList.Sort(Employee.CompareEmployeesByIdAscending);

Console.WriteLine($"{Environment.NewLine}Sorting by Id Ascending using Comparison Delegate");

PrintEmployeeList(employeeList);

static void PrintEmployeeList(List<Employee> employees)
{
    foreach (var employee in employees)
    {
        Console.WriteLine(employee.Id + "\t\t" + employee.Name);
    }
}

If we look at the output, we can see that this sorts the employee list in the ascending order of Ids:

Sorting by Id Ascending using Comparison Delegate
1               Eric
2               Tom
3               Alen
4               John
5               Dan

Of course, if we want to sort in descending order, we just need to interchange the comparison order in the delegate method:

public static int CompareEmployeesByIdDescending(Employee employee1, Employee employee2)
{
    if ((employee1 is null) || (employee2 is null))
    {
        return 0;
    }

    int value1 = employee1?.Id ?? 0;
    int value2 = employee2?.Id ?? 0;

    return value2.CompareTo(value1);
}

This will sort the employee list in the descending order of Ids:

Sorting by Id Descending using Comparison Delegate
5               Dan
4               John
3               Alen
2               Tom
1               Eric

It is possible to implement sorting based on different fields using the Comparison<T> delegate. We just need to create new delegate methods and use them for sorting. 

IComparable vs IComparer vs Comparison Delegate

By implementing the IComparable interface, we are providing the capability to compare that object with other objects of the same class. As the name suggests, IComparable is more like saying I’m Comparable. For comparing objects using the IComparable interface, we have to implement the IComparable interface in that class. Furthermore, we can use the IComparable interface to add just one type of sorting capability to a class. We typically use this when we need to add a default sorting capability for a class.  Once we implement this, we can sort arrays using the Array.Sort(Array) method, Lists using the List<T>.Sort() method, etc. 

On the other hand, by implementing the IComparer interface, we are creating a comparer object which can compare any two instances of the same class. As the name suggests, IComparer is more like saying I’m a Comparer. For comparing objects using the IComparer interface, we don’t have to make any changes to that class, but we just need to create an external Comparer object. Furthermore, we can implement different types of sorting based on different fields and orders by creating the corresponding Comparer objects. Typically we use this when we want to add additional sorting capabilities to a class that may already have a default sorting. We just need to supply the Comparer while sorting arrays using the Sort(Array, IComparer) method overload, or while sorting the Lists using the Sort(IComparer<T>) method overload, etc.

Comparison delegate is just another way to add sorting capabilities to a custom object. We can create a delegate method for comparing two objects of the same type just like how we would create a Comparer object. We can implement any number of sorting capabilities by creating different delegate methods. Later, we can supply the comparison delegate while sorting arrays using the Sort<T>(T[], Comparison<T>) method overload, or sorting Lists using the Sort(Comparison<T>) method overload, etc.

Performance Benchmarking – IComparable vs IComparer vs Comparison Delegate

Now let’s do some performance benchmarking of the IComparable interface, the IComparer interface, and the Comparison delegate and see how they compare against each other.

For comparing the performance, we are going to use Benchmarkdotnet, which is a powerful .NET library that can transform methods into benchmarks and track their performance. As explained in the linked article, let’s configure the BenchmarkDotNet library and design the benchmark class.

Let’s create three benchmark methods that use the different approaches that we discussed earlier for comparing objects:

[MemoryDiagnoser]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByParams)]
public class ObjectComparisonBenchmark
{
    [Params(10000)]
    public int Count { get; set; }

    public Employee[] Employees { get; } = new Employee[5]
    {
        new Employee(4, "John"),
        new Employee(2, "Tom"),
        new Employee(1, "Eric"),
        new Employee(5, "Dan"),
        new Employee(3, "Alen")
    };

    [Benchmark]
    public void CompareUsingIComparable()
    {
        for (int i = 0; i < Count; i++)
        {
            Array.Sort(Employees);
        }
    }

    [Benchmark]
    public void CompareUsingIComparer()
    {
        for (int i = 0; i < Count; i++)
        {
            Array.Sort(Employees, Employee.SortByIdAscending());
        }
    }

    [Benchmark]
    public void CompareUsingComparisonDelegate()
    {
        for (int i = 0; i < Count; i++)
        {
            Array.Sort(Employees, Employee.CompareEmployeesByIdAscending);
        }
    }

}

Notice that we have marked the count field with the [Params] attribute and provided a value of 10000. This will perform the test 10000 times and display the results.

After we run the application in release configuration, it will do the performance benchmarking and print the results:

|                         Method | Count |     Mean |    Error |   StdDev |   Median |     Gen0 | Allocated |
|------------------------------- |------ |---------:|---------:|---------:|---------:|---------:|----------:|
|        CompareUsingIComparable | 10000 | 481.2 us | 10.59 us | 30.20 us | 472.9 us |        - |         - |
| CompareUsingComparisonDelegate | 10000 | 566.5 us | 11.22 us | 19.04 us | 558.3 us | 101.5625 |  640000 B |
|          CompareUsingIComparer | 10000 | 923.7 us | 28.34 us | 82.23 us | 900.9 us | 139.6484 |  880000 B |

When it performed the sorting 10000 times, we can see that the IComparable interface performed the fastest by taking just 481 microseconds without consuming additional memory. On the other hand, the comparison delegate took around 566 microseconds to achieve the same operation and it consumed some additional memory as well for that. However, the IComparer interface took more than 923 microseconds to perform this operation and consumed the most amount of memory.

Conclusion

In this article, we learned the IComparable interface, IComparer interface, and the Comparison delegate. We looked at how to use each of them and how they compare against each other.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!