Help us understand the problem. What is going on with this article?

[.NET] コードを見直したくなる「参照型」等価判定の思わぬ落とし穴(一般編) ※最初のクイズがわかる方は読む必要がありません

More than 1 year has passed since last update.

最初にクイズです。
True か False か、すべて自信をもって答えられる方はこの記事を読む必要がありません。

Console.WriteLine(Object.ReferenceEquals("s", "s"));
Console.WriteLine(new StringBuilder("sb") == new StringBuilder("sb"));
Console.WriteLine(new StringBuilder("sb").Equals(new StringBuilder("sb")));
Console.WriteLine(new StringBuilder("sb").Equals((object)new StringBuilder("sb")));
Console.WriteLine(Object.Equals(new StringBuilder("sb"), new StringBuilder("sb")));

答えは最後にあります。


参照型の等価比較には、大きく分けて参照の比較と値の比較があります。

Object.ReferenceEquals メソッドや VB.NET の Is 演算子を使うと参照の比較になります。
等価演算子 '=='、Equals メソッドは Object 型の既定実装は参照の比較ですが、等価演算子はオーバーロード、Equals メソッドはオーバーライド/オーバーロード(もあることに注意)という方法で型独自の振る舞いを実装することができますので、注意が必要です。
Microsoft Docs(「参照型のガイドライン」)では、型がイミュータブルな(変更できない)値を表現する場合、Equals メソッドのオーバーライドを検討することが推奨されていますが、ほとんどの場合において等価演算子 '==' はオーバーロードすべきでない、とされています。

ここでは String クラスと StringBuilder クラスを例に、等価判定の動作を検証してみます。

※以下、Assert.IsTrue なら () 内が 真、Assert.IsFalse なら () 内が 偽です。
※★印の箇所は注意が必要です。

String 型 の例

検証に使用する変数

string s1 = new string(new char[] {'s'});
string s2 = new string(new char[] {'s'});
object s1AsObj = s1;
object s2AsObj = s2;

【等価演算子】

String 型同士の場合、値比較となります。

等価演算子がオーバーロードされています。

Assert.IsTrue(s1 == s2);

★どちらかが Object 型に収まっている場合、参照比較となります。

等価演算子は Object 型のものが使用されます。

Assert.IsFalse(s1AsObj == s2AsObj);
Assert.IsFalse(s1 == s2AsObj);
Assert.IsFalse(s2AsObj == s1);

【Equals メソッド】

String 型同士の場合、値比較となります。

String 型引数のオーバーロードが使用されます。

Assert.IsTrue(s1.Equals(s2));

どちらかが Object 型に収まっている場合も値比較となります。

Object.Equals(object) のオーバーライドが使用されます。

Assert.IsTrue(s1.Equals(s2AsObj));

// Object 型のメソッド呼び出しでもオーバーライドされた Equals メソッドが使用されます。
Assert.IsTrue(s2AsObj.Equals(s1));

【Object.Equals 静的メソッド】

値比較となります。

中からオーバーライドされた Equals(object) メソッドが呼ばれます。

Assert.IsTrue(Object.Equals(s1, s2));
Assert.IsTrue(Object.Equals(s1AsObj, s2AsObj));

/* 《参考》Object.Equals の実装
public static bool Equals(object objA, object objB)
{
   return ((objA == objB) || (((objA != null) && (objB != null)) && objA.Equals(objB)));
}
*/

【Object.ReferenceEquals 静的メソッド】

同じインスタンスなら一致します。

Assert.IsTrue(Object.ReferenceEquals(s1, s1));
Assert.IsTrue(Object.ReferenceEquals(s1, s1AsObj));

インスタンス異なれば、値が同じでも一致しません。

Assert.IsFalse(Object.ReferenceEquals(s1, s2));
Assert.IsFalse(Object.ReferenceEquals(s1AsObj, s2AsObj));

★文字列のリテラルは同一値に同じ参照が使用されます。

