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.
Let’s start.
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.