構造体を定義すると Equals が自動的に実装されるが、IEquatable<T> を実装した方がよい

方法: 型の値の等価性を定義する (C# プログラミング ガイド) | Microsoft Docs より:

構造体を定義すると、System.Object.Equals(Object) メソッドの System.ValueType オーバーライドから継承された値の等価性が既定で実装されます。 この実装では、リフレクションを使用して、型のフィールドとプロパティをすべて調べます。 この実装によって正しい結果が生成されますが、その型専用に記述したカスタム実装と比較すると、処理にかなり時間がかかります。

自作の Point 構造体を定義して、配列、HashSet, Dictionary から値を検索できるか確認してみます。値の等価性の確認です。

using System;
using System.Collections.Generic;

class Program {
    struct Point {
        public int X { get; }
        public int Y { get; }

        public Point(int x, int y) {
            X = x;
            Y = y;
        }
    }

    static void Main() {
        void p<T>(T o) => Console.WriteLine(o);

        var a = new Point(1, 2);
        var b = new Point(1, 2);

        // a を格納した配列から b を見つける
        p(Array.IndexOf(new[] { a }, b)); // 0

        // a を格納した HashSet から b を見つける
        var hs = new HashSet<Point>() { a };
        p(hs.Contains(b)); // True

        // a を格納した Dictionary から b を見つける
        var d = new Dictionary<Point, bool>() { [a] = true };
        p(d.ContainsKey(b)); // True
    }
}

実行結果です。

0
True
True

構造体のメンバーごとの比較が行われており、配列、HashSet、Dictionary に格納した場合でも値を見つけることができていることが確認できました。

次に、IEquatable<T> を実装した場合と、速度比較をしてみます。

using System;
using System.Collections.Generic;

class Program {
    struct Point {
        public int X { get; }
        public int Y { get; }

        public Point(int x, int y) {
            X = x;
            Y = y;
        }
    }

    // IEquatable<T> を実装した Point 構造体
    struct PointImplementsIEquatable : IEquatable<PointImplementsIEquatable> {
        public int X { get; }
        public int Y { get; }

        public PointImplementsIEquatable(int x, int y) {
            X = x;
            Y = y;
        }

        public bool Equals(PointImplementsIEquatable other) {
            return X == other.X && Y == other.Y;
        }

        public override bool Equals(object other) {
            if (other is PointImplementsIEquatable)
                return Equals((PointImplementsIEquatable)other);
            return false;
        }

        public override int GetHashCode() {
            return X ^ Y;
        }
    }

    static void Benchmark(int n) {
        var sw = new System.Diagnostics.Stopwatch();
        sw.Start();
        var xs = new[] { new Point(1, 2), };
        for (int i = 0; i < n; i++) {
            Array.IndexOf(xs, xs[0]);
        }
        sw.Stop();
        Console.WriteLine($"Point             = {sw.ElapsedMilliseconds}ms");

        sw.Restart();
        var ys = new[] { new PointImplementsIEquatable(1, 2), };
        for (int i = 0; i < n; i++) {
            Array.IndexOf(ys, ys[0]);
        }
        sw.Stop();
        Console.WriteLine($"Point(IEquatable) = {sw.ElapsedMilliseconds}ms");
    }

    static void Main() {
        Benchmark(10_000_000);
    }
}

実行結果です。

Point             = 895ms
Point(IEquatable) = 281ms

IEquatable<T>を実装したほうが速いようです。Equals() 呼び出し時のボックス化も重いんですかね。 速度のことを考えると、常に IEquatable<T> を実装したほうが良さそうです。

参考