본문 바로가기
C#

[C#] 상등 비교 (IEquatable<T>)

by DANEW 2023. 8. 25.

상등 비교

값 상등 대 참조 상등

- 값 상등(Value Equality)

- 참조 상등(Referential Equality)

 

- 기본적으로

 * 값 형식은 값 상등

 * 참조 형식은 참조 상등

 

표준 상등 프로토콜

- == 연산자와 != 연산자

- 가상 Equals 메서드

- IEquatable<T> 인터페이스

- 교체 가능(pluggable) 프로토콜들

- IStructuralEquatable 인터페이스

 

== 연산자와 != 연산자

- 연산자이기 때문에 그 의미가 정적으로 구현됨

- 비교를 수행할 형식을 컴파일 시점에서 결정

- 동적 다형성(virtual 메서드)이 고려되지 않음

 

가상 object.Equals 메서드

- System.Object에 정의되어 있으므로 모든 형식이 이 메서드를 제공함

- 실행 시점에서, 해당 객체의 실제 형식에 근거해서 결정됨

 * 값 형식: 값 상등

 * 참조 형식: 참조 상등

 * 구조체 형식: 구조체 상등(모든 필드에 대해서 값 상등 비교)

 

정적 object.Equals 메서드

- 2개의 인수를 받는 비교 매서드

- 컴파일 시점에서 미리 알 수 없는 상황에서도 널에 대해 안전한 방식으로 상등 비교 가능

- 제네릭을 이용해서 일반적 형식을 작성할 때 유용

 

정적 object.ReferenceEquals 메서드

- 참조 상등 비교를 강제

- 참조 형식이라고 해서 반드시 참조 상등을 따른다는 보장이 없기에 사용

 * 가상 Equals 메서드 재정의

 * == 연산자의 중복적재

 

 

IEquatable<T> 인터페이스 (System)

값 형식 또는 클래스에서 인스턴스의 같음을 결정하는 형식별 메서드를 만들기 위해 구현하는 일반화된 메서드를 정의합니다.

learn.microsoft.com

namespace System
{
    public interface IEquatable<T>
    {
        bool Equals(T other);
    }
}

- object.Equals를 호출하면 값 형식의 피연산자들에 대해 반드시 박싱이 적용 됨. 이는 실제 비교해 비해 비싼 연산

- IEquatable<T>를 구현한 형식에 대해 Equals를 호출하면 object.Equals와 같은 결과가 나오고, 속도도 빠름.

 

Equals와 == 연산자가 같지 않은 경우

- NaN과 같은 경우 어떤 것과도 같지 않다고 판정 (같은 NaN이더라도)

- 반면 Equals는 반사적(Reflexive) 상등을 따름 (x.Equals(x)는 항상 true)

 * 컬렉션이나 사전 자료구조는 Equals가 이런 식으로 행동한다고 가정

 

- 값 형식: Equals와 ==가 다른 의미의 상등일 경우가 드뭄

- 참조 형식: Equals는 값 형식을 따르도록 커스텀, ==는 참조 상등을 적용하도록 그대로 두는 편

 

- 굳이 동일한 방식을 쓰지 않는 이유

 1. 첫 피연산자가 null이면 NullReferenceException 예외 발생, Equals의 호출이 실패. 정적 연산자는 그렇지 않음

 2. == 연산자는 정적으로 결정되므로 실행속도가 빠름.

 3. ==와 Equals에게 각자 다른 의미의 상등 비교를 수행하게 하는 것이 유용한 경우가 종종 있음.

 

상등과 커스텀 형식

- 형식을 직접 작성할 때에는 기본 행동방식 이외의 방식을 구현하는 것이 바람직할 때가 있음.

 * 상등의 의미를 바꾸기 위해

 * 구조체들의 상등 비교를 좀 더 빨리 수행하기 위해

 

상등의 의미를 변경

- ==와 Equals의 기본 행동방식이 만드는 형식에 대해 자연스럽지 않거나, 최종 사용자가 기대하는 것과는 다르다면

  상등의 의미를 변경하는 것이 합리적

- DateTimeOffset: UTC DateTime만 비교하고, Offset 필드는 비교하지 않음

- float, double: 상등 비교에서 NaN 비교 논리를 지원

- System.Uri, System.String: 참조 상등 대신 값 상등 사용

 

Uri 클래스 (System)

URI(Uniform Resource Indentifier)의 개체 표현을 제공하며 URI 부분에 쉽게 액세스할 수 있도록 합니다.

learn.microsoft.com

 

구조체 상등 비교 속도 높히기

- Equals를 재정의하면 상등 비교 속도를 5배 정도 빠르게 할 수 있음

- == 연산자를 중복적재하고 IEquatable<T>를 구현하면 박싱 없는 상등 비교가 가능해져 속도를 또 다섯 배 높일 수 있음

- 구조체의 해싱 알고리즘을 개선하여 해시 테이블의 성능을 높힘

 

GetHashCode의 재정의

- 해시테이블(Hashtable) 자료구조

 * System.Collections.Hashtable

 

Hashtable 클래스 (System.Collections)

키의 해시 코드에 따라 구성된 키/값 쌍의 컬렉션을 나타냅니다.

learn.microsoft.com

 * System.Collections.Generic.Dictionary<TKey,TValue>

 

Dictionary<TKey,TValue> 클래스 (System.Collections.Generic)

키와 값의 컬렉션을 나타냅니다.

learn.microsoft.com

 

- 참조 형식들, 값 형식들 모두 GetHashCode의 기본 구현이 갖추어져 있으므로 직접 재정의 할 필요는 없음

 * 구조체 - 런타임이 결정하여 구현, 구조체의 모든 필드를 해싱하는 방식(느림)의 구현이 적용 될 수도 있음

 * 클래스 - 내부 객체 토큰에 기초

 

- object.GetHashCode의 재정의 규칙

 1. Equals가 true를 돌려주는 두 객체에 대해 GetHashCode도 반드시 true를 돌려주어야 함

 2. 예외를 던지면 안 됨

 3. 같은 객체에 대해 되풀이해서 호출 했을 때 항상 같은 값을 돌려주어야 함(객체가 변하지 않았을 때)

 

- Equals, GetHashCode 중 하나가 재정의 된다면 다른 한쪽도 같이 재정의 해줘야 함

반응형

Equals를 재정의 할 때 지켜야 할 규칙

- object.Equals에는 다음과 같은 공리들이 성립

 * 객체와 null은 상등이 되지 않음 (널 가능 형식이 아닌 한)

 * 반사적(Reflexiv) : 객채는 자신과 상등

 * 가환적(Commutative) : 만일 a.Equals(b)이면 b.Equals(a)도 true

 * 추이적(transitive, 전이적) : 만일 a.Equals(b)이고, b.Equals(c)이면 a.Equals(c)도 true

 * 상등 연산은 되풀이 될 수 있고, 안정적 : 예외를 던지지 않음

 

==와 !=의 중복적재

- 구조체

 * 구조체에서는 거의 항상 이 연산자들을 중복 적재

 

- 클래스

 * 그대로 둠. 즉, 기본 형식인 참조 상등을 적용

  : 커스텀 형식들, 특히 가변이(mutable) 형식들에서 주로 사용

 * Equals에 맞게 중복적재

  : 소비자가 절대로 참조 상등을 원하지 않을 형식들에서 주로 사용(string, System.Uri, 일부 Struct)

 

IEquatable<T>의 구현

- Equals를 재정의 할 때 IEquatable<T>도 같이 구현하는것이 좋음

 

GetHashCode의 구현

- 죠슈아 블로크(Joshua Bloch)가 제안한 패턴, 무난한 성능 기대 가능

 int hash = 17;   // 임의의 소수

 int prime = 31;  // 또 다른 소수

 hash = hash * prime + field1.GetHashCode();

 hash = hash * prime + field2.GetHashCode();

 ...

 return hash;

using System;
 
namespace Practice
{
    class Program
    {
        static void Main(string[] args)
        {
            object x = 5;
            object y = 5;
 
            // 가상 Object.Equals
            Console.WriteLine(x.Equals(y));
 
            // 정적 object.Equals
            Console.WriteLine(Equals(x, y));
 
            // 참조 상등 비교 강제
            Console.WriteLine(ReferenceEquals(x, y));
            Console.WriteLine();
 
 
            // NaN의 비교
            double z = double.NaN;
            Console.WriteLine(z == z);
            Console.WriteLine(z.Equals(z));
            Console.WriteLine(ReferenceEquals(z, z));
            Console.WriteLine();
 
 
            // 예제: Area 구조체 사용
            Area a1 = new Area(5, 10);
            Area a2 = new Area(10, 5);
            Console.WriteLine(a1.Equals(a2));
            Console.WriteLine(a1 == a2);
 
        }
 
        // 예제: Area 구조체 구현
        public struct Area : IEquatable<Area>
        {
            public readonly int[] measure;
 
            public Area (int m1, int m2)
            {
                measure = new int[2];
                measure[0] = Math.Min(m1, m2);
                measure[1] = Math.Max(m1, m2);
            }
 
            public override bool Equals(object other)
            {
                // other(object)가 Area 형식으로 변환이 불가능하다면
                if(!(other is Area))
                {
                    return false;
                }
                else
                {
                    // 인터페이스로 구현한 메소드 호출
                    return Equals((Area) other);
                }
            }
 
            // 인터페이스 구현
            public bool Equals(Area other)
                => measure[0] == other.measure[0] && measure[1] == other.measure[1];
 
            // 31은 임의로 선택한 소수
            public override int GetHashCode()
                => measure[1] * 31 + measure[0];
 
            // == 연산자 오버로딩
            public static bool operator ==(Area a1, Area a2) => a1.Equals(a2);
 
            // != 연산자 오버로딩
            public static bool operator !=(Area a1, Area a2) => !a1.Equals(a2);
        }
    }
}

 

 

 

반응형

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

[C#] Process  (1) 2023.08.29
[C#] Environment  (0) 2023.08.28
[C#] Console  (1) 2023.08.27
[C#] 순서 비교 (IComparable<T>, IComparable)  (1) 2023.08.26
[C#] Guid  (0) 2023.08.24
[C#] Math  (1) 2023.08.23
[C#] Tuple  (1) 2023.08.22
[C#] 열거형과 System.Enum  (1) 2023.08.21