C# で二つのオブジェクトを比較する

2021/09/02に公開

C# でのオブジェクトの比較方法の種類

二つのオブジェクトが等しいかどうかはどうやって確かめていますか?
最も多いのは次のように == 演算子を使うケースではないかと思います。

string a = "Foo";
string b = "Bar";
Console.WriteLine(a == b);
False

文字列 ab の内容を a == b で比較したところ、異なっているので False が出力されています。
しかし C# でのオブジェクトの比較法はこれだけではありません。大きく分けて次の三つがあります。

  1. 参照による比較
  2. 値による比較
  3. == による比較

この三つを混同しないよう、これから順に説明していこうと思います。
その前に次のクラスと構造体を用意しましょう。

class ExampleClass
{
	public string Text { get; set; }
}

struct ExampleStruct
{
	public string Text { get; set; }
}

ExampleClass は参照型のクラス、ExampleStruct は値型の構造体です。
参照型と値型について詳しくは次の参考資料をご覧ください。

参考資料: 2-3 値型と参照型

大雑把に説明すると、参照型のオブジェクトを保持する変数にはオブジェクトのデータそのものではなく参照(メモリ上のアドレス)が入っていて、価型のオブジェクトを保持する変数にはオブジェクトのデータそのものが入っています。
あまり大きなデータを値型として宣言すると受け渡しに時間がかかるので、int, long, DateTime などのサイズが固定された比較的小さなオブジェクトが値型で実装されます。そして string などのどれだけデータがあるかわからない大きなオブジェクトは参照型で実装した方が良いでしょう。

実際に比較してみる

参照による比較

参照による比較には System.Object.ReferenceEquals() を使います。
これによって実際のメモリ上の位置によってオブジェクトが比較されますので、異なったインスタンスのもの(それぞれ別個に new したもの)を比較すると False と判定されます。

var classA = new ExampleClass();
var classB = new ExampleClass();
var structA = new ExampleStruct();
var structB = new ExampleStruct();

Console.WriteLine(ReferenceEquals(classA, classB));
Console.WriteLine(ReferenceEquals(structA, structB));
False
False

一番わかりやすい比較ですね。
同じ遺伝子を持っていても別々に生まれたなら双子でも別人ということです。

コメントで指摘を受けました。ReferenceEquals() は参照渡しでないので値型はコピーが渡され、同一インスタンスでも必ず False になります。

Console.WriteLine(ReferenceEquals(structA, structA));
False

値による比較

ExampleClass と ExampleStruct で異なる結果となる

値による比較には System.Object.Equals() を使います。

var classA = new ExampleClass();
var classB = new ExampleClass();
var structA = new ExampleStruct();
var structB = new ExampleStruct();

Console.WriteLine(Equals(classA, classB));
Console.WriteLine(Equals(structA, structB));
False
True

classAclassB は別のものと判定されて False を返しましたが、structAstructB は同じものと判定されて True を返しました。
structAstructB は参照は違いますがどちらの Text プロパティも null で初期化されています。
値が同じなので EqualsTrue を返します。

この続きで structA.Text のみを書き換えてみましょう。

structA.Text = "僕は特別";
Console.WriteLine(Equals(structA, structB));
False

比較すると、False を返しました。
プロパティが異なるので値が異なったとみなされたからです。

参照型と値型の違いと結論付けてはならない

さて、classstructEquals が違う結果を出したわけです。
ここから導かれる結論は、「Equals は比較対象が参照型の場合は参照で比較し、値型の場合はプロパティで比較する」となり……ません
大事なことなのでもう一度言います。
Equals は比較対象が参照型の場合は参照で比較し、値型の場合はプロパティで比較するのではありません。
なぜ二度も言ったかというと、そのような説明を読んだことがあるからです。
もう一度……もういいですか。そうですか。
デフォルトで参照型は参照を比較しますが、それはあくまでデフォルトの話なのです。

では値による比較つまり Equals による比較は何をもとにしているかというと、ずばり Equals です。
何を言っているかわかりませんか。そうですか。

二つの Equals

