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のOrderByやBinarySearchなど、順序を前提とした多くの機能が利用可能になります。値オブジェクト(Value Object)を設計する際は、実装を検討すべき重要なインターフェイスです。
