클래스 또는 구조체에 대한 값 같음을 정의하는 방법(C# 프로그래밍 가이드)

레코드는 값 같음을 자동으로 구현합니다. 형식이 데이터를 모델링하고 값 같음을 구현해야 하는 경우 class 대신 record를 정의하는 것이 좋습니다.

클래스 또는 구조체를 정의할 때 형식에 대한 값 같음(또는 동등)의 사용자 지정 정의를 만드는 것이 적합한지 결정합니다. 일반적으로 형식의 개체를 컬렉션에 추가해야 하는 경우 또는 주요 용도가 필드 또는 속성 세트 저장인 경우 값 같음을 구현합니다. 형식의 모든 필드 및 속성 비교를 기준으로 값 같음의 정의를 만들거나, 하위 집합을 기준으로 정의를 만들 수 있습니다.

두 경우 모두 및 클래스와 구조체 둘 다에서 구현은 다음과 같은 동등의 5가지 보장 사항을 따라야 합니다(다음 규칙의 경우 x, y, z가 null이 아닌 것으로 가정).

  1. 재귀 속성: x.Equals(x)true를 반환합니다.

  2. 대칭 속성: x.Equals(y)y.Equals(x)와 같은 값을 반환합니다.

  3. 전이적 속성: (x.Equals(y) && y.Equals(z))true를 반환하면 x.Equals(z)true를 반환합니다.

  4. x.Equals(y)의 연속 호출은 x 및 y에서 참조하는 개체가 수정되지 않는 한, 같은 값을 반환합니다.

  5. null이 아닌 값은 null과 같지 않습니다. 그러나 x가 null이면 x.Equals(y)는 예외를 throw합니다. Equals에 대한 인수에 따라 규칙 1 또는 2가 위반됩니다.

정의하는 모든 구조체에는 Object.Equals(Object) 메서드의 System.ValueType 재정의에서 상속하는 값 같음의 기본 구현이 이미 있습니다. 이 구현은 리플렉션을 사용하여 형식의 모든 필드와 속성을 검사합니다. 이 구현은 올바른 결과를 생성하지만 해당 형식에 맞게 작성한 사용자 지정 구현에 비해 비교적 속도가 느립니다.

값 같음에 대한 구현 세부 정보는 클래스 및 구조체에서 서로 다릅니다. 그러나 클래스와 구조체는 둘 다 같음 구현을 위해 동일한 기본 단계가 필요합니다.

  1. virtual Object.Equals(Object) 메서드를 재정의합니다. 대부분의 경우 bool Equals( object obj ) 구현에서 System.IEquatable<T> 인터페이스 구현인 형식별 Equals 메서드만 호출하면 됩니다. 2단계를 참조하세요.

  2. 형식별 Equals 메서드를 제공하여 System.IEquatable<T> 인터페이스를 구현합니다. 여기서 실제 동등 비교가 수행됩니다. 예를 들어 형식에서 한 개나 두 개의 필드만 비교하여 같음 정의를 결정할 수도 있습니다. Equals에서 예외를 throw하지 않습니다. 상속과 관련된 클래스의 경우:

    • 이 메서드는 클래스에 선언된 필드만 검사해야 합니다. base.Equals를 호출하여 기본 클래스에 있는 필드를 검사해야 합니다. (Object.Equals(Object)Object 구현에서 참조 같음 검사를 수행하므로 형식이 Object에서 직접 상속하는 경우에는 base.Equals를 호출하지 마세요.)

    • 비교하는 변수의 런타임 형식이 같으면 두 변수는 같은 것으로 간주되어야 합니다. 또한 변수의 런타임 형식과 컴파일 시간 형식이 다른 경우 런타임 형식에 대한 Equals 메서드의 IEquatable 구현이 사용되는지 확인합니다. 런타임 형식이 항상 올바르게 비교되도록 하기 위한 한 가지 전략은 sealed 클래스에서만 IEquatable을 구현하는 것입니다. 자세한 내용은 이 문서 뒷부분의 코드 예제를 참조하세요.

  3. 선택 사항이지만 권장됨: ==!= 연산자를 오버로드합니다.

  4. 값이 같은 두 개체가 동일한 해시 코드를 생성하도록 Object.GetHashCode를 재정의합니다.

  5. 선택 사항: “보다 큼” 또는 “보다 작음”에 대한 정의를 지원하기 위해 형식에 대한 IComparable<T> 인터페이스를 구현하고 <=>= 연산자도 오버로드합니다.

참고 항목

불필요한 상용구 코드 없이 레코드를 사용하여 값 평등 의미 체계를 가져올 수 있습니다.

클래스 예제

다음 예제에서는 클래스(참조 형식)에서 값 같음을 구현하는 방법을 보여 줍니다.

namespace ValueEqualityClass;

class TwoDPoint : IEquatable<TwoDPoint>
{
    public int X { get; private set; }
    public int Y { get; private set; }

    public TwoDPoint(int x, int y)
    {
        if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
        {
            throw new ArgumentException("Point must be in range 1 - 2000");
        }
        this.X = x;
        this.Y = y;
    }

    public override bool Equals(object obj) => this.Equals(obj as TwoDPoint);

    public bool Equals(TwoDPoint p)
    {
        if (p is null)
        {
            return false;
        }

        // Optimization for a common success case.
        if (Object.ReferenceEquals(this, p))
        {
            return true;
        }

        // If run-time types are not exactly the same, return false.
        if (this.GetType() != p.GetType())
        {
            return false;
        }

        // Return true if the fields match.
        // Note that the base class is not invoked because it is
        // System.Object, which defines Equals as reference equality.
        return (X == p.X) && (Y == p.Y);
    }

    public override int GetHashCode() => (X, Y).GetHashCode();

    public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
    {
        if (lhs is null)
        {
            if (rhs is null)
            {
                return true;
            }

            // Only the left side is null.
            return false;
        }
        // Equals handles case of null on right side.
        return lhs.Equals(rhs);
    }

    public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
}

// For the sake of simplicity, assume a ThreeDPoint IS a TwoDPoint.
class ThreeDPoint : TwoDPoint, IEquatable<ThreeDPoint>
{
    public int Z { get; private set; }

    public ThreeDPoint(int x, int y, int z)
        : base(x, y)
    {
        if ((z < 1) || (z > 2000))
        {
            throw new ArgumentException("Point must be in range 1 - 2000");
        }
        this.Z = z;
    }

    public override bool Equals(object obj) => this.Equals(obj as ThreeDPoint);

    public bool Equals(ThreeDPoint p)
    {
        if (p is null)
        {
            return false;
        }

        // Optimization for a common success case.
        if (Object.ReferenceEquals(this, p))
        {
            return true;
        }

        // Check properties that this class declares.
        if (Z == p.Z)
        {
            // Let base class check its own fields
            // and do the run-time type comparison.
            return base.Equals((TwoDPoint)p);
        }
        else
        {
            return false;
        }
    }

    public override int GetHashCode() => (X, Y, Z).GetHashCode();

    public static bool operator ==(ThreeDPoint lhs, ThreeDPoint rhs)
    {
        if (lhs is null)
        {
            if (rhs is null)
            {
                // null == null = true.
                return true;
            }

            // Only the left side is null.
            return false;
        }
        // Equals handles the case of null on right side.
        return lhs.Equals(rhs);
    }

    public static bool operator !=(ThreeDPoint lhs, ThreeDPoint rhs) => !(lhs == rhs);
}

class Program
{
    static void Main(string[] args)
    {
        ThreeDPoint pointA = new ThreeDPoint(3, 4, 5);
        ThreeDPoint pointB = new ThreeDPoint(3, 4, 5);
        ThreeDPoint pointC = null;
        int i = 5;

        Console.WriteLine("pointA.Equals(pointB) = {0}", pointA.Equals(pointB));
        Console.WriteLine("pointA == pointB = {0}", pointA == pointB);
        Console.WriteLine("null comparison = {0}", pointA.Equals(pointC));
        Console.WriteLine("Compare to some other type = {0}", pointA.Equals(i));

        TwoDPoint pointD = null;
        TwoDPoint pointE = null;

        Console.WriteLine("Two null TwoDPoints are equal: {0}", pointD == pointE);

        pointE = new TwoDPoint(3, 4);
        Console.WriteLine("(pointE == pointA) = {0}", pointE == pointA);
        Console.WriteLine("(pointA == pointE) = {0}", pointA == pointE);
        Console.WriteLine("(pointA != pointE) = {0}", pointA != pointE);

        System.Collections.ArrayList list = new System.Collections.ArrayList();
        list.Add(new ThreeDPoint(3, 4, 5));
        Console.WriteLine("pointE.Equals(list[0]): {0}", pointE.Equals(list[0]));

        // Keep the console window open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}

/* Output:
    pointA.Equals(pointB) = True
    pointA == pointB = True
    null comparison = False
    Compare to some other type = False
    Two null TwoDPoints are equal: True
    (pointE == pointA) = False
    (pointA == pointE) = False
    (pointA != pointE) = True
    pointE.Equals(list[0]): False
*/

클래스(참조 형식)에서 두 Object.Equals(Object) 메서드의 기본 구현은 값이 같은지 검사하지 않고 참조가 같은지 비교합니다. 구현자가 가상 메서드를 재정의하는 경우 값 같음 의미 체계를 제공하기 위한 것입니다.

클래스에서 재정의하지 않는 경우에도 ==!= 연산자를 클래스와 함께 사용할 수 있습니다. 그러나 기본 동작은 참조가 같은지 검사하는 것입니다. 클래스에서 Equals 메서드를 오버로드하는 경우 ==!= 연산자를 오버로드해야 하지만 필수는 아닙니다.

Important

앞의 예제 코드는 모든 상속 시나리오를 원하는 방식으로 처리하지 않을 수 있습니다. 다음 코드를 생각해 봅시다.

TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
Console.WriteLine(p1.Equals(p2)); // output: True

이 코드는 z 값의 차이에도 불구하고 p1p2와 같다고 보고합니다. 컴파일러가 컴파일 시간 형식에 따라 IEquatableTwoDPoint 구현을 선택하므로 차이는 무시됩니다.

record 형식의 기본 제공 값 같음이 이와 같은 시나리오를 올바르게 처리합니다. TwoDPointThreeDPointrecord 형식인 경우 p1.Equals(p2)의 결과는 False가 됩니다. 자세한 내용은 record 형식 상속 계층 구조의 같음을 참조하세요.

구조체 예제

다음 예제에서는 구조체(값 형식)에서 값 같음을 구현하는 방법을 보여 줍니다.

namespace ValueEqualityStruct
{
    struct TwoDPoint : IEquatable<TwoDPoint>
    {
        public int X { get; private set; }
        public int Y { get; private set; }

        public TwoDPoint(int x, int y)
            : this()
        {
            if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
            {
                throw new ArgumentException("Point must be in range 1 - 2000");
            }
            X = x;
            Y = y;
        }

        public override bool Equals(object? obj) => obj is TwoDPoint other && this.Equals(other);

        public bool Equals(TwoDPoint p) => X == p.X && Y == p.Y;

        public override int GetHashCode() => (X, Y).GetHashCode();

        public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs) => lhs.Equals(rhs);

        public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
    }

    class Program
    {
        static void Main(string[] args)
        {
            TwoDPoint pointA = new TwoDPoint(3, 4);
            TwoDPoint pointB = new TwoDPoint(3, 4);
            int i = 5;

            // True:
            Console.WriteLine("pointA.Equals(pointB) = {0}", pointA.Equals(pointB));
            // True:
            Console.WriteLine("pointA == pointB = {0}", pointA == pointB);
            // True:
            Console.WriteLine("object.Equals(pointA, pointB) = {0}", object.Equals(pointA, pointB));
            // False:
            Console.WriteLine("pointA.Equals(null) = {0}", pointA.Equals(null));
            // False:
            Console.WriteLine("(pointA == null) = {0}", pointA == null);
            // True:
            Console.WriteLine("(pointA != null) = {0}", pointA != null);
            // False:
            Console.WriteLine("pointA.Equals(i) = {0}", pointA.Equals(i));
            // CS0019:
            // Console.WriteLine("pointA == i = {0}", pointA == i);

            // Compare unboxed to boxed.
            System.Collections.ArrayList list = new System.Collections.ArrayList();
            list.Add(new TwoDPoint(3, 4));
            // True:
            Console.WriteLine("pointA.Equals(list[0]): {0}", pointA.Equals(list[0]));

            // Compare nullable to nullable and to non-nullable.
            TwoDPoint? pointC = null;
            TwoDPoint? pointD = null;
            // False:
            Console.WriteLine("pointA == (pointC = null) = {0}", pointA == pointC);
            // True:
            Console.WriteLine("pointC == pointD = {0}", pointC == pointD);

            TwoDPoint temp = new TwoDPoint(3, 4);
            pointC = temp;
            // True:
            Console.WriteLine("pointA == (pointC = 3,4) = {0}", pointA == pointC);

            pointD = temp;
            // True:
            Console.WriteLine("pointD == (pointC = 3,4) = {0}", pointD == pointC);

            Console.WriteLine("Press any key to exit.");
            Console.ReadKey();
        }
    }

    /* Output:
        pointA.Equals(pointB) = True
        pointA == pointB = True
        Object.Equals(pointA, pointB) = True
        pointA.Equals(null) = False
        (pointA == null) = False
        (pointA != null) = True
        pointA.Equals(i) = False
        pointE.Equals(list[0]): True
        pointA == (pointC = null) = False
        pointC == pointD = True
        pointA == (pointC = 3,4) = True
        pointD == (pointC = 3,4) = True
    */
}

구조체의 경우 Object.Equals(Object)의 기본 구현(System.ValueType의 재정의된 버전)에서 리플렉션을 통해 형식에 있는 모든 필드의 값을 비교하여 값이 같은지 검사합니다. 구현자가 구조체의 가상 Equals 메서드를 재정의하는 경우 값이 같은지 검사하는 보다 효율적인 수단을 제공하고 필요에 따라 구조체 필드 또는 속성의 하위 집합을 기준으로 비교하기 위한 것입니다.

==!= 연산자는 구조체에서 명시적으로 오버로드하지 않는 한 구조체에 대해 연산을 수행할 수 없습니다.

참고 항목