◆ object キャストして比較すると ReferenceEquals
◆ 同じ値でも object キャストごとに別参照になる
◆ 文字列型は参照周りが特殊で intern pool でリテラルは同じ参照になってる
  ◆ object.Equals で参照が違っても同じ文字列なら true になるので値比較も特殊 

イコール関係を調べてみました

== 演算子と Equals と ReferenceEquals です
正確には == は比較するインスタンスのメソッドで Equals 2つは object の static メソッドです

ReferenceEquals は名前の通り参照を比較します
Equals の方は値を比較します

struct

まずは 値型です
中身は int を使っています
var sa = 1;
var sb = 1;
var sa2 = sa;

// そのまま比較


Console.Write(sa == sb);
Console.Write(Equals(sa, sb));
Console.Write(ReferenceEquals(sa, sb));
Console.WriteLine("");
// True True False

Console.Write(sa == sa2);
Console.Write(Equals(sa, sa2));
Console.Write(ReferenceEquals(sa, sa2));
Console.WriteLine("");
// True True False

== は値型として比較しています
この場合だと int 型なので int の == メソッドは値を使うようになってるということ

object キャストすると
object o_sa = sa;
object o_sb = sb;
object o_sa2 = sa2;

// ▼ object にして比較

Console.Write(o_sa == o_sb);
Console.Write(Equals(o_sa, o_sb));
Console.Write(ReferenceEquals(o_sa, o_sb));
Console.WriteLine("");
// False True False

Console.Write(o_sa == o_sa2);
Console.Write(Equals(o_sa, o_sa2));
Console.Write(ReferenceEquals(o_sa, o_sa2));
Console.WriteLine("");
// False True False

object の == は ReferenceEquals が使われてるみたい

値が同じでも object にキャストするときに別の参照になるので中の値が一緒でも参照は異なるものになってます


int と object を比較してみます
// ▼ そのままと object を比較

// Console.Write(sa == o_sa);
Console.Write(Equals(sa, o_sa));
Console.Write(ReferenceEquals(sa, o_sa));
Console.WriteLine("");
// Error True False

// Console.Write(o_sa == sa);
Console.Write(Equals(o_sa, sa));
Console.Write(ReferenceEquals(o_sa, sa));
Console.WriteLine("");
// Error True False

== は型が異なるのでエラーです
Equals と ReferenceEquals は型が違っても使えました

参照は 参照同士のときと同じで別物になってるので False です
値比較では object にしていても中の値を見てくれて True になってます


参照は全く同じ値でも別々に参照型にするとイコールにはなりません
参照型にしたあとのものを = で代入したものは参照もイコールです
Console.Write((object)sa == (object)sa);
Console.Write(Equals((object)sa, (object)sa));
Console.Write(ReferenceEquals((object)sa, (object)sa));
Console.WriteLine("");
// False True False

var o2_sa = o_sa;
Console.Write(o_sa == o2_sa);
Console.Write(Equals(o_sa, o2_sa));
Console.Write(ReferenceEquals(o_sa, o2_sa));
Console.WriteLine("");
// True True True

自作 struct

自分で作った struct で == を使うと

Operator '==' cannot be applied to operands of type 'Sample.S' and 'Sample.S'

というエラーになります

Equals と ReferenceEquals は問題なしです
private struct S {}

var sta = new S();
var stb = new S();
var sta2 = sta;

// Console.Write(sta == stb);
Console.Write(Equals(sta, stb));
Console.Write(ReferenceEquals(sta, stb));
Console.WriteLine("");
// Error True False

// Console.Write(sta == sta2);
Console.Write(Equals(sta, sta2));
Console.Write(ReferenceEquals(sta, sta2));
Console.WriteLine("");
// Error True False

また object キャストした場合も ReferenceEquals が使われるので == はエラーになりません
Console.Write((object)sta == (object)stb);
Console.Write(Equals((object)sta, (object)stb));
Console.Write(ReferenceEquals((object)sta, (object)stb));
Console.WriteLine("");
// False True False

== を使うには自分で == メソッドを定義しないといけないようです
struct なので基本は値型で比較すればいいのでこんなの
public static bool operator ==(S s1, S s2)
{
    return s1.Equals(s2);
}
public static bool operator !=(S s1, S s2)
{
    return !s1.Equals(s2);
}

