【C#】個人的備忘録:Equalsとリストの罠


ワタアメです。

今回は、最近プログラミングでまんまとハマってしまった事柄を備忘録としてここに置いておきます。

インターフェース「IEquatable<T>」を実装した場合

前提条件

ここでは、以下のようなクラスがあるとして話を進めます。

//	Equatable<T>のEquals(T)に内容の等価判定を記述したクラス
public class EqualsTClassA : IEquatable<EqualsTClassA>
{
	private int a, b;

	public EqualsTClassA(int a, int b)
	{
		this.a = a;
		this.b = b;
	}

	public bool Equals(EqualsTClassA? other)
	{
		return other != null && a == other.a && b == other.b;
	}

	public override bool Equals(object? obj)
	{
		return base.Equals(obj);
	}

	public override int GetHashCode()
	{
		return base.GetHashCode();
	}
}

//	objectのEquals(object)に内容の等価判定を記述したクラス
public class EqualsTClassB : IEquatable<EqualsTClassB>
{
	private int a, b;

	public EqualsTClassB(int a, int b)
	{
		this.a = a;
		this.b = b;
	}

	public bool Equals(EqualsTClassB? other)
	{
		return base.Equals(other);
	}

	public override bool Equals(object? obj)
	{
		EqualsTClassB? instance = obj as EqualsTClassB;
		return instance != null && a == instance.a && b == instance.b;
	}

	public override int GetHashCode()
	{
		return base.GetHashCode();
	}
}

//	objectのGetHashCodeが常に0を返すようにしたクラス
public class EqualsTClassC : IEquatable<EqualsTClassC>
{
	private int a, b;

	public EqualsTClassC(int a, int b)
	{
		this.a = a;
		this.b = b;
	}

	public bool Equals(EqualsTClassC? other)
	{
		return base.Equals(other);
	}

	public override bool Equals(object? obj)
	{
		return base.Equals(obj);
	}

	public override int GetHashCode()
	{
		return 0;
	}
}

実行とその結果

ここでは、以下のコードを実行します。上の各クラスについて、

  1. 新しくインスタンスを生成して

  2. 生成したインスタンスがリストの何番目にあるかを前後から探し

  3. 生成したインスタンスが既にリストにある場合はその旨を表示して

  4. 生成したインスタンスをリストに追加する

という処理を行っています。

static void Main()
{
	Console.WriteLine("EqualsTClassA: ");
	List<EqualsTClassA> etcAList = new();
	for (int i = 0; i < 10; i++)
	{
		EqualsTClassA etcA = new(20, 30);
		Console.WriteLine($"\t[{i + 1} / 10] | IndexOf: {etcAList.IndexOf(etcA)}, LastIndexOf: {etcAList.LastIndexOf(etcA)}");
		if (etcAList.Contains(etcA))
		{
			Console.WriteLine("\t\tこのインスタンスはリストに含まれている");
		}
		etcAList.Add(etcA);
	}
	Console.WriteLine();

	Console.WriteLine("EqualsTClassB: ");
	List<EqualsTClassB> etcBList = new();
	for (int i = 0; i < 10; i++)
	{
		EqualsTClassB etcB = new(20, 30);
		Console.WriteLine($"\t[{i + 1} / 10] | IndexOf: {etcBList.IndexOf(etcB)}, LastIndexOf: {etcBList.LastIndexOf(etcB)}");
		if (etcBList.Contains(etcB))
		{
			Console.WriteLine("\t\tこのインスタンスはリストに含まれている");
		}
		etcBList.Add(etcB);
	}
	Console.WriteLine();

	Console.WriteLine("EqualsTClassC: ");
	List<EqualsTClassC> etcCList = new();
	for (int i = 0; i < 10; i++)
	{
		EqualsTClassC etcC = new(20, 30);
		Console.WriteLine($"\t[{i + 1} / 10] | IndexOf: {etcCList.IndexOf(etcC)}, LastIndexOf: {etcCList.LastIndexOf(etcC)}");
		if (etcCList.Contains(etcC))
		{
			Console.WriteLine("\t\tこのインスタンスはリストに含まれている");
		}
		etcCList.Add(etcC);
	}
}

