Back to Blog
C#

C# Comparable: Implementing IComparable for Sorting

Learn how to implement IComparable<T> in C# to sort custom objects with List.Sort and Array.Sort, including null handling and common edge cases.

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. IComparable<T> gives you a way to define that ordering.

The Problem: Sorting Custom Objects

Without a comparison contract, List<Person>.Sort() still compiles, but it throws at runtime when it tries to compare elements because the default comparer does not know how to order Person objects. IComparable<T> 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; // a non-null instance is greater than null return LastName.CompareTo(other.LastName); } }

The CompareTo method is called by sorting algorithms to determine the order. When other is null, the .NET convention is that a non-null instance is greater than null, so returning a positive value is correct. In an ascending sort, null values therefore appear before non-null values.

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 a null argument explicitly. The .NET documentation states that any reference-type instance compares greater than null. string.CompareTo already handles nulls, but for custom fields you need to be explicit.

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

public int CompareTo(Product? other) { if (other is null) return 1; // This example sorts missing prices before real prices. if (Price is null && other.Price is null) return 0; if (Price is null) return -1; if (other.Price is null) return 1; return Price.Value.CompareTo(other.Price.Value); }

This avoids null reference exceptions and gives missing prices a deterministic position. If you want missing prices to sort last instead, swap the -1 and 1 branches.

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 required to match Equals. When CompareTo returns 0, sorted collections such as SortedSet<T> and SortedDictionary<TKey,TValue> treat two instances as equal for ordering even if Equals returns false. That can be surprising, so choose a comparison rule that matches the semantics your callers expect. For hash-based collections, there is a separate rule: objects that Equals considers equal must return the same hash code.

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 for every comparison instead of comparing the fields that define the intended order. That breaks the ordering contract because CompareTo must be transitive and consistent in both directions; for example, if A.CompareTo(B) and B.CompareTo(A) both return 1, the ordering is contradictory. Compare the underlying fields, or combine them with a clearly defined rule.

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: Sorting Custom Objects with IComparable<T> | RYUSLOG DEV