片方だけだとエラーになります
== と != 両方必要です

このときの Equals は ValueType という struct すべてが継承してるクラスのメソッドです
Equals(s1, s2) でもよさそうですけど ネットでよくみる例はこうなってるのでとりあえずこうしました

これだけだと Object.Equals や Obect.GetHashCode をオーバーライドしてませんと警告でますけどエラーではなく動きます
独自の動きにしなくていいならオーバーライドいらなそうだけどなんでだろ


ところで == が定義できるなら === も作れないのかな と思ったのですけど C# で用意されてる演算子の挙動は変えれても 新規に演算子を作るのはできないようです

class

クラスの場合
var ca = new C();
var cb = new C();
var ca2 = ca;

object o_ca = ca;
object o_cb = cb;
object o_ca2 = ca2;
object o2_ca = o_ca;

// ▼ そのまま比較

Console.Write(ca == cb);
Console.Write(Equals(ca, cb));
Console.Write(ReferenceEquals(ca, cb));
Console.WriteLine("");
// False False False

Console.Write(ca == ca2);
Console.Write(Equals(ca, ca2));
Console.Write(ReferenceEquals(ca, ca2));
Console.WriteLine("");
// True True True

// ▼ object にして比較

Console.Write(o_ca == o_cb);
Console.Write(Equals(o_ca, o_cb));
Console.Write(ReferenceEquals(o_ca, o_cb));
Console.WriteLine("");
// False False False

Console.Write(o_ca == o_ca2);
Console.Write(Equals(o_ca, o_ca2));
Console.Write(ReferenceEquals(o_ca, o_ca2));
Console.WriteLine("");
// True True True

// ▼ そのままと object を比較

Console.Write(ca == o_ca);
Console.Write(Equals(ca, o_ca));
Console.Write(ReferenceEquals(ca, o_ca));
Console.WriteLine("");
// True True True

Console.Write(o_ca == ca);
Console.Write(Equals(o_ca, ca));
Console.Write(ReferenceEquals(o_ca, ca));
Console.WriteLine("");
// True True True

クラスは単純です
最初から参照型なので object 型にキャストしても一緒です
実体が一緒(継承関係ある場合も)なら 異なる型(C 型と object 型) の == も使えます

class のときは struct と違って == は自分で作らなくても object 型のメソッドなので参照比較です
Equals の結果は ReferenceEquals と一緒です

String

文字列型のときは特殊です
var str1 = "abc";
var str2 = "abc";

Console.Write(str1 == str2);
Console.Write(Equals(str1, str2));
Console.Write(ReferenceEquals(str1, str2));
Console.WriteLine("");
// True True True

Console.Write((object)str1 == (object)str1);
Console.Write(Equals((object)str1, (object)str1));
Console.Write(ReferenceEquals((object)str1, (object)str1));
Console.WriteLine("");
// True True True

Console.Write((object)str1 == (object)str2);
Console.Write(Equals((object)str1, (object)str2));
Console.Write(ReferenceEquals((object)str1, (object)str2));
Console.WriteLine("");
// True True True

str1 と str2 が同じ参照になっています


ですが char array から作ると
var cstr1 = new String(new[] { 'a', 'b', 'c'});
var cstr2 = new String(new[] { 'a', 'b', 'c'});

Console.Write(cstr1 == cstr2);
Console.Write(Equals(cstr1, cstr2));
Console.Write(ReferenceEquals(cstr1, cstr2));
Console.WriteLine("");
// True True False

Console.Write((object)cstr1 == (object)cstr1);
Console.Write(Equals((object)cstr1, (object)cstr1));
Console.Write(ReferenceEquals((object)cstr1, (object)cstr1));
Console.WriteLine("");
// True True True

Console.Write((object)cstr1 == (object)cstr2);
Console.Write(Equals((object)cstr1, (object)cstr2));
Console.Write(ReferenceEquals((object)cstr1, (object)cstr2));
Console.WriteLine("");
// False True False

文字列が同じだと 値の比較は一緒で参照は異なるということになります
object キャストした場合は == も false になり Equals のみ True になります

