【C#】IComparableを実装して自作クラスの大小比較とソートを行う

C#のList<T>.Sort()メソッドやArray.Sort()メソッドは、数値や文字列などの基本的な型であればそのまま機能しますが、自作のクラスや構造体に対して呼び出すと、どのように順序を付ければよいか分からず例外(InvalidOperationException)が発生します。

自作の型に対して「自然な順序(デフォルトのソート順)」を定義するためには、IComparable<T>インターフェイスを実装する必要があります。今回は、ソフトウェアのバージョン番号(メジャー、マイナー、ビルド)を管理する構造体を題材に、階層的な大小比較の実装方法を解説します。


目次

IComparable<T>インターフェイスの概要

IComparable<T>インターフェイスは、その型のインスタンス同士が比較可能であることを示します。実装する必要があるのは CompareTo(T other) メソッドひとつだけです。

このメソッドは、比較結果を以下の整数値で返すルールになっています。

  • 0より小さい値 (通常 -1): 自身が比較対象より小さい(前にある)
  • 0: 自身と比較対象は等しい
  • 0より大きい値 (通常 1): 自身が比較対象より大きい(後ろにある)

実践的なコード例:バージョン番号のソート

以下のコードは、メジャーバージョン、マイナーバージョン、ビルド番号を持つSoftwareVersion構造体にIComparable<T>を実装し、リストを正しい順序でソートする例です。

比較ロジックでは、「まずメジャー番号を比較し、同じならマイナー番号、さらに同じならビルド番号を比較する」という段階的な判定を行っています。

using System;
using System.Collections.Generic;

namespace VersionControl
{
    class Program
    {
        static void Main()
        {
            // バージョン情報のリスト(順序はバラバラ)
            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. Sortメソッドの呼び出し
            // SoftwareVersionがIComparable<T>を実装しているため、自動的にソートされます。
            versions.Sort();

            // 結果の出力
            Console.WriteLine("--- バージョン昇順ソート ---");
            foreach (var v in versions)
            {
                Console.WriteLine(v);
            }

            // (参考) 降順にしたい場合はReverseを使うか、比較ロジックを逆にします
            // versions.Reverse();
        }
    }

    /// <summary>
    /// ソフトウェアのバージョンを表す構造体
    /// IComparable<T>を実装して大小比較可能にします。
    /// </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;
        }

        // --- IComparable<T> の実装 ---

        /// <summary>
        /// 自身と他のバージョンを比較します。
        /// </summary>
        /// <param name="other">比較対象</param>
        /// <returns>大小関係を表す整数 (-1, 0, 1)</returns>
        public int CompareTo(SoftwareVersion other)
        {
            // 1. メジャーバージョンの比較
            // int.CompareToの結果をそのまま利用します
            int majorCompare = this.Major.CompareTo(other.Major);
            
            // 0以外(大小が決まった)ならその結果を返す
            if (majorCompare != 0)
            {
                return majorCompare;
            }

            // 2. メジャーが同じならマイナーバージョンの比較
            int minorCompare = this.Minor.CompareTo(other.Minor);
            if (minorCompare != 0)
            {
                return minorCompare;
            }

            // 3. マイナーも同じならビルド番号の比較
            return this.Build.CompareTo(other.Build);
        }

        // 出力用にToStringをオーバーライド
        public override string ToString()
        {
            return $"{Major}.{Minor}.{Build}";
        }
    }
}

実行結果

--- バージョン昇順ソート ---
0.9.9
1.0.0
1.5.0
1.5.2
2.1.5

技術的なポイント

1. 非ジェネリック版との違い

C#にはIComparable(非ジェネリック)とIComparable<T>(ジェネリック)の両方が存在します。 現代のC#開発では、必ずジェネリック版のIComparable<T>を実装してください。非ジェネリック版は引数がobject型であるため、キャストのコストやボクシング(値型の場合)が発生し、パフォーマンスが低下する原因となります。また、型安全性も失われます。

2. 数値化による比較の注意点

入力された題材のコードでは、時刻を Hour * 10000 + ... のように一つの整数に変換して比較していました。これはデータ範囲が明確でオーバーフローしない場合には有効なテクニック(高速化手法)です。 しかし、汎用的なクラス設計においては、上記のコード例のようにフィールドごとの CompareTo を順次呼び出す方法が最も安全で確実です。これにより、桁溢れのリスクを回避できます。

3. IComparer<T>との使い分け

  • IComparable<T>: クラス自身に「私はこうやって比較される」という既定の順序を持たせる場合に使用します(例:バージョン番号、日付、金額)。
  • IComparer<T>: クラスの外側から「今回はID順、今回は名前順」のように、特定の場面での比較ルールを与えたい場合に使用します。

まとめ

IComparable<T>を実装することで、自作クラスに「順序」という概念を与えることができます。これにより、List<T>.Sort()だけでなく、LINQのOrderByBinarySearchなど、順序を前提とした多くの機能が利用可能になります。値オブジェクト(Value Object)を設計する際は、実装を検討すべき重要なインターフェイスです。

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

この記事を書いた人

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

目次