うにてぃブログ

UnityやUnreal Engineの記事を書いていきます

【C#】==演算子 と Equals について読み解く

変数を比較する際に ==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

ここでは型が違うが、暗黙的に上位の数値により shortint に変換されるため正しく処理される

上位の数値に関してはこちらをご確認ください

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 で比較している