見出し画像

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

一般編に続き特殊編です。

インターンプール

文字列のリテラルは「インターンプール」というテーブルに保持され、同一値に同じ参照が使用されます。
String.Intern メソッドでそれらの参照を取り出すことができます。

▼同一値のリテラルは参照も一致します。

string literal = "CodeOne";

Assert.IsTrue(Object.ReferenceEquals(literal, "CodeOne"));

▼値は同一でも、リテラルと連結された文字列では参照が異なります。

string built = new StringBuilder().Append("Code").Append("One").ToString();

Assert.IsTrue(Object.Equals(literal, built));
Assert.IsFalse(Object.ReferenceEquals(literal, built));

▼インターンプールから取得すると、同一値のリテラルと参照が一致します。

Assert.IsTrue(Object.ReferenceEquals(literal, String.Intern(built)));

※Ngen.exe でアセンブリをコンパイルした場合など、インターンプールに格納されない場合もあります。
《参考》String.Intern メソッド | Microsoft Docs

匿名型(C#)

匿名型では、Equals, GetHashCode メソッドのオーバーライドがコンパイラによって生成されます。
プロパティの数や順序によっても比較結果が異なりますので注意が必要です。

▼インスタンスが同じなら参照比較は一致します。

var staffA = new { Id = 1, Name = "山田" };

Assert.IsTrue(staffA == staffA);
Assert.IsTrue(Object.ReferenceEquals(staffA, staffA));

▼プロパティの数と順序、値が同じでも、インスタンスが異なれば参照比較は一致しません。

var staffA = new { Id = 1, Name = "山田" };
var staffB = new { Id = 1, Name = "山田" };

Assert.IsFalse(staffA == staffB);
Assert.IsFalse(Object.ReferenceEquals(staffA, staffB));

▼プロパティの数と順序、値が同じ場合、Equals の結果は true になります。

Assert.IsTrue(staffA.Equals(staffB));
Assert.IsTrue(staffA.Equals((object)staffB));
Assert.IsTrue(Object.Equals(staffA, staffB));

▼プロパティの順序が異なる場合、Equals の結果は false になります。

var staffA = new { Id = 1, Name = "山田" };
var staffC = new { Name = "山田", Id = 1 };

Assert.IsFalse(staffA.Equals(staffC));

▼プロパティの数が異なる場合、Equals の結果は false になります。

var staffA = new { Id = 1, Name = "山田" };
var staffD = new { Id = 1, Name = "山田", Role = "開発者" };
var staffE = new { Id = 1, Name = "山田", Role = (string)null };

Assert.IsFalse(staffA.Equals(staffD));
Assert.IsFalse(staffA.Equals(staffE));

匿名型(VB.NET)

VB.NET の匿名型では、Key キーワードによって Equals 比較に使用するプロパティを指定できます。
プロパティの数や順序に Key キーワードが絡みますので、C# よりさらに複雑となります。

▼インスタンスが同じなら参照比較は一致します。

Dim staffA = New With { Key .Id = 1, Key .Name = "山田" }

Assert.IsTrue(staffA Is staffA)
Assert.IsTrue(Object.ReferenceEquals(staffA, staffA))

▼プロパティの数と順序、値が同じでも、インスタンスが異なれば参照比較は一致しません。

Dim staffA = New With { Key .Id = 1, Key .Name = "山田" }
Dim staffB = New With { Key .Id = 1, Key .Name = "山田" }

Assert.IsFalse(staffA Is staffB)
Assert.IsFalse(Object.ReferenceEquals(staffA, staffB))

▼プロパティの数と順序、値が同じですべて Key キーワード付きの場合、Equals の結果は True になります。

Assert.IsTrue(staffA.Equals(staffB))
Assert.IsTrue(staffA.Equals(DirectCast(staffB, Object)))
Assert.IsTrue(Object.Equals(staffA, staffB))

▼プロパティの順序が異なる場合、Equals の結果は False になります。

Dim staffA = New With { Key .Id = 1, Key .Name = "山田" }
Dim staffC = New With { Key .Name = "山田", Key .Id = 1 }

Assert.IsFalse(staffA.Equals(staffC))

▼プロパティの数が異なる場合、Equals の結果は False になります。

Dim staffA = New With { Key .Id = 1, Key .Name = "山田" }
Dim staffD = New With { Key .Id = 1, Key .Name = "山田", Key .Role = "開発者" }

Assert.IsFalse(staffA.Equals(staffD))

▼プロパティの数と順序、値が同じでも、一方に Key キーワードが漏れていると Equals の結果は False になります。

Dim staffA = New With { Key .Id = 1, Key .Name = "山田" }
Dim staffE = New With { Key .Id = 1, .Name = "山田" }

Assert.IsFalse(staffA.Equals(staffE))

▼プロパティの数は Key キーワードなしも含めて一致しないと Equals は True を返しません。

Dim staffA = New With { Key .Id = 1, Key .Name = "山田" }
Dim staffF = New With { Key .Id = 1, Key .Name = "山田", .Role = "開発者" }

Assert.IsFalse(staffA.Equals(staffF))

▼Key キーワードの付いていないプロパティは Equals 判定に使用されません。

Dim staffG = New With { Key .Id = 1, Key .Name = "山田", .Role = "開発者" }
Dim staffH = New With { Key .Id = 1, Key .Name = "山田", .Role = "営業担当者" }

Assert.IsTrue(staffG.Equals(staffH))

▼Key キーワード付きのプロパティが1つもない場合、Equals は参照比較となります。

Dim staffI = New With { .Id = 1, .Name = "山田" }
Dim staffJ = New With { .Id = 1, .Name = "山田" }

Assert.IsTrue(staffI.Equals(staffI))
Assert.IsFalse(staffI.Equals(staffJ))

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