One_KWS

==, Equals(), ReferenceEquals() 본문

C#

==, Equals(), ReferenceEquals()

One-Kim 2023. 5. 21. 18:58

Equality

C#에는 두 가지 같음(Equality 또는 Identity)가 존재한다. 

 

Reference Equality

두 개의 객체가 동일한 인스턴스를 참조하는지 여부를 비교한다. 즉, 두 개의 참조 변수가 같은 객체를 가리키는지를 확인한다. 

 

Value Equality

두 개의 객체가 내부적으로 같은 값을 가지는지 여부를 비교한다.

 

==, Equals(), ReferenceEquals()

두 객체의 같음을 확인하기 위해서 아래 세 가지 방법을 사용할 수 있다.

 

== 

== 연산자는 두 개의 객체의 참조를 비교하여 같은지 판단한다. == 연산자를 재정의하지 않은 경우에는 참조가 같은지 판단한다. (Value Type 비교에 사용할 경우 값을 비교한다.) 

ReferenceEquals()

ReferenceEquals() 메서드는 두 개의 객체의 참조가 동일한지 비교한다. 즉, 두 객체가 메모리에서 동일한 인스턴스를 가리키는지를 확인한다.

 

Equals()

Equals() 메서드는 객체의 내부적인 비교를 수행하며 Equals() 메서드를 사용하여 두 개의 객체가 동등한지 비교할 수 있다. 기본적으로 Object 클래스에서 상속된 Equals() 메서드는 참조 동등성 비교를 수행하지만, 많은 클래스에서 이를 재정의할 수 있다. 

 

Value Equality

Struct를 제외한 Value Type의 같음 비교시 ==와 Equals() 모두 값을 비교한다. 따라서 비교하는 두 값이 같다면 == 와 Equals 모두 True를 반환한다. ReferenceEquals()는 참조 비교이므로 False를 반환한다.

using System;

class Test {
    static void Main() {
        int a = 3;
        int b = 1 + 2;
        
        System.Console.WriteLine("ReferenceEquals(a, b): {0}", System.Object.ReferenceEquals(a, b));
        System.Console.WriteLine("a == b: {0}", a == b);
        System.Console.WriteLine("Equals(a, b): {0}", System.Object.Equals(a, b));
        System.Console.WriteLine();
    }
}

 

두 값의 타입이 다를 경우(예를 들어 int와 float) ==와 Equals()의 결과가 다르게 나온다. ==의 경우 3과 3.0이 동일한 값을 가지므로 결과는 True가 된다. 하지만 Equals()의 경우에는 False를 반환한다.

using System;

class Test {
    static void Main() {
        int c = 3;
        float d = 1 + 2;
        
        System.Console.WriteLine("ReferenceEquals(c, d): {0}", System.Object.ReferenceEquals(c, d));
        System.Console.WriteLine("c == d: {0}", c == d);
        System.Console.WriteLine("Equals(c, d): {0}", Equals(c, d));
        System.Console.WriteLine();
    }
}


Equals(a,b)의 구현 코드를 보면 아래와 같다. Equals도 내부적으로는 ==를 사용하고 있으나 object 타입으로 변환하여 비교하기 때문에 결국 참조가 같은지 비교한다. 위의 코드의 경우에는 참조가 다르기 때문에 if문을 지나 return objA.Equals(objB); 코드가 실행될 것이다. 그럼 c.Equals(d)와 같아지게 된다. 

public static bool Equals(object? objA, object? objB)
{
    if (objA == objB)
    {
        return true;
    }
    if (objA == null || objB == null)
    {
        return false;
    }
    return objA.Equals(objB);
}

 

int와 float에 오버라이드되어 있는 Equals() 함수를 보면 아래와 같다. 비교하는 값의 타입이 같은 int 또는 float이 아니라면 false를 반환한다. 따라서 int와 float을 비교할 때 Equals()를 사용하면 값이 같더라도 false를 반환한다.

public override bool Equals([NotNullWhen(true)] object? obj)
{
    if (!(obj is int))
    {
        return false;
    }
    return m_value == ((int)obj).m_value;
}
public override bool Equals([NotNullWhen(true)] object? obj)
{
    if (!(obj is float))
    {
        return false;
    }
    float temp = ((float)obj).m_value;
    if (temp == m_value)
    {
        return true;
    }

    return IsNaN(temp) && IsNaN(m_value);
}

 

이제 Struct의 경우를 보자.

using System;

class Test {
    public struct MyStruct {
        public int num;
        public string str;
    }

    static void Main() {
        var a = new MyStruct() {num = 1, str = "Hi"};
        var b = new MyStruct() {num = 1, str = "Hi"};

        System.Console.WriteLine("ReferenceEquals(a, b): {0}", System.Object.ReferenceEquals(a, b));
        System.Console.WriteLine("a == b: {0}", a == b);
        System.Console.WriteLine("a.Equals(b): {0}", System.Object.Equals(a, b));
        System.Console.WriteLine();
    }
}

 

