Object.Equalsメソッドを使うことで二つのオブジェクトが等しいかどうか単純な比較を行うことが出来ますが、どのような時に等しいとみなすか定義することが必要になる場合があります。

ここでは、オブジェクトの等価性を定義したり比較するためのインターフェイスであるIEquatable・IEqualityComparerと、関連するクラスについて見ていきます。 また、等価演算子のオーバーロードとこれらのインターフェイスの実装についても触れています。

§1 Object.Equals

まずは、Object.Equalsメソッドとオブジェクトの等価性について見ていきます。 Equalsメソッドは現在のインスタンスと引数で指定されたオブジェクトとの等価性を比較するメソッドですが、Equalsメソッドをオーバーライドしない場合のデフォルトの動作は次のようになっています。 (値型と参照型 §.同値性・同一性の比較)

値型(struct)の場合
二つの値型オブジェクトがビット単位で等しい場合にtrue、そうでなければfalseを返す
参照型(class)の場合
二つの参照型オブジェクトが同一のオブジェクト(=参照先のオブジェクトが同一)である場合にtrue、そうでなければfalseを返す

そのため、次の例ではAccountクラスはEqualsメソッドをオーバーライドしていないため、各フィールドが同じ値を持つインスタンスでも異なるインスタンスの場合である限り等しいとは判断されません。 これにより、Array.IndexOfメソッドによる検索でも配列内には同一のインスタンスは存在しないため-1が返されます。

using System;

class Account {
  int id;
  string name;

  public Account(int id, string name)
  {
    this.id = id;
    this.name = name;
  }

  public override string ToString()
  {
    return string.Format("{0}:{1}", id, name);
  }
}

class Sample {
  static void Main()
  {
    Account[] arr = new Account[] {
      new Account(1, "Eve"),
      new Account(4, "Dave"),
      new Account(2, "Alice"),
      new Account(0, "Charlie"),
      new Account(3, "Bob"),
    };

    Account charlie = new Account(0, "Charlie");

    Console.WriteLine("'{0}' Equals '{1}': {2}", charlie, arr[3], charlie.Equals(arr[3]));
    Console.WriteLine("IndexOf '{0}': {1}", charlie, Array.IndexOf(arr, charlie));
  }
}
実行結果
'0:Charlie' Equals '0:Charlie': False
IndexOf '0:Charlie': -1

Equalsメソッドをオーバーライドすることで、どのような場合にオブジェクトが等しいとするかを定義することが出来ます。 次の例は、上記の例を書き換えEqualsメソッドをオーバーライドするようにしたものです。

using System;

class Account {
  int id;
  string name;

  public Account(int id, string name)
  {
    this.id = id;
    this.name = name;
  }

  public override string ToString()
  {
    return string.Format("{0}:{1}", id, name);
  }

  public override bool Equals(object obj)
  {
    // Accountクラスに型変換
    Account other = obj as Account;

    if (other == null) {
      // objがAccountクラスでない場合、またはnullの場合は等しくないとする
      return false;
    }
    else {
      // objがAccountクラスの場合、かつフィールドidとnameの値が等しい場合は、
      // 2つのオブジェクトが等しいとする
      return this.id == other.id && this.name == other.name;
    }
  }
}

class Sample {
  static void Main()
  {
    Account[] arr = new Account[] {
      new Account(1, "Eve"),
      new Account(4, "Dave"),
      new Account(2, "Alice"),
      new Account(0, "Charlie"),
      new Account(3, "Bob"),
    };

    Account charlie = new Account(0, "Charlie");

    Console.WriteLine("'{0}' Equals '{1}': {2}", charlie, arr[3], charlie.Equals(arr[3]));
    Console.WriteLine("IndexOf '{0}': {1}", charlie, Array.IndexOf(arr, charlie));
  }
}
実行結果
'0:Charlie' Equals '0:Charlie': True
IndexOf '0:Charlie': 3

