[C#] Implementing IComparable for Custom Class Comparison and Sorting

C#’s List<T>.Sort() and Array.Sort() methods work out-of-the-box for basic types like numbers and strings. However, if you call them on a custom class or struct, an exception (InvalidOperationException) occurs because the program doesn’t know how to order them.

To define a “natural order (default sort order)” for a custom type, you need to implement the IComparable<T> interface. In this article, I will explain how to implement hierarchical comparison logic using a structure that manages software version numbers (Major, Minor, Build) as an example.

目次

Table of Contents

  1. Overview of the IComparable<T> Interface
  2. Practical Code Example: Sorting Version Numbers
  3. Execution Result
  4. Technical Points
    1. Difference from the Non-Generic Version
    2. Precautions When Comparing via Numeric Conversion
    3. Distinction from IComparer<T>
  5. Summary

Overview of the IComparable<T> Interface

The IComparable<T> interface indicates that instances of that type can be compared with each other. You only need to implement a single method: CompareTo(T other).

The rule for this method is to return the comparison result as an integer:

  • Less than 0 (usually -1): This instance is smaller than (precedes) the comparison target.
  • 0: This instance is equal to the comparison target.
  • Greater than 0 (usually 1): This instance is larger than (follows) the comparison target.

Practical Code Example: Sorting Version Numbers

The following code implements IComparable<T> on a SoftwareVersion struct that holds Major, Minor, and Build numbers, enabling a list to be sorted in the correct order.

The comparison logic performs a step-by-step evaluation: “First compare the Major number; if equal, compare the Minor number; if still equal, compare the Build number.”

using System;
using System.Collections.Generic;

namespace VersionControl
{
    class Program
    {
        static void Main()
        {
            // List of version info (unordered)
            var versions = new List<SoftwareVersion>
            {
                new SoftwareVersion(1, 0, 0),
                new SoftwareVersion(2, 1, 5),
                new SoftwareVersion(1, 5, 2),
                new SoftwareVersion(1, 5, 0),
                new SoftwareVersion(0, 9, 9)
            };

            // 1. Call the Sort method
            // Since SoftwareVersion implements IComparable<T>, it sorts automatically.
            versions.Sort();

            // Output results
            Console.WriteLine("--- Sorted by Version Ascending ---");
            foreach (var v in versions)
            {
                Console.WriteLine(v);
            }

            // (Reference) To sort descending, use Reverse or invert the comparison logic
            // versions.Reverse();
        }
    }

    /// <summary>
    /// Struct representing software version.
    /// Implements IComparable<T> to enable size comparison.
    /// </summary>
    public readonly struct SoftwareVersion : IComparable<SoftwareVersion>
    {
        public int Major { get; }
        public int Minor { get; }
        public int Build { get; }

        public SoftwareVersion(int major, int minor, int build)
        {
            Major = major;
            Minor = minor;
            Build = build;
        }

        // --- Implementation of IComparable<T> ---

        /// <summary>
        /// Compares this instance with another version.
        /// </summary>
        /// <param name="other">Comparison target</param>
        /// <returns>Integer representing relative order (-1, 0, 1)</returns>
        public int CompareTo(SoftwareVersion other)
        {
            // 1. Compare Major version
            // Use the result of int.CompareTo directly
            int majorCompare = this.Major.CompareTo(other.Major);
            
            // If not 0 (order determined), return the result
            if (majorCompare != 0)
            {
                return majorCompare;
            }

            // 2. If Major is same, compare Minor version
            int minorCompare = this.Minor.CompareTo(other.Minor);
            if (minorCompare != 0)
            {
                return minorCompare;
            }

            // 3. If Minor is also same, compare Build number
            return this.Build.CompareTo(other.Build);
        }

        // Override ToString for output
        public override string ToString()
        {
            return $"{Major}.{Minor}.{Build}";
        }
    }
}

Execution Result

--- Sorted by Version Ascending ---
0.9.9
1.0.0
1.5.0
1.5.2
2.1.5

Technical Points

1. Difference from the Non-Generic Version

C# has both IComparable (non-generic) and IComparable<T> (generic). In modern C# development, always implement the generic version IComparable<T>. The non-generic version takes an object argument, causing boxing (for value types) and casting costs, which degrades performance. It also lacks type safety.

2. Precautions When Comparing via Numeric Conversion

Sometimes, multiple fields are combined into a single integer for comparison (e.g., Major * 10000 + Minor...). While this is a valid technique for optimization when the data range is clear and will not overflow, it carries risks. In general class design, the method of sequentially calling CompareTo for each field (as shown in the code example) is the safest and most reliable approach. This avoids the risk of integer overflow.

3. Distinction from IComparer<T>

  • IComparable<T>: Used when the class itself has a defined “default order” (e.g., version numbers, dates, monetary amounts).
  • IComparer<T>: Used when you want to apply specific comparison rules from outside the class, such as “Sort by ID this time, Sort by Name next time.”

Summary

By implementing IComparable<T>, you give your custom class the concept of “order.” This enables the use of many features that rely on order, such as List<T>.Sort(), LINQ’s OrderBy, and BinarySearch. This is a critical interface to consider implementing when designing Value Objects.

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次