Struct의 경우 ==를 사용할 경우 아래와 같은 오류가 발생한다. ==를 사용하려면 a.num == b.num 처럼 멤버 변수 값을 비교해야 한다.

두 Struct 비교에 ==를 사용했을 경우 오류가 난다.

 

== 비교를 제외하고 실행시켜 보면 아래와 같은 결과를 얻을 수 있다. ReferenceEquals()의 경우 역시 참조 비교이기 때문에 False를 반환한다. Equals()의 경우에는 해당 Struct의 멤버 변수 값들이 모두 같기 때문에 True를 반환한다.

 

Equals 함수를 오버라이드 하지 않는다면 기본적으로 아래 코드가 실행된다. C#의 Reflection을 이용하여 해당 Struct의 멤버 변수들을 모두 비교하여 같을 경우 True 다른 값이 있으면 False를 반환한다.

public override bool Equals([NotNullWhen(true)] object? obj)
{
    if (null == obj)
    {
        return false;
    }
    Type thisType = this.GetType();
    Type thatType = obj.GetType();

    if (thatType != thisType)
    {
        return false;
    }

    object thisObj = (object)this;
    object? thisResult, thatResult;

    // if there are no GC references in this object we can avoid reflection
    // and do a fast memcmp
    if (CanCompareBits(this))
        return FastEqualsCheck(thisObj, obj);

    FieldInfo[] thisFields = thisType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

    for (int i = 0; i < thisFields.Length; i++)
    {
        thisResult = thisFields[i].GetValue(thisObj);
        thatResult = thisFields[i].GetValue(obj);

        if (thisResult == null)
        {
            if (thatResult != null)
                return false;
        }
        else
        if (!thisResult.Equals(thatResult))
        {
            return false;
        }
    }

    return true;
}

 

Reference Equality

C#에서 string은 참조 타입이지만 결과는 값 타입 비교처럼 동작한다. 아래 코드를 보자.

using System;

class Test {
    static void Main() {
        string a = "hello";
        string b = "hello";

        System.Console.WriteLine("ReferenceEquals(a, b): {0}", System.Object.ReferenceEquals(a, b));
        System.Console.WriteLine("a == b: {0}", a == b);
        System.Console.WriteLine("Equals(a, b): {0}", System.Object.Equals(a, b));
        System.Console.WriteLine();
    }
}

 

==, Equals(), ReferenceEquals() 모두 True를 반환한다. 어떻게 이런 결과가 나오는 걸까 ?

 

C#의 string은 내부적으로 String Pool을 사용한다. String Pool은 문자열 인스턴스를 관리하기 위한 메모리 영역이며 String Pool은 공유되는 문자열을 저장하고, 동일한 문자열 값에 대해 중복된 인스턴스를 생성하지 않고 재사용함으로써 메모리 사용을 최적화한다. 

 

따라서 a와 b string 변수를 따로 선언했어도 참조가 같기 때문에 True를 반환한다. string 내부의 Equals() 함수를 보면 아래와 같다. 가장 위의 if 문에서 ReferenceEquals()를 이용하여 비교를 하고 있고 string 값이 같다면 참조가 같기 때문에 True를 반환한다.

public override bool Equals([NotNullWhen(true)] object? obj)
{
    if (object.ReferenceEquals(this, obj))
        return true;

    if (!(obj is string str))
        return false;

    if (this.Length != str.Length)
        return false;

    return EqualsHelper(this, str);
}

 

 

Class의 경우, ==와 ReferenceEquals()는 참조가 다르면 False 같으면 True를 반환한다. Equals()의 경우에도 오버라이드하지 않을 경우 내부에서는 ==를 사용하고 있기 때문에 같은 값을 반환한다.

 

using System;

class Test {
    public class MyClass {
        public int num;
        public string str;
    }

    static void Main() {
        var a = new MyClass() {num = 1, str = "Hi"};
        var b = new MyClass() {num = 1, str = "Hi"};

        System.Console.WriteLine("ReferenceEquals(a, b): {0}", System.Object.ReferenceEquals(a, b));
        System.Console.WriteLine("a == b: {0}", a == b);
        System.Console.WriteLine("Equals(a, b): {0}", System.Object.Equals(a, b));
        System.Console.WriteLine();
        
        var c = new MyClass() {num = 1, str = "Hi"};
        var d = c;
        
        System.Console.WriteLine("ReferenceEquals(c, d): {0}", System.Object.ReferenceEquals(c, d));
        System.Console.WriteLine("c == d: {0}", c == d);
        System.Console.WriteLine("Equals(c, d): {0}", Equals(c, d));
        System.Console.WriteLine();
    }
}

'C#' 카테고리의 다른 글

박싱(Boxing) & 언박싱(Unboxing)  (0) 2023.05.07
CLR, IL, JIT  (0) 2023.03.29
Value Type, Reference Type  (0) 2023.03.01
Reflection  (0) 2023.02.14