そして以下が実行結果です。
どうやら、IEquatable<T>を実装したクラスのリストでは、IEqutable<T>によって実装を強制されるEquals(T)での比較結果をもとに等価判定をしているようです。

EqualsTClassA:
	[1 / 10] | IndexOf: -1, LastIndexOf: -1
	[2 / 10] | IndexOf: 0, LastIndexOf: 0
		このインスタンスはリストに含まれている
	[3 / 10] | IndexOf: 0, LastIndexOf: 1
		このインスタンスはリストに含まれている
	[4 / 10] | IndexOf: 0, LastIndexOf: 2
		このインスタンスはリストに含まれている
	[5 / 10] | IndexOf: 0, LastIndexOf: 3
		このインスタンスはリストに含まれている
	[6 / 10] | IndexOf: 0, LastIndexOf: 4
		このインスタンスはリストに含まれている
	[7 / 10] | IndexOf: 0, LastIndexOf: 5
		このインスタンスはリストに含まれている
	[8 / 10] | IndexOf: 0, LastIndexOf: 6
		このインスタンスはリストに含まれている
	[9 / 10] | IndexOf: 0, LastIndexOf: 7
		このインスタンスはリストに含まれている
	[10 / 10] | IndexOf: 0, LastIndexOf: 8
		このインスタンスはリストに含まれている

EqualsTClassB:
	[1 / 10] | IndexOf: -1, LastIndexOf: -1
	[2 / 10] | IndexOf: -1, LastIndexOf: -1
	[3 / 10] | IndexOf: -1, LastIndexOf: -1
	[4 / 10] | IndexOf: -1, LastIndexOf: -1
	[5 / 10] | IndexOf: -1, LastIndexOf: -1
	[6 / 10] | IndexOf: -1, LastIndexOf: -1
	[7 / 10] | IndexOf: -1, LastIndexOf: -1
	[8 / 10] | IndexOf: -1, LastIndexOf: -1
	[9 / 10] | IndexOf: -1, LastIndexOf: -1
	[10 / 10] | IndexOf: -1, LastIndexOf: -1

EqualsTClassC:
	[1 / 10] | IndexOf: -1, LastIndexOf: -1
	[2 / 10] | IndexOf: -1, LastIndexOf: -1
	[3 / 10] | IndexOf: -1, LastIndexOf: -1
	[4 / 10] | IndexOf: -1, LastIndexOf: -1
	[5 / 10] | IndexOf: -1, LastIndexOf: -1
	[6 / 10] | IndexOf: -1, LastIndexOf: -1
	[7 / 10] | IndexOf: -1, LastIndexOf: -1
	[8 / 10] | IndexOf: -1, LastIndexOf: -1
	[9 / 10] | IndexOf: -1, LastIndexOf: -1
	[10 / 10] | IndexOf: -1, LastIndexOf: -1

IEquatable<T>を実装しない場合

前提条件

ここでは、以下のようなクラスがあるとして話を進めます。

//	objectのEquals(object)に内容の等価判定を記述したクラス
public class EqualsObjClassA
{
	private int a, b;

	public EqualsObjClassA(int a, int b)
	{
		this.a = a;
		this.b = b;
	}

	public override bool Equals(object? obj)
	{
		EqualsObjClassA? instance = obj as EqualsObjClassA;
		return instance != null && a == instance.a && b == instance.b;
	}

	public override int GetHashCode()
	{
		return base.GetHashCode();
	}
}

//	objectのGetHashCodeが常に0を返すようにしたクラス
public class EqualsObjClassB
{
	private int a, b;

	public EqualsObjClassB(int a, int b)
	{
		this.a = a;
		this.b = b;
	}

	public override bool Equals(object? obj)
	{
		return base.Equals(obj);
	}

	public override int GetHashCode()
	{
		return 0;
	}
}

実行とその結果