結果を見て分かる通り、二つの異なるインスタンスでも各フィールドの値が等しければ、二つのインスタンスは等しいと扱われるようになりました。 また、これにより、Array.IndexOfメソッドによる検索でも配列内にある等しいインスタンスのインデックスが返されるようになりました。

なお、Equalsメソッドをオーバーライドする場合はObject.GetHashCodeメソッドもオーバーライドする必要があります。 そのため、上記の例をコンパイルすると警告(CS0659)が出ます。 GetHashCodeが返す値は値型では特に重要になりますが、ここでは実装を省略しています。 詳しくはGetHashCodeメソッドの解説を参照してください。



§2 IEquatable<T>

IEquatable<T>インターフェイス(System名前空間)は、型パラメータTで指定された型との等価性の比較が可能であることを表すインターフェイスです。 Object.Equalsメソッドがobject型の引数を取るのに対し、IEquatable<T>.Equalsメソッドでは型Tを引数に取ります。 このため、Object.Equalsメソッドをオーバーライドする場合とは異なり、型チェックの必要が無くなります。

以下の例は、先の例をIEquatable<T>インターフェイスを実装したものに書き換えたものです。

using System;

class Account : IEquatable<Account> {
  int id;
  string name;

  public Account(int id, string name)
  {
    this.id = id;
    this.name = name;
  }

  public override string ToString()
  {
    return string.Format("{0}:{1}", id, name);
  }

  // IEquatable<Account>.Equalsの実装
  public bool Equals(Account other)
  {
    if (other == null) return false;

    return this.id == other.id && this.name == other.name;
  }
}

class Sample {
  static void Main()
  {
    Account[] arr = new Account[] {
      new Account(1, "Eve"),
      new Account(4, "Dave"),
      new Account(2, "Alice"),
      new Account(0, "Charlie"),
      new Account(3, "Bob"),
    };

    Account charlie = new Account(0, "Charlie");

    Console.WriteLine("'{0}' Equals '{1}': {2}", charlie, arr[3], charlie.Equals(arr[3]));
    Console.WriteLine("IndexOf '{0}': {1}", charlie, Array.IndexOf(arr, charlie));

    // objectにキャスト
    object obj = charlie;

    Console.WriteLine("'{0}' Equals '{1}': {2}", obj, arr[3], obj.Equals(arr[3]));
  }
}
実行結果
'0:Charlie' Equals '0:Charlie': True
IndexOf '0:Charlie': -1
'0:Charlie' Equals '0:Charlie': False

単純にAccount.Equalsメソッドを呼び出した比較は正しく動作していますが、結果を見て分かる通りArray.IndexOfメソッドの結果は意図に反して-1を返しています。 これは、Array.IndexOfメソッドではオブジェクトがIEquatable<T>.Equalsメソッドを実装しているかどうかに関わらず、常にObject.Equalsメソッドを使って等価性の比較を行うためです。 AccountクラスはObject.Equalsをオーバーライドしていないため、このような結果となります。 objectにキャストしてからEqualsメソッドで比較した場合も同様に、意図に反してfalseを返しています。

このように、型がIEquatable<T>を実装していても、比較する側がIEquatable<T>.Equalsメソッドを呼び出すようになっていない限り、意図しない結果となります。 そのため、多くの場合はIEquatable<T>を実装すると同時に、Object.Equalsメソッドもオーバーライドすることになります。 以下の例は、上記の例にObject.Equalsメソッドのオーバーライドを追加したものです。

using System;

class Account : IEquatable<Account> {
  int id;
  string name;

  public Account(int id, string name)
  {
    this.id = id;
    this.name = name;
  }

  public override string ToString()
  {
    return string.Format("{0}:{1}", id, name);
  }

  // IEquatable<Account>.Equalsの実装
  public bool Equals(Account other)
  {
    if (other == null) return false;

    return this.id == other.id && this.name == other.name;
  }

  // Object.Equalsをオーバーライド
  public override bool Equals(object obj)
  {
    // Accountに型変換
    Account other = obj as Account;

    // 型変換出来ない場合、または引数がnullの場合はfalse
    if (other == null) return false;

    // 型変換出来た場合は、IEquatable<Account>.Equalsを使って比較する
    return Equals(other);
  }
}

