オブジェクトの比較

比較ルールの定義

クラスを自作した場合、そのままではインスタンス同士を比較することはできません。
これはそのクラスには比較のためのメソッドが存在しないためです。
(等価の比較のみは限定的ながら可能です。後述)

配列のSortメソッドListのSortメソッドを使用するためには、オブジェクト同士を比較して「並べ替えの順序」を何らかの方法で決定できなければなりません。
数値型ならば単純に数値の大小、文字列型ならばABC順などですが、自作クラスの場合はその順序決定のルールも自分で定義する必要があります。

配列のSortメソッドListのSortメソッドでも説明していますが、これらのメソッドは並べ替え順序を決定するメソッドを引数に指定する方法もあります。

このページは比較用のメソッドによる比較の説明です。
演算子による比較を行う場合は演算子のオーバーロードの項を参照してください。

IComparable

自作クラスを比較可能にするにはIComparableインターフェイスを継承する方法があります。
IComparableインターフェイスは「int CompareTo(object obj)」というメソッドを持っていますので、これを派生クラスで定義します。

インターフェイスについてはインターフェイスを参照してください。


//IComparableの継承
class Test : IComparable
{
    public int Num;
    public string Str;

    public Test(int n, string s)
    {
        Num = n;
        Str = s;
    }

    //CompareToメソッドを実装
    public int CompareTo(object obj)
    {
        Test t = obj as Test;
        if (t == null)
            return 1;

        int n = num.CompareTo(t.Num);
        if (n != 0)
            return n;
        if (Str == null)
            return -1;
        return str.CompareTo(t.Str);
    }
}

static void Main(string[] args)
{
    List<Test> tests = new List<Test>()
    {
        new Test(1, "zzz"),
        new Test(2, "bbb"),
        new Test(3, "aaa"),
        new Test(1, "bbb"),
        new Test(3, "ccc"),
    };

    //Sortメソッドが使用可能
    tests.Sort();    

    foreach (var t in tests)
    {
        Console.WriteLine("{0} {1}", t.Num, t.Str);
    }

    Console.ReadLine();
}
1 bbb
1 zzz
2 bbb
3 aaa
3 ccc

CompareToメソッドは、自分自身と引数に指定した値とを比較して、自分自身が小さい場合は0未満を(順序が前になる)、自分自身のほうが大きければ0より大きい値を返します。
同じ値であった場合には0を返します。

int型やstring型などはCompareToメソッドを持っていますので、自作クラスのCompareToメソッド内でもこれを利用します。
(独自に比較ルールを定義することも可能です)
サンプルコードのCompareToメソッドでは、まずフィールドNumの値を比較対象にし、順序が同じだった場合にはフィールドStrで比較を行います。
フィールドStrも同じだった場合には同じ順序と判定されます。

IComparable<T>

IComparableインターフェイスにはジェネリック版も存在します。
IComparableジェネリックインターフェイスは「int CompareTo(T other)」というメソッドを持っていますので、これを派生クラスで定義します。
「T」には自作のクラス型を指定します。
引数の型が異なる以外はほぼ非ジェネリック版と同じです。

非ジェネリック版、ジェネリック版はどちらか一方の継承で構いません。


//IComparable<T>の継承
class Test : IComparable<Test>
{
    public int Num;
    public string Str;

    public Test(int n, string s)
    {
        Num = n;
        Str = s;
    }

    //CompareToメソッドを実装
    public int CompareTo(Test t)
    {
        if (t == null)
            return 1;

        int n = Num.CompareTo(t.Num);
        if (n != 0)
            return n;
        if (Str == null)
            return -1;
        return str.CompareTo(t.Str);
    }
}

static void Main(string[] args)
{
    //省略
}

IComparer<T>

IComparableインターフェイスは自作クラスの場合には有効な方法ですが、組み込み型やライブラリなどで定義されているデータ型はコードを変更できないためIComparableインターフェイスを実装できない場合があります。
その場合は比較のためのコードをIComparerインターフェイスを継承した別のクラスで実装します。

IComparerにもジェネリック版と非ジェネリック版がありますが、ほとんどの場合でジェネリック版の実装で良いでしょう。

IComparer<T>インターフェイス(IComparerのジェネリックインターフェイス)は「int Compare(T x, T y)」というメソッドを持っていますので、これを実装します。
「T」には比較したいクラスを指定します。
引数xが比較されるインスタンス(自分自身)、引数yが比較するインスタンスです。
処理自体はIComparableインターフェイスのCompareToメソッドとほとんど同じです。


