変数を比較する際に ==
や Equals
を利用しますが、正しく内部処理を理解してなかったので各型による挙動を調査しました
int の比較
int intValue1 = 10; int intValue2 = 10; Console.WriteLine($"{intValue1 == intValue2}"); // true Console.WriteLine($"{intValue1.Equals(intValue2)}"); // true Console.WriteLine($"{(object)intValue1 == (object)intValue2}"); // false Console.WriteLine($"{intValue1.Equals((object)intValue2)}"); // true
intValue1 == intValue2
内部処理を見たかったが、見つからなかったため IL から処理を確認する
bool = int == int
を IL に変換すると以下になる
ldloc.0 // Load local variable 0 onto stack ldloc.1 // Load local variable 1 onto stack ceq // Push 1 (of type int32) if value1 equals value2, else push 0 stloc.2 // Pop a value from stack into local variable 2
ローカル変数0(int) と ローカル変数1(int) の値をスタックに入れてその比較した値を ローカル変数2(bool) に入れる処理になる
intValue1.Equals(intValue2)
こちらの処理は .Net のコードを発見できた
内部で ==
による比較を行っていた
public bool Equals(Int32 obj) { return m_value == obj; }
(object)intValue1 == (object)intValue2}
object
にキャストして比較を行った場合、値型なため boxing が発生してアドレスによる比較に変わる
これにより値の比較では無くなったため、false
になった
ldloc.0 box [System.Private.CoreLib]System.Int32 ldloc.1 box [System.Private.CoreLib]System.Int32 ceq stloc.2
intValue1.Equals((object)intValue2)
Equals の引数が object になった場合以下の処理が呼ばれる
内部で int 型だった場合は ==
による比較が行われる
public override bool Equals(Object obj) { if (!(obj is Int32)) { return false; } return m_value == ((Int32)obj).m_value; }
int と short の比較
int intValue = 10; short shortValue = 10; Console.WriteLine($"{intValue == shortValue}"); // true Console.WriteLine($"{intValue.Equals(shortValue)}"); // true
intValue == shortValue
ここでは型が違うが、暗黙的に上位の数値により short
が int
に変換されるため正しく処理される
上位の数値に関してはこちらをご確認ください
intValue.Equals(shortValue)
こちらでも同様に、数値の上位変換が行われるため正しく処理される
int と long の比較
int intValue = 10; long longValue = 10; Console.WriteLine($"{intValue == longValue}"); // true Console.WriteLine($"{intValue.Equals(longValue)}"); // false
intValue == longValue
int と short のときと同じように数値の上位変換が行われるため、正しく処理される
intValue.Equals(longValue)
先程記述したように int の Equals は以下の処理が行われている、引数が下位の型であれば上位変換されるが上位の型の場合変換が行われないそのため、false
が返ってくる
public override bool Equals(Object obj) { if (!(obj is Int32)) { return false; } return m_value == ((Int32)obj).m_value; }
下位変換を行おうとすると以下のエラーが出る
Cannot implicitly convert type 'long' to 'int'. An explicit conversion exists (are you missing a cast?)
char の比較
char charValue1 = 'a'; char charValue2 = 'a'; Console.WriteLine($"{charValue1 == charValue2}"); // true Console.WriteLine($"{charValue1.Equals(charValue2)}"); // true Console.WriteLine($"{(object)charValue1 == (object)charValue2}"); // false
int と変わらないため特に説明は不要
struct の比較
Struct struct1 = new Struct(){Value = 10}; Struct struct2 = new Struct(){Value = 10}; //Console.WriteLine($"{struct1 == struct2}"); // Operator '==' cannot be applied to operands Console.WriteLine($"{struct1.Equals(struct2)}"); // true public struct Struct { public int Value; }
struct はデフォルトで == は実装されていないため、利用する場合オーバーロードする必要がある
struct1.Equals(struct2)
リフレクションを利用して、全フィールドを取得し等価の確認を行っている
https://referencesource.microsoft.com/#mscorlib/system/valuetype.cs,d8b9b308e644b983
class の比較
Class class1 = new Class(){Value = 10}; Class class2 = new Class(){Value = 10}; Console.WriteLine($"{class1 == class2}"); // false Console.WriteLine($"{class1.Equals(class2)}"); // false class2 = class1; Console.WriteLine($"{class1 == class2}"); // true Console.WriteLine($"{class1.Equals(class2)}"); // true public class Class { public int Value; }
class1 == class2
こちらも値型と同様処理が見つからなかったため、IL から確認する
var bool = class1 == class2;
上記を変換すると 値型と同様の IL が生成される
しかし、値型と違うのはスタックの入れられる値がアドレスという点
そのため、インスタンスが異なるためアドレスも異なり false
になる
ldloc.0 // Load local variable 0 onto stack ldloc.1 // Load local variable 1 onto stack ceq // Push 1 (of type int32) if value1 equals value2, else push 0 stloc.2 // Pop a value from stack into local variable 2
class1.Equals(class2)
.Net 内で処理が見つかりましたが、先の初期がここに記述されておらず
調べたところ ReferenceEquals が呼ばれていそうでした
string の比較
string stringValue1 = "a"; string stringValue2 = "a"; Console.WriteLine($"{stringValue1 == stringValue2}"); // true Console.WriteLine($"{stringValue1.Equals(stringValue2)}"); // true Console.WriteLine($"{stringValue1 == (object)stringValue2}"); // true Console.WriteLine($"{Object.ReferenceEquals(stringValue1, stringValue2)"); // true ;
stringValue1 == stringValue2
オーバーロードされており、Equals と同じ挙動をする
public static bool operator == (String a, String b) { return String.Equals(a, b); }
stringValue1.Equals(stringValue2)
Override されておりアドレスではなく文字の一致を確認している
https://referencesource.microsoft.com/mscorlib/R/372d790ddae4cbb4.html
stringValue1 == (object)stringValue2
オブジェクトとの比較の際は、型を確認し参照の一致を確認した後文字の一致を確認している
https://referencesource.microsoft.com/mscorlib/R/62d7ef71d323486a.html
Object.ReferenceEquals(stringValue1, stringValue2)
string も参照型なので比較した場合は false
になるはずですが true
になっている
これは同じ文字列だった場合にメモリ節約のために同じアドレスになっているようでした
Constant strings within the same assembly are always interned by the runtime. This means they are stored in the same location in memory. Therefore, the two strings have reference equality although no assignment takes place.
そのため、StringBuilder で文字列を作成した場合文字自体は同じですが、参照はことなるようになります
string stringValue1 = "a"; string stringValue3 = new StringBuilder().Append("a").ToString(); Console.WriteLine($"{stringValue1 == stringValue3}"); // true Console.WriteLine($"{stringValue1 == (object)stringValue3}"); // false
Array ・ List の比較
// Array の比較 int[] intArray1 = new int[]{1}; int[] intArray2 = new int[]{1}; Console.WriteLine($"{intArray1 == intArray2}"); // false Console.WriteLine($"{intArray1.Equals(intArray2)}"); // false Console.WriteLine($"{intArray1.SequenceEqual(intArray2)}"); // true // List の比較 List<int> intList1 = new List<int>(){1}; List<int> intList2 = new List<int>(){1}; Console.WriteLine($"{intList1 == intList2}"); // false Console.WriteLine($"{intList1.Equals(intList2)}"); // false Console.WriteLine($"{intList1.SequenceEqual(intList2)}"); // true
Array, List 共に参照型のため、 ==
と Equals
は従来通りアドレスによる参照が行われている
そのため、中の値の一致を確認するには SequenceEqual
を利用する必要がある
SequenceEqual の内部処理はこちらで要素一つ一つを Equals
で比較している