class Sample {
  static void Main()
  {
    Account[] arr = new Account[] {
      new Account(1, "Eve"),
      new Account(4, "Dave"),
      new Account(2, "Alice"),
      new Account(0, "Charlie"),
      new Account(3, "Bob"),
    };

    Account charlie = new Account(0, "Charlie");

    Console.WriteLine("'{0}' Equals '{1}': {2}", charlie, arr[3], charlie.Equals(arr[3]));
    Console.WriteLine("IndexOf '{0}': {1}", charlie, Array.IndexOf(arr, charlie));

    // objectにキャスト
    object obj = charlie;

    Console.WriteLine("'{0}' Equals '{1}': {2}", obj, arr[3], obj.Equals(arr[3]));
  }
}
実行結果
'0:Charlie' Equals '0:Charlie': True
IndexOf '0:Charlie': 3
'0:Charlie' Equals '0:Charlie': True

§3 IEqualityComparer, IEqualityComparer<T>

IEqualityComparerインターフェイス(System.Collections名前空間)とIEqualityComparer<T>インターフェイス(System.Collections.Generic名前空間)は、等価性の比較処理を提供するためのインターフェイスです。 IEquatableとIEqualityComparerの関係はIComparableとIComparerの関係に似ていて、IEquatableではインターフェイスを実装する型に比較処理を実装するのに対し、IEqualityComparerでは比較される型とは別に比較処理を実装することが出来ます。

Hashtableはキーの比較にObject.Equals、DictionaryはObject.EqualsまたはIEquatable<T>.Equalsをデフォルトで使用しますが、コンストラクタでキーの比較時に使用するIEqualityComparer・IEqualityComparer<T>を指定することが出来ます これにより、キーとなる型でこれらを実装・オーバーライドする代わりに指定したIEqualityComparer・IEqualityComparer<T>を用いてキーの比較処理を行わせるようにすることが出来ます。

インターフェイスの詳細と実装例は省略します。 次に解説するEqualityComparer<T>クラスを参照してください。

§4 EqualityComparer<T>

EqualityComparer<T>クラス(System.Collections.Generic名前空間)は、IEqualityComparer非ジェネリックインターフェイスとIEqualityComparer<T>ジェネリックインターフェイスを実装する抽象クラスです。 Comparer<T>クラスと同様、IEqualityCompareとIEqualityComparer<T>の二つのインターフェイスを実装するクラスを作成しなくても、このクラスを継承して抽象メソッドであるEqualsメソッドGetHashCodeメソッドを実装するだけで同じ機能を提供することができるようになります。

EqualityComparer<T>.EqualsメソッドはT型の引数を二つ取り、等しい場合はtrue、そうでない場合はfalseを返すように実装します。 EqualityComparer<T>.GetHashCodeメソッドはT型の引数を一つ取り、引数で指定されたオブジェクトのハッシュ値を返すように実装します。 なお、二つのオブジェクトxとyについて、Equalsメソッドがtrueとなる場合はGetHashCodeメソッドがxとyに対して同じ値を返すように実装しなければなりません。

以下の例では、EqualityComparer<T>を継承したクラスを作成し、Dictionaryでのキーの比較に使用しています。 AccountクラスではIEquatable<T>やEqualsメソッドをオーバーライドしていない点に注目してください。

using System;
using System.Collections.Generic;

class Account {
  public int ID;
  public string Name;

  public Account(int id, string name)
  {
    this.ID = id;
    this.Name = name;
  }

  public override string ToString()
  {
    return string.Format("{0}:{1}", ID, Name);
  }
}

// AccountクラスのためのEqualityComparer
class AccountComparer : EqualityComparer<Account> {
  public override bool Equals(Account x, Account y)
  {
    // xとyがともにnullの場合は等しいものとする
    if (x == null && y == null) return true;
    // xとyのどちらかがnullの場合は異なるものとする
    else if (x == null || y == null) return false;

    // フィールドIDが等しければ二つのAccountは等しいものとする
    return x.ID == y.ID;
  }

