【C#】LINQのContainsメソッドをIEquatable<T>を実装したバリューオブジェクト的なクラスで利用するときは気を付けたい

何を気を付けるか

バリューオブジェクトを自作する場合、基本的にIEquatable<T>やIComparable<T>などのインターフェースを利用することになるかと思う。で、IEquatable<T>を実装したクラスをLINQのContains()メソッドで利用しようとしてもハマるケースがありますよ、という話。

サンプル

/// <summary>
/// 何かの名前を表すイミュータブルなクラスです。
/// </summary>
public sealed class HogehogeName : IComparable<HogehogeName>, IEquatable<HogehogeName>
{
    /// <summary>
    /// 名前の値を取得します。
    /// </summary>
    public string Value { get; }

    /// <summary>
    /// 既定値かどうかを取得します。
    /// </summary>
    public bool IsDefault => Value == string.Empty;

    /// <summary>
    /// 既定値となるインスタンスを取得します。
    /// </summary>
    public static readonly HogehogeName Default = new HogehogeName();

    /// <summary>
    /// HogehogeNameクラスの既定値として扱われるインスタンスを初期化します。
    /// </summary>
    public HogehogeName()
    {
        Value = string.Empty;
    }

    /// <summary>
    /// HogehogeNameクラスの新しいインスタンスを初期化します。
    /// </summary>
    /// <param name="value">名前</param>
    public HogehogeName(string value)
    {
        if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value));
        Value = value;
    }

    /// <summary>
    /// 現在のインスタンスを同じ型の別のオブジェクトと比較し、
    /// 現在のインスタンスの並べ替え順序での位置が、
    /// 比較対象のオブジェクトと比べて前か、後か、または同じかを示す整数を返します。
    /// </summary>
    /// <param name="other">このインスタンスと比較するオブジェクト</param>
    /// <returns>比較対象の相対順序を示す値</returns>
    public int CompareTo(HogehogeName other) => Value.CompareTo(other?.Value);

    /// <summary>
    /// 現在のオブジェクトが、同じ型の別のオブジェクトと等しいかどうかを示します。
    /// </summary>
    /// <param name="other">このオブジェクトと比較するオブジェクト</param>
    /// <returns>現在のオブジェクトが other パラメーターと等しい場合は true、それ以外の場合は false です。</returns>
    public bool Equals(HogehogeName other) => CompareTo(other) == 0;

    /// <summary>
    /// ハッシュ コードを返します。
    /// </summary>
    /// <returns>現在のオブジェクトのハッシュ コード</returns>
    public override int GetHashCode() => Value.GetHashCode();
}

 こんな感じのバリューオブジェクト的なクラスがあった時に、

var hogehogeA = new HogehogeName("HOGEHOGE-A");

var hogehogeArray = new[]
{
    new HogehogeName("HOGEHOGE-A"),
    new HogehogeName("HOGEHOGE-B"),
    new HogehogeName("HOGEHOGE-C"),
};

var checkIsEquals = hogehogeA.Equals(hogehogeArray[0]); //True
var checkContains = hogehogeArray.Contains(hogehogeA); //False

 こうなるよ。という話。

まあ知ってる人は知ってるんですが、Containsにはオーバーロードメソッドとして対象のオブジェクトとは別にIEqualityComparerを引数に指定することができます。

docs.microsoft.com

で、Containsの判定にちゃんとEqualsメソッドを使ってくれるようになるのですが、かといってバリューオブジェクト的なクラスを定義するたびにIEqualityComparerを実装したクラスを定義するのもバカバカしい。

流石にMSもこれにはすでに気づいていたということで、EqualityComparer<T>.Defaultを使えばうまくいきます、ということです。

var neoCheckContains = hogehogeArray.Contains(hogehogeA, EqualityComparer<HogehogeName>.Default); //True 

 よかったですね。

全然関係ないんですが

はてなブログソースコード埋め込むの、いい方法あったりするんだろうか(まだしらべてない)。