ここでは、以下のコードを実行します。上の各クラスについてIEquatable<T>実装クラスの時と同じく、

  1. 新しくインスタンスを生成して

  2. 生成したインスタンスがリストの何番目にあるかを前後から探し

  3. 生成したインスタンスが既にリストにある場合はその旨を表示して

  4. 生成したインスタンスをリストに追加する

という処理を行っています。

static void Main()
{
	Console.WriteLine("EqualsObjClassA: ");
	List<EqualsObjClassA> eocAList = new();
	for (int i = 0; i < 10; i++)
	{
		EqualsObjClassA eocA = new(20, 30);
		Console.WriteLine($"\t[{i + 1} / 10] | IndexOf: {eocAList.IndexOf(eocA)}, LastIndexOf: {eocAList.LastIndexOf(eocA)}");
		if (eocAList.Contains(eocA))
		{
			Console.WriteLine("\t\tこのインスタンスはリストに含まれている");
		}
		eocAList.Add(eocA);
	}
	Console.WriteLine();

	Console.WriteLine("EqualsObjClassB: ");
	List<EqualsObjClassB> eocBList = new();
	for (int i = 0; i < 10; i++)
	{
		EqualsObjClassB eocB = new(20, 30);
		Console.WriteLine($"\t[{i + 1} / 10] | IndexOf: {eocBList.IndexOf(eocB)}, LastIndexOf: {eocBList.LastIndexOf(eocB)}");
		if (eocBList.Contains(eocB))
		{
			Console.WriteLine("\t\tこのインスタンスはリストに含まれている");
		}
		eocBList.Add(eocB);
	}
}

そして以下が実行結果です。
どうやら、objectのEquals(object)をオーバーライドしただけのクラスのリストでは、Equals(object)での比較結果をもとに等価判定をしているようです。

EqualsObjClassA:
	[1 / 10] | IndexOf: -1, LastIndexOf: -1
	[2 / 10] | IndexOf: 0, LastIndexOf: 0
		このインスタンスはリストに含まれている
	[3 / 10] | IndexOf: 0, LastIndexOf: 1
		このインスタンスはリストに含まれている
	[4 / 10] | IndexOf: 0, LastIndexOf: 2
		このインスタンスはリストに含まれている
	[5 / 10] | IndexOf: 0, LastIndexOf: 3
		このインスタンスはリストに含まれている
 	[6 / 10] | IndexOf: 0, LastIndexOf: 4
		このインスタンスはリストに含まれている
	[7 / 10] | IndexOf: 0, LastIndexOf: 5
		このインスタンスはリストに含まれている
	[8 / 10] | IndexOf: 0, LastIndexOf: 6
		このインスタンスはリストに含まれている
	[9 / 10] | IndexOf: 0, LastIndexOf: 7
		このインスタンスはリストに含まれている
	[10 / 10] | IndexOf: 0, LastIndexOf: 8
		このインスタンスはリストに含まれている

EqualsObjClassB:
	[1 / 10] | IndexOf: -1, LastIndexOf: -1
	[2 / 10] | IndexOf: -1, LastIndexOf: -1
	[3 / 10] | IndexOf: -1, LastIndexOf: -1
	[4 / 10] | IndexOf: -1, LastIndexOf: -1
	[5 / 10] | IndexOf: -1, LastIndexOf: -1
	[6 / 10] | IndexOf: -1, LastIndexOf: -1
	[7 / 10] | IndexOf: -1, LastIndexOf: -1
	[8 / 10] | IndexOf: -1, LastIndexOf: -1
	[9 / 10] | IndexOf: -1, LastIndexOf: -1
	[10 / 10] | IndexOf: -1, LastIndexOf: -1

結論

List<T>におけるContainsやIndexOf等でなされる等価判定の優先度は、

  1. Tがインターフェース「IEquatable<T>」を実装している場合はEquals(T)

  2. Tがインターフェース「IEquatable<T>」を実装していない場合はEquals(object)

となるようです。リスト内でContainsを使用してインスタンスの重複を避けたい場合はEqualsを触らない方が良さげかもしれない。(個人的所感)

この記事が気に入ったらサポートをしてみませんか?