先ほど紹介した Equals は二つの引数を取り、そこで与えられた二つのオブジェクトを比較するものでした。
Equals にはもう一つのオーバーロードがあります。
それは引数を一つだけ取り、自分とほかのオブジェクトを比較するものです。

これを説明するために ExampleClass を書き換えてみましょう。

class ExampleClass
{
	public string Text { get; set; }

	public override bool Equals(object obj)
	{
		var @class = obj as ExampleClass;
		return @class != null &&
			   Text == @class.Text;
	}

	public override int GetHashCode()
	{
		return 1249999374 + EqualityComparer<string>.Default.GetHashCode(Text);
	}
}

Equals()GetHashCode() をオーバーライドしました。

ではこのクラスを使って同じように実験してみます。

var classA = new ExampleClass();
var classB = new ExampleClass();
var structA = new ExampleStruct();
var structB = new ExampleStruct();

Console.WriteLine(Equals(classA, classB));
Console.WriteLine(Equals(structA, structB));
True
True

先ほどとは異なる結果が出ました。
両者とも同じ値として True を返しています。

二つの引数を取る Equals は、そのクラスで実装された一つの引数を取る Equals を内部で呼び出し、その結果をもって比較しているのです。
つまり値による比較は比較されるオブジェクト自身が自分と他者を比較するというのが正解です。

これが何を意味するかと言うと、次のような恐ろしい結論です。

Equals() で比較するときには、そのオブジェクトが比較をどのように実装しているか、つまり何をもって値が等しいと判断しているかを知らなければならないということです。

何が恐ろしいかですって?
決まってるじゃありませんか。
そのオブジェクトの仕様を知らなければ Equals は使えないということです。
参照による比較と最も大きな違いはここです。
参照による比較なら何も読まなくても簡単に結果がわかります。
しかし値による比較はソースかドキュメントを読む、または他のオブジェクトから類推するより他ないのです。
例えばこのように無茶苦茶な仕様で実装してみましょう。

class ExampleClass
{
	public string Text { get; set; }

	public override bool Equals(object obj)
	{
		return true;
	}

	public override int GetHashCode()
	{
		return 1249999374 + EqualityComparer<string>.Default.GetHashCode(Text);
	}
}

struct ExampleStruct
{
	public string Text { get; set; }

	public override bool Equals(object obj)
	{
		return false;
	}

	public override int GetHashCode()
	{
		var hashCode = 1041509726;
		hashCode = hashCode * -1521134295 + base.GetHashCode();
		hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Text);
		return hashCode;
	}
}

これを比較してみます。

Console.WriteLine(Equals(classA, structA));
Console.WriteLine(Equals(structA, classA));
True
False

classA はどんなオブジェクトと比較されても true を返します。
structA はどんなオブジェクトと比較されても false を返します。
その結果、同じものを比較しているはずなのに順番によって結果が異なるという、このような狂った結果が出るようになってしまいました。

値による比較が使えないと結論付けてはならない

このように実装次第でとんでもないことになってしまう値による比較ですが、「怖い怖い。もう参照でしか比較しない!」とは言わないでください。
例えば Dictionary<,> などはキーを参照ではなく値で比較しています。
だから異なるインスタンスでも辞書に入れることができるのです。
値による比較ができなくなればとても不便なことになります。

ですから、値による比較を適切に実装するのは作者の責任です。
ソースやドキュメントを読まなくても使えるよう、ごく自然に「同じ値だね」と言えるような実装をしてください。

== による比較

== は参照の比較でも値の比較でもない!

次のクラスで試してみます。

class ExampleClass
{
	public string Text { get; set; }

	public override bool Equals(object obj)
	{
		var @class = obj as ExampleClass;
		return @class != null &&
			   Text == @class.Text;
	}

	public override int GetHashCode()
	{
		return 1249999374 + EqualityComparer<string>.Default.GetHashCode(Text);
	}
}
var a = new ExampleClass();
var b = new ExampleClass();

Console.WriteLine(ReferenceEquals(a, b));
Console.WriteLine(Equals(a, b));
Console.WriteLine(a == b);
False
True
False