ReferenceEquals が False で Equals が True になるのはこれくらい?

他にも
var s1 = "a" + "bc";
var s2 = "a" + "bc";
は 足し算なしリテラルのみと一緒で参照も等しくなりますが
var s0 = "a";
var s1 = s0 + "bc";
var s2 = s0 + "bc";
では 参照は異なるようになります


JavaScript の
"abc" === "abc" // true
new String("abc") === new String("abc") // false
みたいなものかな とも思いましたが 調べてみると違うものでした


CLR に intern pool というのがあって文字列のリテラルは毎回インスタンスを作るのではなくて同じ参照にしてるようです
"str" と "s" + "tr" はコンパイル時に同じものとみなして 同じ実体への参照が代入されています

C# も JavaScript と同じで文字列は immutable です
文字列の 2 文字目を書き換え といったことはできないです
こういうの
var str = "str";
str[1] = 'X';
Replace で置換した場合は 2 文字目が異なる新しい文字列を作っています

文字列は変更されないので同じ文字列のリテラルは実体一つで同じ参照にしてしまえばメモリ節約になるということなんでしょう


ただ 内部の触れられない領域かとおもいきやコードから intern にアクセスしたり作ったりできます
var s1 = "s1";
var s2 = new String(new[] { 's', '2' });
var s3 = "s" + "3";
var s_ = "s";
var s4 = s_ + "4";

Console.WriteLine(String.IsInterned(s1)); // s1
Console.WriteLine(String.IsInterned(s2)); //
Console.WriteLine(String.IsInterned(s3)); // s3
Console.WriteLine(String.IsInterned(s4)); //

IsInterned は interned (リテラルとみなされて pool にあるもの) な文字列か判断できます
interned の場合はその文字列参照
interned でないときは null が返り値になります


intern pool を非 interned な文字列で検索したり intern 化したい場合は Intern メソッドでできます
var s1 = "str";
var s2 = new String(new[] { 's', 't', 'r' });
var s3 = new String(new[] { 's', 't', 'r' });

var si2 = String.Intern(s2);
var si3 = String.Intern(s3);

Console.WriteLine(ReferenceEquals(s2, s3)); // False
Console.WriteLine(ReferenceEquals(si2, si3)); // True

Console.WriteLine(ReferenceEquals(s2, si2)); // False

Console.WriteLine(ReferenceEquals(s2, s1)); // False
Console.WriteLine(ReferenceEquals(si2, s1)); // True

var st = "st";
var si4 = String.Intern(st + "r");
Console.WriteLine(ReferenceEquals(si2, si4)); // True

s2 と s3 は interned でないので参照は異なります

Intern メソッドの返り値は同じ文字列ですが intern pool にある文字列参照です
interned 出ない場合は新しく pool に文字列を作ってその参照が返って来ます

なので s2 と s3 の Intern で参照取得したときに s2 では新しく生成して s3 では intern pool にある s2 と同じ参照を取得してます
(今回は同じ文字列 "str" が s1 のリテラルにすでにあるので s2 で新規作成じゃなくて s1 の参照を取得になってます)

s2 自体が参照化されるわけじゃないので s2 と si2 の参照は異なります

s1 はすでに interned なので s1 と si2 は同じ参照です
si4 でももちろん同じ参照です


intern pool から検索するためにその場で同じ長さの文字列を作ってるので わざわざ intern pool から毎回文字列を取り出して使ってもメモリ節約的には無意味そうですが 「参照が同じ」 ものがほしいというときには使えそうです
そんな場面があるのかわかりませんけど

また intern 化するとアプリケーション終了時まで文字列がメモリ上に永続化されるようです
intern pool の仕組みからすると仕方ないことだと思いますけど そう考えると自分で pool に追加するメリットはほとんどなさそうです

よくわからないとこ

struct のメソッド中で static な Equals と ReferenceEquals をみると Equals は ValueType のものになっていて ReferenceEquals は object のもの
でも 定義をみると Equals は override してるけど static になってない

object.Equals で値比較できてるのに ValueType.Equals なんているの?

あと 上で書いた == を定義するときに Equals をオーバーライドしないといけないというのもなんで?