オブジェクトの比較

比較ルールの定義

クラスを自作した場合、そのままではインスタンス同士を比較することはできません。
これはそのクラスには比較のための方法が定義されていないためです。
(等値の比較のみは限定的ながら可能です。後述)

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

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

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

等値演算子による比較

比較処理の説明の前に、等値演算子(==)による比較を行った場合の挙動を説明しておきます。
(演算子のオーバーロードをしていない場合)

ユーザー定義クラス(自作クラス)に対する等値演算子は、同一のインスタンスを比較した場合(インスタンスの参照先が同じである場合)に真となります。
メンバが保存する値が同じでも異なるインスタンスの場合は偽となります。


class Test
{
    public int Num;
    public string Str;

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

static Test F(Test t)
{
    //引数に受け取ったインスタンスをそのまま返す
    return t;
}

static void Main(string[] args)
{
    Test t1 = new Test(1, "abc");
    Test t2 = new Test(1, "abc");

    Console.WriteLine(t1 == F(t1));
    Console.WriteLine(t1 == t2);
    Console.ReadLine();
}
True
False

参照型変数のコピーは参照情報のコピーなので、関数の引数や戻り値等でインスタンスが受け渡しされても参照情報は変わりません。

自作の構造体の場合は、演算子オーバーロードをしなければ等値演算子は使用できません。

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を返します。
0未満を返した場合、自分自身は引数の値よりも並び順が前になることを意味します。

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

int型などの組み込み型は内部的には構造体として実装されており、この構造体はIComparableを継承して作られています。
つまりCompareToメソッドも定義されています。

配列、ListクラスのSortメソッドは、対象がIComparable派生クラスである場合はCompareToメソッドを呼び出して並べ替えを実行します。

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)
        {
            if(t2 == null)
                return 0;
            return -1;
        }
        if(t2 == null)
            return 1;

        int n = t1.Num.CompareTo(t2.Num);
        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メソッドはpublic virtual bool Equals(object obj)という定義なので、これをオーバーライドします。
派生元となるobject型はインターフェイス型ではないため、overrideキーワードが必要です。

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

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

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

GetHashCodeメソッドはEqualsメソッドが真を返すインスタンスからは常に同じ値を返すというものです。
この「値」をハッシュコード(ハッシュ値)といいます。

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

ハッシュコードは、同じ値を持つオブジェクト同士では必ず一致しなければなりませんが、異なる値を持つオブジェクト同士ではハッシュコードが一致してはならない、というものではありません。
例えば整数値を100で剰余した値をハッシュコードとする場合、「1」と「101」が同じハッシュコード(1)になりますが、このような実装があっても構いません。

そのため、ハッシュコードはオブジェクトの同一性を判定するためには使えないことに注意してください。
またその実装や値そのものに依存するようなコードはバグの原因となります。

ReferenceEqualsメソッド

Equalsメソッドと似ているものにobject.ReferenceEqualsという静的メソッドがあります。
このメソッドは「オブジェクトの参照元が同じか否か」を判定します。
つまり同じ値というだけでなく、オブジェクトそのものが同一である場合に真が返されます。

この動作はEqualsメソッドの規定の動作と同じですが、Equalsメソッドはobject型内では仮想関数(virtual)で定義されているので、派生クラスでオーバーライドされる可能性があります。
Equalsメソッドには静的メソッド版(object.Equals)がありますが、これも内部的にインスタンスメソッドのEqualsメソッドを呼び出しているため、派生クラスで動作が変更される可能性があります。
ReferenceEqualsメソッドはEqualsメソッドのオーラーライドに影響を受けず、このメソッド自身のオーバーライドもできないので、常にオブジェクトの同一性を判定することができます。


class Test
{
    public int Num;

    public override bool Equals(object other)
    {
        Test t = other as Test;
        if(t == null)
            return false;
        return Num == t.Num;
    }

    public override int GetHashCode() { return Num; }
}

static void Main(string[] args)
{
    Console.WriteLine(
        "object.Equals(test1, test1) -> {0}",
        object.Equals(test1, test1));
    Console.WriteLine(
        "object.Equals(test1, test2) -> {0}",
        object.Equals(test1, test2));
    Console.WriteLine(
        "object.ReferenceEquals(test1, test1) -> {0}",
        object.ReferenceEquals(test1, test1));
    Console.WriteLine(
        "object.ReferenceEquals(test1, test2) -> {0}",
        object.ReferenceEquals(test1, test2));

    Console.ReadLine();
}
object.Equals(test1, test1) -> True
object.Equals(test1, test2) -> True
object.ReferenceEquals(test1, test1) -> True
object.ReferenceEquals(test1, test2) -> False

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)
        {
            if(y == null)
                return true;
            return false;
        }
        if(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メソッドのオーバーライドの場合とほとんど同じです。
サンプルコードではフィールドNumStrの両方の値が等しい場合に真を返しています。

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

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

StringComparer

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

CurrentCulture 現在のカルチャの単語ベースの比較規則を使用して、大文字と小文字を区別して文字列を比較する
CurrentCultureIgnoreCase 現在のカルチャの単語ベースの比較規則を使用して、大文字と小文字を区別せずに文字列を比較する
InvariantCulture インバリアント カルチャの単語ベースの比較規則を使用して、大文字と小文字を区別して文字列を比較する
InvariantCultureIgnoreCase インバリアント カルチャの単語ベースの比較規則を使用して、大文字と小文字を区別せずに文字列を比較する
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);