「インターンプール」というテーブルに保持されます。

Assert.IsTrue(Object.ReferenceEquals("s", "s"));
string s1FromLiteral = "s";
Assert.IsTrue(Object.ReferenceEquals("s", s1FromLiteral));

// リテラルから生成されていない値とは一致しません。
Assert.IsFalse(Object.ReferenceEquals("s", s1));

※「インターンプール」については特殊編であらためて解説しています。

StringBuilder 型の例

検証に使用する変数

var sb1 = new StringBuilder("sb");
var sb2 = new StringBuilder("sb");
var sb1Assigned = sb1;
object sb1AsObj = sb1;
object sb2AsObj = sb2;
string sb1AsStr = sb1.ToString();

【等価演算子】

Object 型の等価演算子で参照比較となります。

Assert.IsFalse(sb1AsObj == sb2AsObj);

★StringBuilder 型同士でも参照比較となります。

ガイドラインに沿って等価演算子がオーバーロードされていません。
下の Equals メソッドとの違いに注意が必要です。

Assert.IsFalse(sb1 == sb2);

/* 「演算子 '==' を 'string' と 'System.Text.StringBuilder' 型のオペランドに適用することはできません。」
* コンパイルエラーになるため、ToString() の漏れなどミスに気づきます。
Assert.IsFalse(sb1AsStr == sb1);
*/

【Equals メソッド】

★StringBuilder 型同士の場合、値比較となります。

StringBuilder 型引数のオーバーロードが使用されます。
Equals(object) メソッドが呼ばれているわけではないことに注意しましょう。

Assert.IsTrue(sb1.Equals(sb2));

★どちらかが Object 型の場合、参照比較となります。

Object.Equals(object) メソッドはオーバーライドされていません。
上のオーバーロードとの違いに注意が必要です。
まさに落とし穴的ですが、StringBuilder の表す値はミュータブル(変更可能)ですので、オーバーロード定義の是非はともかく、Equals(object) メソッドをオーバーライドしないこと自体はガイドラインに沿っています。

Assert.IsFalse(sb1.Equals(sb2AsObj));
Assert.IsFalse(sb2AsObj.Equals(sb1));

★ToString() の付け忘れに注意しましょう。

型が違ってもコンパイルが通ってしまい、常に false を返すことになります。

Assert.IsFalse(sb1AsStr.Equals(sb1));

// 文字列同士なら一致します。
Assert.IsTrue(sb1AsStr.Equals(sb1.ToString()));

【Object.Equals 静的メソッド】

★参照比較となります。

中からオーバーライドされていない Equals(object) メソッドが呼ばれます。
上の Equals(StringBuilder) オーバーロードとの違いに注意が必要です。

Assert.IsFalse(Object.Equals(sb1, sb2));

// 代入された同一インスタンスとは一致します。
Assert.IsTrue(Object.Equals(sb1, sb1Assigned));

【Object.ReferenceEquals 静的メソッド】

いずれも参照比較です。

Assert.IsTrue(Object.ReferenceEquals(sb1, sb1));
Assert.IsFalse(Object.ReferenceEquals(sb1, sb2));
Assert.IsTrue(Object.ReferenceEquals(sb1, sb1Assigned));

クイズの答え

True
False
True
False
False


弊社サイトにて2013年に公開したものを若干整理して転載しています。


[.NET] 値型 等価判定の思わぬ落とし穴(一般編)
[.NET] 値型 等価判定の思わぬ落とし穴(特殊編)
[.NET] 参照型 等価判定の思わぬ落とし穴(一般編)
[.NET] 参照型 等価判定の思わぬ落とし穴(特殊編)

CodeOne
【品質と生産性にこだわるシステム開発】 .NET(C#/VB.NET)専門・リモート開発歴10年。即日・1時間から頼める常駐しないエンジニア。確かな技術で開発チームを手堅くサポートします。
https://codeone.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした