Back to Blog
C#

C# Comparable: Implementing IComparable for Sorting

c# comparable: Learn how to implement IComparable<T> in C# to sort custom objects with List.Sort, Array.Sort, and handle edge cases like nulls.

IComparableC# SortingCompareToIComparer
A C# code snippet showing a class implementing IComparable<T> with a sorting arrow icon in the background.

When you call List<T>.Sort() on a list of integers, the runtime knows how to order them because int implements IComparable<int>. For your own classes, there is no default ordering. The c# comparable pattern gives you a way to define that ordering by implementing the IComparable<T> interface.

The Problem: Sorting Custom Objects

Without a comparison contract, sorting a list of custom objects fails at compile time or throws an exception. For example, List<Person>.Sort() will not compile unless Person implements IComparable<Person> or you pass a comparer. The IComparable<T> interface is the standard mechanism for defining the natural sort order of a type.

Implementing IComparable<T>

The interface requires a single method: CompareTo(T? other). It returns an integer that indicates the relative order of the current instance and the other object. A negative value means the current instance precedes the other, zero means they are equal, and a positive value means the current instance follows the other.

Here is a minimal implementation for a Person class that sorts by LastName:

public class Person : IComparable<Person> { public string LastName { get; set; } public string FirstName { get; set; } public int CompareTo(Person? other) { if (other is null) return 1; // this instance follows null return LastName.CompareTo(other.LastName); } }

The CompareTo method is called by sorting algorithms to determine the order. When other is null, the convention is that a non-null object precedes null, so returning a positive value is correct.

Using CompareTo with Built-in Sorting Methods

Once your class implements IComparable<T>, you can sort collections without extra arguments:

var people = new List<Person> { new Person { LastName = "Smith", FirstName = "John" }, new Person { LastName = "Adams", FirstName = "Jane" } }; people.Sort();

The List<T>.Sort() method uses the default comparer, which relies on IComparable<T> if implemented. The same works for Array.Sort, SortedSet<T>, and other collection types that require an ordering.

Handling Null and Edge Cases

The CompareTo method must handle null explicitly. The .NET documentation states that any instance compares greater than null. If you compare two strings, string.CompareTo already handles nulls, but for custom fields you need to be careful.

Consider a Product class that sorts by Price, which is a decimal?:

public int CompareTo(Product? other) { if (other is null) return 1; // Null prices are treated as less than any numeric price return (Price ?? decimal.MinValue).CompareTo(other.Price ?? decimal.MinValue); }

This approach avoids null reference exceptions and defines a deterministic ordering for missing values.

IComparable vs IComparer: When to Use Which

IComparable<T> defines the natural sort order for a type. It is useful when there is one obvious way to order instances. However, you often need multiple sort orders, such as sorting by name, date, or price. In that case, implement IComparer<T> as separate classes and pass them to the sort method.

CriterionIComparable<T>IComparer<T>
PurposeNatural orderingAlternative orderings
LocationOn the type itselfSeparate class
Usagelist.Sort()list.Sort(new MyComparer())
FlexibilityOne order per typeMultiple orders possible

Use IComparable<T> when the type has a single, obvious ordering. Use IComparer<T> when you need to sort the same type in different ways or when you cannot modify the type.

Performance and Allocation Considerations

The CompareTo method is called many times during a sort, typically O(n log n) times. Keeping the implementation lightweight matters for large collections. Avoid allocating new objects inside CompareTo, such as creating strings or boxing value types. Compare primitive fields directly.

For example, comparing two int fields is cheap, but comparing two strings uses culture-sensitive comparison by default. If you only need ordinal comparison, use StringComparer.Ordinal.Compare or string.CompareOrdinal to avoid culture overhead.

Also, be aware that CompareTo is not necessarily consistent with Equals. If two objects compare as equal (return 0), they are not required to have the same hash code. For sorting, consistency with Equals is not required, but it is recommended to avoid surprising behavior in collections like SortedSet or SortedDictionary.

Common Mistakes When Implementing CompareTo

One frequent mistake is ignoring the null parameter. If you call other.SomeProperty without checking for null, you get a NullReferenceException during sorting when the collection contains null elements.

Another mistake is returning a constant value like -1 or 1 instead of delegating to a field's CompareTo. That breaks the transitive property required for correct sorting. For example, if you have A.CompareTo(B) and B.CompareTo(C), the result must be consistent with A.CompareTo(C). Always compare the underlying fields.

Finally, be careful with floating-point values. double.NaN does not compare equal to itself, which can break sorting. If your type contains double or float, decide how to handle NaN explicitly.

c# comparable: Practical Usage and Code Examples | RYUSLOG DEV