見出し画像

[.NET] コードを見直したくなる「値型」等価判定の思わぬ落とし穴(一般編)

値型の等価判定には気をつけるべき点があります。
以下の例で、Assert.IsTrue なら () 内が 真、Assert.IsFalse なら () 内が 偽です。
★印の箇所は注意が必要です。

ボックス化した値の比較

int i = 1;
object o1 = i;
object o2 = i;

// Object.Equals が int の Equals を呼び出すので値で比較されます。
Assert.IsTrue(o1.Equals(o2));

// o1 != o2 かつ o1 != null なら上の o1.Equals(o2) と同じ結果となります。
Assert.IsTrue(Object.Equals(o1, o2));

ここまではいいでしょう。
次は少し注意が必要です。

// ★ボックス化(値型 → object)すると参照比較になるので一致しません。
Assert.IsFalse(o1 == o2);

// ★引数として渡されるときにボックス化されるので一致しません。
Assert.IsFalse(Object.ReferenceEquals(1, 1));
Assert.IsFalse(Object.ReferenceEquals(i, i));

DataRow の各フィールド値(インデクサ/Itemプロパティ)なども Object 型なので、比較の際は注意しましょう。

異なる型の比較

int i = 1;
long l = 1;

// 比較演算子では、int が long に暗黙的に型変換され、long 同士として比較されます。
Assert.IsTrue(i == l);
Assert.IsTrue(l == i);

// 暗黙の型変換ができるなら Equals で比較できます。
Assert.IsTrue(l.Equals(i));

ここまではいいでしょう。
以降は注意が必要です。

// ★暗黙の型変換ができないと Equals は false を返します。
// コンパイルは通り、例外も発生しないので見逃しがちです。
Assert.IsFalse(i.Equals(l));

// ★異なる型へのボックス化解除は失敗します。
object iBoxed = i;
try
{
   // int 型をボックス化しているので long 型に戻すことはできません。(InvalidCastException が発生)
   bool b = (l == (long)iBoxed);
   Assert.Fail();
}
catch (InvalidCastException) {}

おまけです。

// 実行されるのは Object.Equals です。
// 字面だけ見ると true を期待してしまいそう(?)です。
Assert.IsFalse(Type.Equals(1, 2));

値型を自作する場合の注意点

値型(struct/Structure)の Equals メソッドは、既定ではリフレクションを使用して全フィールド(自動実装プロパティのバッキングフィールドを含む)を定義された型の Equals メソッドで比較します。
値型を自分で作成する場合、Equals メソッドをオーバーライドすることで、この比較処理をカスタマイズすることができます。

等価演算子(==)を使用するためには、「==」「!=」のオーバーロードを定義する必要があります。

値型を定義する場合には、Equals メソッドのオーバーライドと等価演算子のオーバーロードが推奨されています。
《参考》コード分析(FxCop)
CA1815: equals および operator equals を値型でオーバーライドします

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