  public override int GetHashCode(Account obj)
  {
    // nullの場合はArgumentNullExceptionをスローする
    if (obj == null) throw new ArgumentNullException("obj");

    // ハッシュ値にフィールドIDの値を使用する
    return obj.ID;
  }
}

class Sample {
  static void Main()
  {
    // アカウントとコンタクト先のディクショナリ
    Dictionary<Account, Uri> dict = new Dictionary<Account, Uri>(new AccountComparer());

    Account alice   = new Account(2, "Alice");
    Account bob     = new Account(0, "Bob");
    Account charlie = new Account(1, "Charlie");

    dict[alice]   = new Uri("mailto:alice@example.net");
    dict[bob]     = new Uri("mailto:bob@example.net");
    dict[charlie] = new Uri("mailto:charlie@example.net");

    foreach (KeyValuePair<Account, Uri> pair in dict) {
      Console.WriteLine("{0,-10} => {1}", pair.Key, pair.Value);
    }

    Console.WriteLine();

    // IDフィールドはBobと同じ、NameフィールドはBobと異なるインスタンスdaveを作成
    Account dave = new Account(0, "Dave");

    // キーbob, daveに該当する要素が存在するかどうか
    Console.WriteLine("ContainsKey '{0}': {1}", bob,  dict.ContainsKey(bob));
    Console.WriteLine("ContainsKey '{0}': {1}", dave, dict.ContainsKey(dave));

    // キーdaveに該当する要素の値を上書き
    dict[dave] = new Uri("http://exmaple.net/~dave/");

    foreach (KeyValuePair<Account, Uri> pair in dict) {
      Console.WriteLine("{0,-10} => {1}", pair.Key, pair.Value);
    }
  }
}
実行結果
2:Alice    => mailto:alice@example.net
0:Bob      => mailto:bob@example.net
1:Charlie  => mailto:charlie@example.net

ContainsKey '0:Bob': True
ContainsKey '0:Dave': True
2:Alice    => mailto:alice@example.net
0:Bob      => http://exmaple.net/~dave/
1:Charlie  => mailto:charlie@example.net

この例ではジェネリックなコレクションであるDictionaryを使っていますが、非ジェネリックなコレクションであるHashtableを使ってもDictionaryと同様に動作します。

EqualityComparer<T>のその他の使用例としてはプロパティ §.プロパティ変更の通知 (INotifyPropertyChanged)の例もご覧ください。

§5 StringComparer

既に解説したStringComparerクラスは、IComparer・IComparer<string>だけでなくIEqualityComparer・IEqualityComparer<string>も実装しています。 以下の例では、StringComparerを使って大文字小文字を無視するDictionaryを作成しています。 StringComparerの詳細については、StringComparerについての個別の解説を参照してください。

using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    Dictionary<string, Uri> dict = new Dictionary<string, Uri>(StringComparer.CurrentCultureIgnoreCase);

    dict["Alice"]   = new Uri("mailto:alice@example.net");
    dict["Bob"]     = new Uri("mailto:bob@example.net");
    dict["Charlie"] = new Uri("mailto:charlie@example.net");

    foreach (KeyValuePair<string, Uri> pair in dict) {
      Console.WriteLine("{0,-10} => {1}", pair.Key, pair.Value);
    }

    Console.WriteLine();

    // 各キーに該当する要素の値を上書き
    dict["ALICE"]   = new Uri("http://example.net/~alice/");
    dict["ChArLiE"] = new Uri("mailto:charlie2@mail.example.com");

    foreach (KeyValuePair<string, Uri> pair in dict) {
      Console.WriteLine("{0,-10} => {1}", pair.Key, pair.Value);
    }
  }
}
実行結果
Alice      => mailto:alice@example.net
Bob        => mailto:bob@example.net
Charlie    => mailto:charlie@example.net

Alice      => http://example.net/~alice/
Bob        => mailto:bob@example.net
Charlie    => mailto:charlie2@mail.example.com