【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を引数に指定することができます。
で、Containsの判定にちゃんとEqualsメソッドを使ってくれるようになるのですが、かといってバリューオブジェクト的なクラスを定義するたびにIEqualityComparerを実装したクラスを定義するのもバカバカしい。
流石にMSもこれにはすでに気づいていたということで、EqualityComparer<T>.Defaultを使えばうまくいきます、ということです。
var neoCheckContains = hogehogeArray.Contains(hogehogeA, EqualityComparer<HogehogeName>.Default); //True
よかったですね。