上記のように結果は、参照は異なり、値は等しく、== ではないという結果になりました。
同じことを string で行ってみましょう。

var a = "Foo";
var b = new string(a.ToCharArray());

Console.WriteLine(ReferenceEquals(a, b));
Console.WriteLine(Equals(a, b));
Console.WriteLine(a == b);
False
True
True

おわかりでしょうか。
今度は == になりました。
以上のように、==ReferenceEquals でも Equals でもありません。

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

== 演算子と != 演算子は、オーバーロードされなくてもクラスで使用できます。 ただし、既定の動作として参照の等価性のチェックが実行されます。 クラスで Equals メソッドをオーバーロードする場合は、== 演算子と != 演算子をオーバーロードすることをお勧めしますが、必須ではありません。

とあります。つまり Equals をオーバーライド(オーバーロード? オーバーライドの間違いじゃないのかな)するならば ==!= も併せて<del>オーバーライド</del>オーバーロードする方がいいということですが、同時に「必須ではない」というざっくり加減です。
警告も出ません。

出ませんが、ここはきちんと<del>オーバーライド</del>オーバーロードして値の比較に合わせることにしましょう。

※ 追記:コメントにより指摘をいただきました。ミュータブルな参照型については == 演算子 != 演算子のオーバーロードは避けるべきとのことです。

class ExampleClass
{
	public string Text { get; set; }

	public override bool Equals(object obj)
	{
		var @class = obj as ExampleClass;
		return @class != null &&
			   Text == @class.Text;
	}

	public override int GetHashCode()
	{
		return 1249999374 + EqualityComparer<string>.Default.GetHashCode(Text);
	}

	public static bool operator ==(ExampleClass class1, ExampleClass class2)
	{
		return EqualityComparer<ExampleClass>.Default.Equals(class1, class2);
	}

	public static bool operator !=(ExampleClass class1, ExampleClass class2)
	{
		return !(class1 == class2);
	}
}

その他の比較

以上、三種類の基本的な比較について説明しました。
他にも比較の方法がありますので、少し触れます。

シーケンスの比較

配列を比較してみます。

var a = new[] { 1, 2, 3 };
var b = new[] { 1, 2, 3 };

Console.WriteLine(ReferenceEquals(a, b));
Console.WriteLine(Equals(a, b));
Console.WriteLine(a == b);
False
False
False

はい、全部違います。
しかし要素がすべて同じなら同じものとみなしたいことがあると思います。
そんな時はこうします。

Console.WriteLine(a.SequenceEqual(b));
True

シーケンス(複数の要素の連続)の比較は LINQ の SequenceEqual() を使います。
これは IEnumerable<T> を実装するものなら配列でも List でも Stack でも何でも使えます。
この時、各要素の比較に使われるのは Equals() です。

IComparable<T>

class ExampleClass : IComparable<ExampleClass>
{
	public string Text { get; set; }

	public int CompareTo(ExampleClass other)
	{
		if (other == null) return 1;
		return Text.CompareTo(other.Text);
	}
}

CompareTo() を実装します。
このインターフェースは等しいか等しくないかだけの比較ではありません。
二つのオブジェクトのデフォルトの並び順を指示するものです。

IComparer<T>

IComparable<T> と非常に紛らわしいですが、IComparable<T> がデフォルトの並び順を指示するのに対し、こちらはその時々に応じた並び順を指示します。
例えば数値や文字列はデフォルトの並び順を持っています。
何も指定されなければその順にソートされます。
IComparer<T> は実際にソートする時点でデフォルトでない並び順を指定したいときに使うものです。

Comparison<T>

IComparer<T>Comparison<T> の用途は同じで、並び順のカスタマイズに使います。
しかし前者がインターフェースなのに対して後者はデリゲートです。
インターフェースを実装するにはクラスを作らなければならないのに対してデリゲートはラムダ式で作ることができるので、後者の方が遥かに手軽です。
これらは使い分けをするものではなく、単に前者が古い方法で後者が新しい方法というだけですので、後者が使える時には後者を使いましょう。

執筆日: 2018/03/14

GitHubで編集を提案

Discussion