//こんなクラスがライブラリに存在するとする
//このコードは変更できない
class Test
{
    public int Num;
    public string Str;

    public Test(int n, string s)
    {
        Num = n;
        Str = s;
    }
}

//Testクラスを比較するための
//クラスを定義
class TestComparer : IComparer<Test>
{
    public int Compare(Test t1, Test t2)
    {
        if (t1 == null && t2 == null)
            return 0;
        if (t1 == null)
            return -1;
        if (t2 == null)
            return 1;

        int n = t1.Num.CompareTo(t2.Mum);
        if (n != 0)
            return n;
        if (t1.Str == null)
            return -1;
        return t1.Str.CompareTo(t2.Str);
    }
}

static void Main(string[] args)
{
    List<Test> tests = new List<Test>()
    {
        new Test(1, "zzz"),
        new Test(2, "bbb"),
        new Test(3, "aaa"),
        new Test(1, "bbb"),
        new Test(3, "ccc"),
    };

    //TestComparerのインスタンスを引数に指定
    tests.Sort(new TestComparer());

    foreach (var t in tests)
    {
        Console.WriteLine("{0} {1} ", t.Num, t.Str);
    }

    Console.ReadLine();    
}

文字列の比較を行う場合は.NET標準のStringComparerの使用をおすすめします。

Equals

C#ではすべてのデータ型はobject型から派生しており、何も継承しない自作クラスを定義しても暗黙的にobject型を継承しています。
object型にはEqualsという比較のためのメソッドが定義されているため、何も定義していない自作クラスでもEqualsメソッドは使用できます。

Equalsメソッドはオブジェクト同士が等しい場合に真を返します。
データ型が値型であれば特に問題はありませんが、クラスなどの参照型の場合は「同じ参照元」を指している場合に等しいと判定されます。
そのままでは都合が悪い場合、Equalsメソッドをオーバーライドして動作を変更することができます。


class Test
{
    public int Num;
    public string Str;

    public Test(int n, string s)
    {
        Num = n;
        Str = s;
    }

    //Equalsメソッドのオーバーライド
    public override bool Equals(object obj)
    {
        Test t = obj as Test;
        if (t == null)
            return false;
        return Num == t.Num && Str == t.Str;
    }

    //Equalsメソッドをオーバーライドする場合
    //GetHashCodeメソッドもオーバーライドする
    public override int GetHashCode()
    {
        if (Str == null)
            return num;
        return Num ^ Str.GetHashCode();
    }
}

Equalsメソッドは「bool Equals(object obj)」という定義ですので、これをオーバーライドします。
派生元となるobject型はインターフェイス型ではないため、overrideキーワードが必要です。

サンプルコードではフィールドNumとStrの両方の値が等しい場合に真を返しています。

GetHashCodeメソッドのオーバーライド

Equalsメソッドをオーバーライドした場合、GetHashCodeというメソッドもオーバーライドします。
GetHashCodeメソッドはDictionaryクラスのキーなどで使用されるメソッドで、Equalsメソッドの動作を変更すると上手く動作しなくなる場合があるので一緒にオーバーライドしておきます。

GetHashCodeメソッドはEqualsメソッドが真を返すインスタンスからは常に同じ値を返すというものです。
この「値」をハッシュコードといいます。
ただし、偽を返す場合に同じハッシュコードが返ってくるのは構いません。
(違うインスタンス同士で同じハッシュコードになるのは良い)

実装の方法はいくつかあるようですが、広く使われているのは比較対象となるフィールド(またはプロパティ)のGetHashCodeの値を^演算子(XOR)でビット演算した値を返す、というものです。
ちなみにint型のGetHashCodeメソッドの戻り値はそのint型の値そのものになるのでGetHashCodeメソッドは使用しなくても構いません。

IEqualityComparer

Equalsのオーバーライドは自作メソッドの場合に可能ですが、組み込み型やライブラリなどで定義されているデータ型の場合はコードが変更できないためEqualsメソッドをオーバーライドできません。
Equalsメソッドと同等の定義が必要な場合はIEqualityComparerインターフェイスを継承したクラスを別に用意します。


//こんなクラスがライブラリに存在するとする
//このコードは変更できない
class Test
{
    public int Num;
    public string Str;

