IEquatable<T>インターフェースで実装されるEqualsメソッドは、==, !=(<>) といった等価・不等価演算子の役割と似たものですが、両者は全く独立したものであり、このインターフェイスを実装したからといって等価演算子を用いた比較が出来るようになるわけではありません。 等価演算子を用いて比較するには、別途演算子をオーバーロードしなければなりません。 また逆に、等価演算子・不等価演算子をオーバーロードしたからといってList.Containsメソッドで要素の有無をチェックしたり、Dictionaryのキーとして使えるようになるというわけでもありません。 List.Containsメソッドで要素の有無を調べられるようにするには少なくともEqualsメソッド、Dictionaryのキーとして使用するにはEqualsメソッドとGetHashCodeメソッドの両方が適切に実装されている必要があります。

しかし、独自のクラスを構築する際、IEquatable<T>等による比較と等価演算子による比較の両方が出来るようにしたい場合も当然出てきます。

以下の例では、IEquatable<T>の実装と等価・不等価演算子のオーバーロードを行った構造体を作成しています。 この構造体では、等価演算子で等価性の定義と比較処理を実装し、IEquatable<T>.Equalsメソッドではオーバーロードした等価演算子を使って結果を返すようにしています。

using System;
using System.Collections.Generic;

struct Point : IEquatable<Point> {
  int x, y;

  public Point(int x, int y)
  {
    this.x = x;
    this.y = y;
  }

  // IEquatable<Point>.Equalsの実装
  public bool Equals(Point other)
  {
    // 実際の比較はオーバーロードした等価演算子で行う
    return this == other;
  }

  // Object.Equalsをオーバーライド
  public override bool Equals(object obj)
  {
    if (obj is Point)
      // 型がPointならオーバーロードした等価演算子を使って比較
      return this == (Point)obj;
    else
      // 異なる型の場合はfalse
      return false;
  }

  // 等価演算子 == のオーバーロード
  public static bool operator == (Point l, Point r)
  {
    // フィールドxとyが等しければ二つのPointは等しいものとする
    return l.x == r.x && l.y == r.y;
  }

  // 不等価演算子 == のオーバーロード
  public static bool operator != (Point l, Point r)
  {
    // 等価演算子 == の結果を否定した結果を返す
    return !(l == r);
    // 当然、次のようにしても可
    //return l.x != r.x || l.y != r.y;
  }

  public override int GetHashCode()
  {
    return x.GetHashCode() ^ y.GetHashCode();
  }

  public override string ToString()
  {
    return string.Format("({0}, {1})", x, y);
  }
}

class Sample {
  static void Main()
  {
    List<Point> list = new List<Point>(new Point[] {
      new Point(1, 0),
      new Point(0, 1),
      new Point(0, 0),
    });

    Point zero = new Point(0, 0);

    // オーバーロードした等価・不等価演算子のテスト
    Console.WriteLine("{0} {2} {1}", zero, list[0], zero == list[0] ? "==" : "!=");
    Console.WriteLine("{0} {2} {1}", zero, list[1], zero == list[1] ? "==" : "!=");
    Console.WriteLine("{0} {2} {1}", zero, list[2], zero != list[2] ? "!=" : "==");
    Console.WriteLine();

    // IEquatable<Point>.EqualsとオーバーライドしたObject.Equalsのテスト
    Console.WriteLine("{0} {2} {1}", zero, list[0], zero.Equals(list[0]) ? "Equals" : "Not Equals");
    Console.WriteLine("{0} {2} {1}", zero, list[2], zero.Equals(list[2]) ? "Equals" : "Not Equals");
    Console.WriteLine();
    Console.WriteLine("{0} {2} {1}", zero, 0, zero.Equals(0) ? "Equals" : "Not Equals");
    Console.WriteLine("{0} {2} {1}", zero, "null", zero.Equals(null) ? "Equals" : "Not Equals");
    Console.WriteLine();

    Console.WriteLine("Contains {0} = {1}", zero, list.Contains(zero));
    Console.WriteLine("Contains {0} = {1}", new Point(1, 1), list.Contains(new Point(1, 1)));
  }
}
実行結果
(0, 0) != (1, 0)
(0, 0) != (0, 1)
(0, 0) == (0, 0)

(0, 0) Not Equals (1, 0)
(0, 0) Equals (0, 0)

(0, 0) Not Equals 0
(0, 0) Not Equals null

Contains (0, 0) = True
Contains (1, 1) = False

なお、この例では構造体での等価演算子のオーバーロードを行いましたが、参照型でのオーバーロードを行う場合は x == y が参照の等価性(つまり同一のインスタンス)を表すのか、インスタンスの持つ値の等価性を表すのか曖昧になる場合がある点に注意が必要です。

もう一つ別の例を挙げます。 次の例において、Point構造体では等価・不等価演算子のオーバーロードのみを行い、キーとしての比較で必要となる処理はEqualityComparer<T>クラスを継承した別のクラスPointComparerで実装しています。

using System;
using System.Collections.Generic;

struct Point {
  int x, y;

  public Point(int x, int y)
  {
    this.x = x;
    this.y = y;
  }

  // 等価演算子 == のオーバーロード
  public static bool operator == (Point l, Point r)
  {
    // フィールドxとyが等しければ二つのPointは等しいものとする
    return l.x == r.x && l.y == r.y;
  }

  // 不等価演算子 == のオーバーロード
  public static bool operator != (Point l, Point r)
  {
    // 等価演算子 == の結果を否定した結果を返す
    return !(l == r);
  }

  public override int GetHashCode()
  {
    return x.GetHashCode() ^ y.GetHashCode();
  }

  public override string ToString()
  {
    return string.Format("({0}, {1})", x, y);
  }
}

// Point構造体のためのEqualityComparer
class PointComparer : EqualityComparer<Point> {
  public override bool Equals(Point a, Point b)
  {
    // オーバーロードした等価演算子を使って比較
    return a == b;
  }

  public override int GetHashCode(Point obj)
  {
    return obj.GetHashCode();
  }
}

class Sample {
  static void Main()
  {
    // ある座標とその座標の名前のディクショナリ
    Dictionary<Point, string> dict = new Dictionary<Point, string>(new PointComparer());

    Point o = new Point(0, 0);
    Point a = new Point(1, 0);
    Point b = new Point(0, 1);
    Point c = new Point(1, 1);

    dict[o] = "O";
    dict[a] = "A";
    dict[b] = "B";

    Console.WriteLine("ContainsKey {0} = {1}", b, dict.ContainsKey(b));
    Console.WriteLine("ContainsKey {0} = {1}", c, dict.ContainsKey(c));

    foreach (KeyValuePair<Point, string> pair in dict) {
      Console.WriteLine("{0} => {1}", pair.Key, pair.Value);
    }
  }
}
実行結果
ContainsKey (0, 1) = True
ContainsKey (1, 1) = False
(0, 0) => O
(1, 0) => A
(0, 1) => B