    public Test(int n, string s)
    {
        Num = n;
        Str = s;
    }
}

//Testクラスを比較するための
//クラスを定義
class TestComparer : IEqualityComparer<Test>
{
    public bool Equals(Test x, Test y)
    {
        if (x == null && y == null)
            return true;
        if (x == null || y == null)
            return false;
        return x.Num == y.Num && x.Str == y.Str;
    }

    public int GetHashCode(Test t)
    {
        if (t == null)
            return 0;
        if (t.Str == null)
            return t.num;
        return t.Num ^ t.Str.GetHashCode();
    }
}

static void Main(string[] args)
{
    List<Test> tests = new List<Test>()
    {
        new Test(1, "aaa"),
        new Test(2, "bbb"),
        new Test(1, "aaa")
    };

    //TestComparerのインスタンスを引数に指定
    IEnumerable<Test> distinct =
        tests.Distinct(new TestComparer());

    foreach (var x in distinct)
        Console.WriteLine("{0} {1}", x.Num, x.Str);

    Console.ReadLine();
}

IEqualityComparerインターフェイスでは「bool Equals(T x, T y)」と「int GetHashCode(T obj)」の二つが定義されているので、これらを実装します。
「T」には比較したいクラスを指定します。
引数xが比較されるインスタンス(自分自身)、引数yが比較するインスタンスです。
コードの中身はEqualsメソッドのオーバーライドの場合とほとんど同じです。

サンプルコードではフィールドNumとStrの両方の値が等しい場合に真を返しています。

例えばLINQDistinctメソッドは、要素が参照型の場合に「Equalsメソッドのオーバーライド」か「IEqualityComparerインターフェイス」のどちらかの方法を用いる必要があります。

文字列の比較を行う場合は.NET標準のStringComparerの使用をおすすめします。

StringComparer

上記は比較のルールを自作する場合の話でしたが、.NET FrameworkにはStringComparerという文字列比較のためのクラスが用意されています。
StringComparerはIComparerとIEqualityComparerを継承した抽象クラスで、以下のプロパティが存在します。
(Microsoft Docs)

CurrentCulture 現在のカルチャの単語ベースの比較規則を使用して、大文字と小文字を区別して文字列を比較する
CurrentCultureIgnoreCase 現在のカルチャの単語ベースの比較規則を使用して、大文字と小文字を区別せずに文字列を比較する
InvariantCulture インバリアント カルチャの単語ベースの比較規則を使用して、大文字と小文字を区別して文字列を比較する
tdInvariantCultureIgnoreCase インバリアント カルチャの単語ベースの比較規則を使用して、大文字と小文字を区別せずに文字列を比較する
Ordinal 大文字と小文字を区別して序数の文字列比較を実行する
OrdinalIgnoreCase 大文字と小文字を区別せずに序数の文字列比較を実行する

カルチャというのは、国や地域、言語によって異なるものをまとめた情報のことです。
(Culture=文化)
例えば日付の表記は日本とアメリカでは書式が異なります。
C#プログラムには「現在のカルチャ」が設定されており、それに基づいて書式が決定されます。

インバリアントカルチャというのは特定の国、地域、言語に依存しないカルチャのことです。
ベースは英語圏のカルチャとなっています。
特定のカルチャに依存しないので、多言語化を前提としたプログラムで共通して使用したいデータなどに利用されています。

序数の文字列比較とは、文字を内部的に定義されている順序通りに比較を行います。
例えば「a」と「A」では内部的には「A」のほうが先に定義されており、並び順が先になります。
(文字コード。Aは65番目、aは97番目に定義されている)
文字列の意味などは考慮しないので他のふたつより若干高速に動作します。

ほとんどの場合でOrdinal(IgnoreCase)かCurrentCulture(IgnoreCase)が使用されます。
「IgnoreCase」の有無は大文字小文字を無視するかしないかの違いです。
(Ignore=無視、Case=大文字小文字(UppweCase、LowerCase))

なお、これらのプロパティはStringComparerのインスタンスを返すので、newキーワードは必要ありません。
(というより抽象クラスなのでnewは使えません)


List<string> strs = new List<string>()
{
    "aaa", "bbb", "Aaa"
};

//大文字小文字を無視
IEnumerable<string> distinct =
    strs.Distinct(StringComparer.OrdinalIgnoreCase);